<?php

namespace App\Console\Commands;

use App\Models\Permission;
use App\Models\Role;
use Illuminate\Console\Command;
use Illuminate\Routing\Route as RoutingRoute;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Throwable;

class AuthorizationInventory extends Command
{
    protected $signature = 'authorization:inventory
        {--json : Emit the complete inventory as JSON}
        {--strict : Fail when an application route or permission reference is unclassified}
        {--output= : Write the JSON inventory to this absolute or project-relative path}';

    protected $description = 'Export and validate the current roles, permissions, route protection, and authorization call sites without changing access';

    public function handle(): int
    {
        $routes = $this->routeInventory();
        $permissions = $this->permissionInventory();
        $roles = $this->roleInventory();
        $permissionNames = collect($permissions)->pluck('name')->all();
        $registeredRouteNames = collect($routes)->pluck('name')->filter()->all();

        $findings = $this->findings($routes, $permissionNames, $registeredRouteNames);
        $report = [
            'schema_version' => 1,
            'status' => $findings['errors'] === [] ? 'pass' : 'fail',
            'generated_at' => now()->toIso8601String(),
            'summary' => [
                'routes' => count($routes),
                'permissions' => count($permissions),
                'roles' => count($roles),
                'permission_call_sites' => 0,
                'role_name_call_sites' => 0,
                'errors' => count($findings['errors']),
                'warnings' => count($findings['warnings']),
            ],
            'routes' => $routes,
            'permissions' => $permissions,
            'roles' => $roles,
            'role_permission_matrix' => collect($roles)
                ->mapWithKeys(fn (array $role) => [$role['name'] => $role['permissions']])
                ->all(),
            'authorization_call_sites' => $this->sourceCallSites(),
            'allowlists' => [
                'self_service_routes' => config('authorization_inventory.self_service_routes', []),
                'authenticated_exemptions' => config('authorization_inventory.authenticated_exemptions', []),
                'public_routes' => config('authorization_inventory.public_routes', []),
            ],
            'legacy_broad_permissions' => config('authorization_inventory.legacy_broad_permissions', []),
            'findings' => $findings,
        ];

        $report['summary']['permission_call_sites'] = count($report['authorization_call_sites']['permission']);
        $report['summary']['role_name_call_sites'] = count($report['authorization_call_sites']['role_name']);

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

        if ($output = trim((string) $this->option('output'))) {
            $path = str_starts_with($output, DIRECTORY_SEPARATOR) ? $output : base_path($output);
            File::ensureDirectoryExists(dirname($path));
            File::put($path, $json.PHP_EOL);
        }

        if ($this->option('json')) {
            $this->line($json);
        } else {
            $this->info('Authorization Phase 0 inventory');
            $this->line('Routes: '.$report['summary']['routes']);
            $this->line('Permissions: '.$report['summary']['permissions']);
            $this->line('Roles: '.$report['summary']['roles']);
            $this->line('Permission call sites: '.$report['summary']['permission_call_sites']);
            $this->line('Role-name call sites: '.$report['summary']['role_name_call_sites']);

            if ($findings['errors'] === []) {
                $this->info('Authorization inventory classification passed.');
            } else {
                foreach ($findings['errors'] as $error) {
                    $this->error($error['message']);
                }
            }

            foreach ($findings['warnings'] as $warning) {
                $this->warn($warning['message']);
            }

            if ($output = trim((string) $this->option('output'))) {
                $this->line('Inventory written to '.$output);
            }
        }

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

        return self::SUCCESS;
    }

    /** @return array<int, array<string, mixed>> */
    private function routeInventory(): array
    {
        $selfService = config('authorization_inventory.self_service_routes', []);
        $authenticatedExemptions = config('authorization_inventory.authenticated_exemptions', []);
        $public = config('authorization_inventory.public_routes', []);

        return collect(Route::getRoutes()->getRoutes())
            ->map(function (RoutingRoute $route) use ($selfService, $authenticatedExemptions, $public): array {
                $middleware = array_values($route->gatherMiddleware());
                $name = $route->getName();
                $action = $route->getActionName();
                $permissions = collect($middleware)
                    ->filter(fn ($item) => is_string($item) && str_starts_with($item, 'permission:'))
                    ->flatMap(function (string $item): array {
                        $value = substr($item, strlen('permission:'));

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

                $modules = collect($middleware)
                    ->filter(fn ($item) => is_string($item) && str_starts_with($item, 'module:'))
                    ->map(fn (string $item) => substr($item, strlen('module:')))
                    ->values()
                    ->all();

                $authenticated = collect($middleware)->contains(function ($item): bool {
                    if (! is_string($item)) {
                        return false;
                    }

                    return $item === 'auth'
                        || str_starts_with($item, 'auth:')
                        || str_contains($item, 'Authenticate');
                });

                $applicationManaged = ($name !== null && (
                    array_key_exists($name, $selfService)
                    || array_key_exists($name, $authenticatedExemptions)
                    || array_key_exists($name, $public)
                )) || str_starts_with($action, 'App\\');

                $classification = 'framework-or-unclassified';
                if ($applicationManaged) {
                    if ($authenticated && $permissions !== []) {
                        $classification = 'permission-protected';
                    } elseif ($authenticated && $name !== null && array_key_exists($name, $selfService)) {
                        $classification = 'self-service';
                    } elseif ($authenticated && $name !== null && array_key_exists($name, $authenticatedExemptions)) {
                        $classification = 'authenticated-exemption';
                    } elseif (! $authenticated && $name !== null && array_key_exists($name, $public)) {
                        $classification = 'public';
                    } elseif ($authenticated) {
                        $classification = 'unclassified-authenticated';
                    } else {
                        $classification = 'unclassified-public';
                    }
                }

                return [
                    'methods' => array_values(array_diff($route->methods(), ['HEAD'])),
                    'uri' => $route->uri(),
                    'name' => $name,
                    'action' => $action,
                    'middleware' => $middleware,
                    'authenticated' => $authenticated,
                    'permissions' => $permissions,
                    'modules' => $modules,
                    'application_managed' => $applicationManaged,
                    'classification' => $classification,
                ];
            })
            ->sortBy(fn (array $route) => ($route['name'] ?? '').'|'.$route['uri'])
            ->values()
            ->all();
    }

    /** @return array<int, array<string, mixed>> */
    private function permissionInventory(): array
    {
        return Permission::query()
            ->with(['roles:id,name'])
            ->orderBy('name')
            ->get()
            ->map(fn (Permission $permission) => [
                'name' => $permission->name,
                'display_name' => $permission->display_name,
                'group' => $permission->group,
                'description' => $permission->description,
                'roles' => $permission->roles->pluck('name')->sort()->values()->all(),
            ])
            ->all();
    }

    /** @return array<int, array<string, mixed>> */
    private function roleInventory(): array
    {
        return Role::query()
            ->with(['permissions:id,name'])
            ->withCount('users')
            ->orderBy('name')
            ->get()
            ->map(fn (Role $role) => [
                'name' => $role->name,
                'display_name' => $role->display_name,
                'description' => $role->description,
                'is_system' => (bool) $role->is_system,
                'users_count' => (int) $role->users_count,
                'permissions' => $role->permissions->pluck('name')->sort()->values()->all(),
            ])
            ->all();
    }

    /**
     * @param array<int, array<string, mixed>> $routes
     * @param array<int, string> $permissionNames
     * @param array<int, string> $registeredRouteNames
     * @return array{errors: array<int, array<string, string>>, warnings: array<int, array<string, string>>}
     */
    private function findings(array $routes, array $permissionNames, array $registeredRouteNames): array
    {
        $errors = [];
        $warnings = [];

        foreach ($routes as $route) {
            if (! $route['application_managed']) {
                continue;
            }

            if (in_array($route['classification'], ['unclassified-authenticated', 'unclassified-public'], true)) {
                $errors[] = [
                    'type' => 'unclassified_route',
                    'route' => (string) ($route['name'] ?? $route['uri']),
                    'message' => sprintf(
                        'Application route %s (%s) has no explicit authorization classification.',
                        $route['name'] ?? '[unnamed]',
                        $route['uri'],
                    ),
                ];
            }

            foreach ($route['permissions'] as $permission) {
                if (! in_array($permission, $permissionNames, true)) {
                    $errors[] = [
                        'type' => 'missing_permission_record',
                        'route' => (string) ($route['name'] ?? $route['uri']),
                        'permission' => $permission,
                        'message' => sprintf(
                            'Route %s references permission %s, but that permission is not present after seeding.',
                            $route['name'] ?? $route['uri'],
                            $permission,
                        ),
                    ];
                }
            }
        }

        foreach (['self_service_routes', 'authenticated_exemptions', 'public_routes'] as $configKey) {
            foreach (array_keys(config('authorization_inventory.'.$configKey, [])) as $routeName) {
                if (! in_array($routeName, $registeredRouteNames, true)) {
                    $errors[] = [
                        'type' => 'stale_route_allowlist_entry',
                        'route' => $routeName,
                        'message' => sprintf('Authorization allowlist entry %s does not match a registered route.', $routeName),
                    ];
                }
            }
        }

        foreach (array_keys(config('authorization_inventory.legacy_broad_permissions', [])) as $permission) {
            if (! in_array($permission, $permissionNames, true)) {
                $errors[] = [
                    'type' => 'stale_legacy_permission_entry',
                    'permission' => $permission,
                    'message' => sprintf('Legacy permission registry expects %s, but it is absent after seeding.', $permission),
                ];
            }
        }

        $legacy = array_keys(config('authorization_inventory.legacy_broad_permissions', []));
        foreach ($routes as $route) {
            foreach (array_intersect($route['permissions'], $legacy) as $permission) {
                $warnings[] = [
                    'type' => 'legacy_permission_route_use',
                    'route' => (string) ($route['name'] ?? $route['uri']),
                    'permission' => $permission,
                    'message' => sprintf('Route %s still uses legacy/broad permission %s.', $route['name'] ?? $route['uri'], $permission),
                ];
            }
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /** @return array{permission: array<int, array<string, mixed>>, role_name: array<int, array<string, mixed>>} */
    private function sourceCallSites(): array
    {
        $permissionPatterns = [
            'permission_middleware' => '/permission:[A-Za-z0-9._-]+/',
            'custom_has_permission' => '/\\bhasPermission\\s*\\(/',
            'laravel_can' => '/(?:->|@)can(?:Any|any)?\\s*\\(/',
            'gate' => '/\\bGate::(?:allows|denies|check|any|authorize|inspect)\\s*\\(/',
            'authorize' => '/(?:->|\\b)authorize\\s*\\(/',
        ];
        $rolePatterns = [
            'has_role' => '/\\bhasRole\\s*\\(/',
            'role_model_name_query' => '/\\bRole::where(?:In)?\\s*\\([^\\n]*[\'\"]name[\'\"]/',
            'user_roles_name_query' => '/roles\\(\\)->where(?:In)?\\s*\\([^\\n]*[\'\"]name[\'\"]/',
        ];

        return [
            'permission' => $this->scanSource($permissionPatterns),
            'role_name' => $this->scanSource($rolePatterns),
        ];
    }

    /**
     * @param array<string, string> $patterns
     * @return array<int, array<string, mixed>>
     */
    private function scanSource(array $patterns): array
    {
        $matches = [];

        foreach (config('authorization_inventory.source_scan_paths', []) as $relativeRoot) {
            $root = base_path((string) $relativeRoot);
            if (! File::isDirectory($root)) {
                continue;
            }

            foreach (File::allFiles($root) as $file) {
                if (! in_array(strtolower($file->getExtension()), ['php'], true)) {
                    continue;
                }

                try {
                    $lines = file($file->getPathname(), FILE_IGNORE_NEW_LINES);
                } catch (Throwable) {
                    continue;
                }

                if ($lines === false) {
                    continue;
                }

                foreach ($lines as $lineNumber => $line) {
                    foreach ($patterns as $kind => $pattern) {
                        if (preg_match($pattern, $line) !== 1) {
                            continue;
                        }

                        $matches[] = [
                            'kind' => $kind,
                            'file' => str_replace(base_path().DIRECTORY_SEPARATOR, '', $file->getPathname()),
                            'line' => $lineNumber + 1,
                            'excerpt' => mb_substr(trim($line), 0, 240),
                        ];
                    }
                }
            }
        }

        return collect($matches)
            ->sortBy(fn (array $match) => $match['file'].'|'.str_pad((string) $match['line'], 8, '0', STR_PAD_LEFT).'|'.$match['kind'])
            ->values()
            ->all();
    }
}
