<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Throwable;

class AuthorizationRoleCheckAudit extends Command
{
    protected $signature = 'authorization:audit-role-checks
        {--json : Emit the complete audit as JSON}
        {--strict : Return a failure exit code when unapproved role authorization checks are found}
        {--output= : Write the JSON audit to this absolute or project-relative path}
        {--path=* : Restrict scanning to one or more absolute or project-relative source paths}';

    protected $description = 'Audit direct role-name authorization checks before final RBAC cleanup';

    public function handle(): int
    {
        $callSites = $this->scanSource();
        $authorizationChecks = collect($callSites)
            ->where('category', 'authorization-check')
            ->values()
            ->all();
        $unapprovedChecks = collect($authorizationChecks)
            ->where('approved', false)
            ->values()
            ->all();
        $structuralReferences = collect($callSites)
            ->where('category', 'structural-reference')
            ->values()
            ->all();
        $reviewReferences = collect($structuralReferences)
            ->where('approved', false)
            ->values()
            ->all();

        $report = [
            'schema_version' => 1,
            'phase' => 9,
            'status' => $unapprovedChecks === [] ? 'pass' : 'fail',
            'generated_at' => now()->toIso8601String(),
            'summary' => [
                'call_sites' => count($callSites),
                'authorization_checks' => count($authorizationChecks),
                'unapproved_authorization_checks' => count($unapprovedChecks),
                'structural_role_references' => count($structuralReferences),
                'structural_references_requiring_review' => count($reviewReferences),
            ],
            'scan_paths' => $this->scanPaths(),
            'approved_role_check_paths' => config('authorization_role_check_audit.approved_role_check_paths', []),
            'structural_role_reference_paths' => config('authorization_role_check_audit.structural_role_reference_paths', []),
            'call_sites' => $callSites,
            'findings' => [
                'unapproved_authorization_checks' => $unapprovedChecks,
                'structural_references_requiring_review' => $reviewReferences,
            ],
        ];

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

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

        if ($this->option('json')) {
            $this->line($json);
        } else {
            $this->info('Authorization Phase 9 role-check audit');
            $this->line('Authorization role checks: '.$report['summary']['authorization_checks']);
            $this->line('Unapproved authorization role checks: '.$report['summary']['unapproved_authorization_checks']);
            $this->line('Structural role references: '.$report['summary']['structural_role_references']);
            $this->line('Structural references requiring review: '.$report['summary']['structural_references_requiring_review']);

            foreach ($unapprovedChecks as $finding) {
                $this->error(sprintf(
                    '%s:%d uses %s for authorization. Replace it with a permission/policy check or document a temporary exception.',
                    $finding['file'],
                    $finding['line'],
                    $finding['kind'],
                ));
            }

            foreach ($reviewReferences as $finding) {
                $this->warn(sprintf(
                    '%s:%d contains role-name coupling (%s) outside a documented structural-role path.',
                    $finding['file'],
                    $finding['line'],
                    $finding['kind'],
                ));
            }

            if ($unapprovedChecks === []) {
                $this->info('No unapproved direct role-based authorization checks were found.');
            }

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

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

        return self::SUCCESS;
    }

    /** @return array<int, array<string, mixed>> */
    private function scanSource(): array
    {
        $authorizationPatterns = [
            'has_role' => '/\bhasRole\s*\(/',
            'has_any_role' => '/\bhasAnyRole\s*\(/',
            'has_all_roles' => '/\bhasAllRoles\s*\(/',
            'role_middleware' => '/(?:middleware\s*\(|->middleware\s*\()[^\n]*[\'\"]role:/i',
            'blade_role_directive' => '/@(role|hasrole|hasanyrole|hasallroles)\s*\(/i',
        ];
        $structuralPatterns = [
            'role_model_name_query' => '/\bRole::(?:query\(\)->)?where(?:In)?\s*\([^\n]*[\'\"]name[\'\"]/',
            'user_roles_name_query' => '/roles\(\)->where(?:In)?\s*\([^\n]*[\'\"]name[\'\"]/',
            'role_name_pluck' => '/roles(?:\(\))?[^\n]*pluck\s*\(\s*[\'\"]name[\'\"]/',
        ];
        $matches = [];

        foreach ($this->sourceFiles() as $file) {
            try {
                $lines = file($file, FILE_IGNORE_NEW_LINES);
            } catch (Throwable) {
                continue;
            }

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

            $relativePath = $this->relativePath($file);

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

                    [$approved, $reason] = $this->pathApproval(
                        $relativePath,
                        config('authorization_role_check_audit.approved_role_check_paths', []),
                    );

                    $matches[] = [
                        'category' => 'authorization-check',
                        'kind' => $kind,
                        'file' => $relativePath,
                        'line' => $lineNumber + 1,
                        'approved' => $approved,
                        'approval_reason' => $reason,
                        'excerpt' => mb_substr(trim($line), 0, 240),
                    ];
                }

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

                    [$approved, $reason] = $this->pathApproval(
                        $relativePath,
                        config('authorization_role_check_audit.structural_role_reference_paths', []),
                    );

                    $matches[] = [
                        'category' => 'structural-reference',
                        'kind' => $kind,
                        'file' => $relativePath,
                        'line' => $lineNumber + 1,
                        'approved' => $approved,
                        'approval_reason' => $reason,
                        '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();
    }

    /** @return array<int, string> */
    private function sourceFiles(): array
    {
        $files = [];

        foreach ($this->scanPaths() as $path) {
            $absolute = $this->absolutePath($path);

            if (File::isFile($absolute)) {
                if (strtolower(pathinfo($absolute, PATHINFO_EXTENSION)) === 'php') {
                    $files[] = $absolute;
                }

                continue;
            }

            if (! File::isDirectory($absolute)) {
                continue;
            }

            foreach (File::allFiles($absolute) as $file) {
                if (strtolower($file->getExtension()) === 'php') {
                    $files[] = $file->getPathname();
                }
            }
        }

        return collect($files)->unique()->sort()->values()->all();
    }

    /** @return array<int, string> */
    private function scanPaths(): array
    {
        $paths = collect((array) $this->option('path'))
            ->map(fn ($path) => trim((string) $path))
            ->filter()
            ->values()
            ->all();

        return $paths !== []
            ? $paths
            : array_values(config('authorization_role_check_audit.source_scan_paths', []));
    }

    /** @param array<string, string> $rules @return array{0: bool, 1: ?string} */
    private function pathApproval(string $path, array $rules): array
    {
        $path = str_replace('\\', '/', $path);
        $base = rtrim(str_replace('\\', '/', base_path()), '/').'/';

        foreach ($rules as $rule => $reason) {
            $rule = str_replace('\\', '/', trim((string) $rule));
            if ($rule === '') {
                continue;
            }

            if (str_starts_with($rule, $base)) {
                $rule = substr($rule, strlen($base));
            }

            $matches = str_ends_with($rule, '/')
                ? str_starts_with($path, $rule)
                : $path === $rule;

            if ($matches) {
                return [true, (string) $reason];
            }
        }

        return [false, null];
    }

    private function absolutePath(string $path): string
    {
        return str_starts_with($path, DIRECTORY_SEPARATOR)
            ? $path
            : base_path($path);
    }

    private function relativePath(string $path): string
    {
        $base = rtrim(str_replace('\\', '/', base_path()), '/').'/';
        $normalized = str_replace('\\', '/', $path);

        return str_starts_with($normalized, $base)
            ? substr($normalized, strlen($base))
            : $normalized;
    }
}
