<?php

namespace App\Console\Commands;

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

class AuthorizationCoverageAudit extends Command
{
    protected $signature = 'authorization:coverage-audit
        {--json : Emit the complete coverage report as JSON}
        {--strict : Fail when an authorization surface is uncovered}';

    protected $description = 'Audit HTTP, policy, console, scheduler, queue, and Blade authorization coverage';

    public function handle(): int
    {
        $routeCoverage = $this->routeCoverage();
        $policyCoverage = $this->policyCoverage();
        $executionCoverage = $this->executionCoverage();
        $bladeCoverage = $this->bladeCoverage();

        $errors = array_values(array_merge(
            $routeCoverage['errors'],
            $policyCoverage['errors'],
            $executionCoverage['errors'],
        ));

        $report = [
            'schema_version' => 1,
            'status' => $errors === [] ? 'pass' : 'fail',
            'summary' => [
                'application_routes' => count($routeCoverage['routes']),
                'permission_protected_routes' => $routeCoverage['permission_protected'],
                'policy_protected_routes' => $routeCoverage['policy_protected'],
                'explicit_route_exemptions' => $routeCoverage['explicit_exemptions'],
                'policy_files' => count($policyCoverage['discovered']),
                'registered_policies' => count($policyCoverage['registered']),
                'application_commands' => count($executionCoverage['commands']),
                'scheduled_commands' => count($executionCoverage['scheduled']),
                'queued_jobs' => count($executionCoverage['jobs']),
                'blade_authorization_call_sites' => $bladeCoverage['authorization_call_sites'],
                'errors' => count($errors),
            ],
            'routes' => $routeCoverage['routes'],
            'policies' => $policyCoverage,
            'execution_surfaces' => $executionCoverage,
            'blade' => $bladeCoverage,
            'errors' => $errors,
        ];

        $json = json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);

        if ($this->option('json')) {
            $this->line($json);
        } else {
            $this->info('Authorization Phase 9 full coverage audit');
            $this->line('Application routes: '.$report['summary']['application_routes']);
            $this->line('Policy files / registered: '.$report['summary']['policy_files'].' / '.$report['summary']['registered_policies']);
            $this->line('Commands / scheduled / jobs: '.$report['summary']['application_commands'].' / '.$report['summary']['scheduled_commands'].' / '.$report['summary']['queued_jobs']);

            foreach ($errors as $error) {
                $this->error($error['message']);
            }

            if ($errors === []) {
                $this->info('Full authorization coverage audit passed.');
            }
        }

        return $this->option('strict') && $errors !== [] ? self::FAILURE : self::SUCCESS;
    }

    /** @return array<string, mixed> */
    private function routeCoverage(): array
    {
        $selfService = config('authorization_inventory.self_service_routes', []);
        $authenticatedExemptions = config('authorization_inventory.authenticated_exemptions', []);
        $public = config('authorization_inventory.public_routes', []);
        $declaredExemptions = array_merge($selfService, $authenticatedExemptions, $public);
        $errors = [];
        $permissionProtected = 0;
        $policyProtected = 0;
        $explicitExemptions = 0;

        $routes = collect(Route::getRoutes()->getRoutes())
            ->map(function (RoutingRoute $route) use ($declaredExemptions, &$errors, &$permissionProtected, &$policyProtected, &$explicitExemptions): ?array {
                $name = $route->getName();
                $action = $route->getActionName();
                $applicationManaged = str_starts_with($action, 'App\\') || ($name !== null && array_key_exists($name, $declaredExemptions));

                if (! $applicationManaged) {
                    return null;
                }

                $middleware = array_values($route->gatherMiddleware());
                $permissions = $this->middlewareValues($middleware, 'permission:');
                $abilities = $this->middlewareValues($middleware, 'can:');
                $explicitExemption = $name !== null && array_key_exists($name, $declaredExemptions);

                if ($permissions !== []) {
                    $classification = 'permission-protected';
                    $permissionProtected++;
                } elseif ($abilities !== []) {
                    $classification = 'policy-protected';
                    $policyProtected++;
                } elseif ($explicitExemption) {
                    $classification = 'explicit-exemption';
                    $explicitExemptions++;
                } else {
                    $classification = 'uncovered';
                    $errors[] = [
                        'type' => 'uncovered_application_route',
                        'surface' => (string) ($name ?? $route->uri()),
                        'message' => sprintf('Application route %s (%s) has neither permission/policy middleware nor an explicit authorization exemption.', $name ?? '[unnamed]', $route->uri()),
                    ];
                }

                return [
                    'name' => $name,
                    'uri' => $route->uri(),
                    'methods' => array_values(array_diff($route->methods(), ['HEAD'])),
                    'permissions' => $permissions,
                    'abilities' => $abilities,
                    'classification' => $classification,
                ];
            })
            ->filter()
            ->sortBy(fn (array $route) => ($route['name'] ?? '').'|'.$route['uri'])
            ->values()
            ->all();

        return [
            'routes' => $routes,
            'permission_protected' => $permissionProtected,
            'policy_protected' => $policyProtected,
            'explicit_exemptions' => $explicitExemptions,
            'errors' => $errors,
        ];
    }

    /** @param array<int, mixed> $middleware @return array<int, string> */
    private function middlewareValues(array $middleware, string $prefix): array
    {
        return collect($middleware)
            ->filter(fn ($item) => is_string($item) && str_starts_with($item, $prefix))
            ->flatMap(function (string $item) use ($prefix): array {
                $value = substr($item, strlen($prefix));
                if ($prefix === 'can:') {
                    $value = explode(',', $value, 2)[0];
                }

                return preg_split('/[|,]/', $value) ?: [];
            })
            ->map(fn ($value) => trim((string) $value))
            ->filter()
            ->unique()
            ->values()
            ->all();
    }

    /** @return array<string, mixed> */
    private function policyCoverage(): array
    {
        $policyPath = app_path('Policies');
        $discovered = File::isDirectory($policyPath)
            ? collect(File::files($policyPath))->map(fn ($file) => $file->getBasename('.php'))->sort()->values()->all()
            : [];

        $provider = File::get(app_path('Providers/AppServiceProvider.php'));
        preg_match_all('/Gate::policy\([^,]+,\s*([A-Za-z0-9_]+)::class\)/', $provider, $matches);
        $registered = collect($matches[1] ?? [])->unique()->sort()->values()->all();
        $missing = array_values(array_diff($discovered, $registered));
        $errors = collect($missing)->map(fn (string $policy) => [
            'type' => 'unregistered_policy',
            'surface' => $policy,
            'message' => sprintf('Policy %s exists in app/Policies but is not explicitly registered in AppServiceProvider.', $policy),
        ])->all();

        return [
            'discovered' => $discovered,
            'registered' => $registered,
            'missing' => $missing,
            'errors' => $errors,
        ];
    }

    /** @return array<string, mixed> */
    private function executionCoverage(): array
    {
        $errors = [];
        $commands = [];
        foreach (File::files(app_path('Console/Commands')) as $file) {
            $source = File::get($file->getPathname());
            if (preg_match('/protected\s+\$signature\s*=\s*[\'\"]([^\'\"\r\n]+)/', $source, $match) !== 1) {
                $errors[] = [
                    'type' => 'unclassified_application_command',
                    'surface' => $file->getFilename(),
                    'message' => sprintf('Application command %s has no statically auditable signature.', $file->getFilename()),
                ];
                continue;
            }

            $signature = trim($match[1]);
            $commands[] = preg_split('/\s+/', $signature)[0];
        }
        $commands = collect($commands)->filter()->unique()->sort()->values()->all();

        $consoleRoutes = File::get(base_path('routes/console.php'));
        preg_match_all('/Schedule::command\(\s*[\'\"]([^\'\"]+)/', $consoleRoutes, $scheduledMatches);
        $scheduled = collect($scheduledMatches[1] ?? [])->unique()->sort()->values()->all();

        foreach (array_diff($scheduled, $commands) as $command) {
            $errors[] = [
                'type' => 'unresolved_scheduled_command',
                'surface' => $command,
                'message' => sprintf('Scheduled command %s is not backed by an application command signature.', $command),
            ];
        }

        $jobs = File::isDirectory(app_path('Jobs'))
            ? collect(File::files(app_path('Jobs')))->map(fn ($file) => $file->getBasename('.php'))->sort()->values()->all()
            : [];

        foreach ($jobs as $job) {
            $source = File::get(app_path('Jobs/'.$job.'.php'));
            if (! str_contains($source, 'function handle(')) {
                $errors[] = [
                    'type' => 'unclassified_queue_job',
                    'surface' => $job,
                    'message' => sprintf('Queued execution surface %s has no auditable handle method.', $job),
                ];
            }
        }

        return [
            'commands' => $commands,
            'command_classification' => 'operator-console unless listed as scheduled-system',
            'scheduled' => $scheduled,
            'scheduled_classification' => 'system-scheduler',
            'jobs' => $jobs,
            'job_classification' => 'internal-queued-execution; caller authorization remains at the dispatch boundary',
            'errors' => $errors,
        ];
    }

    /** @return array<string, int> */
    private function bladeCoverage(): array
    {
        $files = File::isDirectory(resource_path('views')) ? File::allFiles(resource_path('views')) : [];
        $authorizationCallSites = 0;
        $stateChangingForms = 0;

        foreach ($files as $file) {
            if (! str_ends_with($file->getFilename(), '.blade.php')) {
                continue;
            }

            $source = File::get($file->getPathname());
            $authorizationCallSites += preg_match_all('/@(can|cannot|canany)\s*\(/i', $source) ?: 0;
            $stateChangingForms += preg_match_all('/@method\(\s*[\'\"](POST|PUT|PATCH|DELETE)[\'\"]\s*\)/i', $source) ?: 0;
        }

        return [
            'authorization_call_sites' => $authorizationCallSites,
            'state_changing_form_markers' => $stateChangingForms,
        ];
    }
}
