<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Routing\Route as RoutingRoute;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

/**
 * PRC-266 — Permission key drift audit.
 *
 * Compares three sources of truth for permission keys:
 *   1. `permissions` table   — what's actually in the DB
 *   2. seeders               — what's defined in PermissionSeeder + AdminSeeder
 *   3. routes                — what's referenced via `can:X` / `permission:X`
 *                              middleware on registered routes
 *
 * Reports two error classes:
 *   - MISSING: route references a key that doesn't exist in seeders/DB
 *              (route would 403 for everyone, including super-admin)
 *   - ORPHAN:  seeder/DB has a key no route consumes (dead permission;
 *              waste of role storage + confusing UI in role management)
 *
 * Plus an INFO row for keys that exist in DB but not in the canonical
 * seeder (drift between AdminSeeder/PermissionSeeder/migration backfill).
 */
class PermissionsCheckDriftCommand extends Command
{
    protected $signature = 'permissions:check-drift {--json : Emit machine-readable JSON instead of a table}';

    protected $description = 'Audit permission key drift between routes, seeders, and the DB';

    public function handle(): int
    {
        $fromRoutes  = $this->collectRouteKeys();
        $fromSeeders = $this->collectSeederKeys();
        $fromDb      = $this->collectDbKeys();

        $allDefined = $fromSeeders->merge($fromDb)->unique()->values();

        $missing = $fromRoutes->reject(fn ($k) => $allDefined->contains($k))->values();
        $orphan  = $allDefined->reject(fn ($k) => $fromRoutes->contains($k))->values();
        $seederDrift = $fromDb->reject(fn ($k) => $fromSeeders->contains($k))->values();

        if ($this->option('json')) {
            $this->line(json_encode([
                'missing'      => $missing->all(),
                'orphan'       => $orphan->all(),
                'seeder_drift' => $seederDrift->all(),
                'counts'       => [
                    'routes'  => $fromRoutes->count(),
                    'seeders' => $fromSeeders->count(),
                    'db'      => $fromDb->count(),
                ],
            ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

            return self::SUCCESS;
        }

        $this->info("Permission audit results:");
        $this->line("  routes  references : {$fromRoutes->count()}");
        $this->line("  seeders definitions: {$fromSeeders->count()}");
        $this->line("  database rows      : {$fromDb->count()}");
        $this->newLine();

        if ($missing->isNotEmpty()) {
            $this->error("MISSING — referenced in routes, not defined in seeders/DB ({$missing->count()}):");
            foreach ($missing as $k) {
                $this->line("  - {$k}");
            }
            $this->newLine();
        }

        if ($orphan->isNotEmpty()) {
            $this->warn("ORPHAN — defined in seeders/DB, not used by any route ({$orphan->count()}):");
            foreach ($orphan as $k) {
                $this->line("  - {$k}");
            }
            $this->newLine();
        }

        if ($seederDrift->isNotEmpty()) {
            $this->comment("INFO — present in DB but missing from PermissionSeeder/AdminSeeder ({$seederDrift->count()}):");
            foreach ($seederDrift as $k) {
                $this->line("  - {$k}");
            }
            $this->newLine();
        }

        if ($missing->isEmpty() && $orphan->isEmpty() && $seederDrift->isEmpty()) {
            $this->info('No drift detected. Routes, seeders, and DB agree.');
        }

        // Non-zero only if MISSING keys exist (will 403 in prod). ORPHAN and
        // INFO are advisory.
        return $missing->isEmpty() ? self::SUCCESS : self::FAILURE;
    }

    /**
     * Extract permission keys from registered route middleware.
     *
     * Detects two forms:
     *   - `can:<key>`         — Laravel's built-in
     *   - `permission:<key>`  — Spatie\Permission's Middleware
     *
     * Multiple keys can be pipe-delimited (Spatie). Stripping is best-effort.
     */
    private function collectRouteKeys(): \Illuminate\Support\Collection
    {
        $keys = collect();

        /** @var \Illuminate\Routing\RouteCollection $routes */
        $routes = Route::getRoutes();

        foreach ($routes as $route) {
            /** @var RoutingRoute $route */
            foreach ($route->gatherMiddleware() as $mw) {
                if (! is_string($mw)) {
                    continue;
                }
                if (Str::startsWith($mw, 'can:')) {
                    $keys->push(trim(Str::after($mw, 'can:')));
                    continue;
                }
                if (Str::startsWith($mw, 'permission:')) {
                    foreach (explode('|', trim(Str::after($mw, 'permission:'))) as $k) {
                        $keys->push(trim($k));
                    }
                }
            }
        }

        return $keys->filter()->unique()->sort()->values();
    }

    /**
     * Extract permission keys from seeder source files.
     *
     * Static regex scan of PermissionSeeder + AdminSeeder. Avoids running
     * the seeder (which would require a fresh DB) and avoids the Spatie
     * Permission cache.
     */
    private function collectSeederKeys(): \Illuminate\Support\Collection
    {
        $files = [
            database_path('seeders/PermissionSeeder.php'),
            database_path('seeders/AdminSeeder.php'),
        ];

        $keys = collect();

        foreach ($files as $f) {
            if (! File::exists($f)) {
                continue;
            }
            $contents = File::get($f);

            // Form A — 'name' => 'view clients'.
            // Permission keys are ALWAYS lowercase (`view clients`); admin user
            // names and labels (`Faturalama Admin`) start with a capital. Require
            // a lowercase first char to filter out non-permission `name` keys
            // (the seeder uses the same key for `Permission::create(['name'=>])`
            // AND `Admin::create(['name'=>])`).
            if (preg_match_all("/'name'\s*=>\s*'([a-z][^']*)'/", $contents, $m)) {
                foreach ($m[1] as $k) {
                    $keys->push($k);
                }
            }

            // Form B — bare string in $permissions = [ ... ] array
            // Match standalone quoted strings on a line that looks like:
            //     'view clients',
            // BUT only when they appear in a permissions-like context — i.e.
            // they contain a space (e.g. 'view clients') OR start with a known
            // verb prefix. Role names like 'super-admin' are excluded by both
            // checks.
            if (preg_match_all("/^\s*'([a-z][a-z0-9 _\-]+)'\s*,\s*$/m", $contents, $m)) {
                foreach ($m[1] as $k) {
                    if (str_contains($k, ' ')) {
                        $keys->push($k);
                    }
                }
            }
        }

        // Known false-positives in the form A capture that match the `'name'
        // => '...'` pattern but aren't permission keys. Drop them.
        $blacklist = [
            'admin', 'web', 'super-admin', 'user', 'ai-staff', 'client-manager',
            'client-viewer',
        ];

        return $keys
            ->filter(fn ($k) => ! in_array($k, $blacklist, true))
            ->unique()
            ->sort()
            ->values();
    }

    /**
     * Extract permission keys from the live `permissions` table (admin guard).
     */
    private function collectDbKeys(): \Illuminate\Support\Collection
    {
        if (! Schema::hasTable('permissions')) {
            return collect();
        }

        return DB::table('permissions')
            ->where('guard_name', 'admin')
            ->pluck('name')
            ->unique()
            ->sort()
            ->values();
    }
}
