<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;
use Illuminate\Routing\Route as IlluminateRoute;

final class AuditAdminAuthorizationCommand extends Command
{
    protected $signature = 'lottery:admin-auth-audit
        {--skip-route-coverage : 跳过受保护后台路由是否已注册 API 资源的检查}
        {--skip-resource-bindings : 跳过 permission_required 资源是否绑定动作权限的检查}';

    protected $description = '检查后台权限配置是否存在路由覆盖缺失或资源绑定缺失';

    public function handle(): int
    {
        $issues = [];

        if (! (bool) $this->option('skip-route-coverage')) {
            $issues = array_merge($issues, $this->checkProtectedAdminRouteCoverage());
        }

        if (! (bool) $this->option('skip-resource-bindings')) {
            $issues = array_merge($issues, $this->checkPermissionResourceBindings());
        }

        if ($issues === []) {
            $this->info('Admin authorization audit passed.');

            return self::SUCCESS;
        }

        $this->error(sprintf('Admin authorization audit found %d issue(s).', count($issues)));
        foreach ($issues as $issue) {
            $this->line(sprintf('- [%s] %s', $issue['type'], $issue['message']));
        }

        return self::FAILURE;
    }

    /**
     * @return list<array{type: string, message: string}>
     */
    private function checkProtectedAdminRouteCoverage(): array
    {
        $protectedRoutes = $this->protectedAdminRoutes();
        if ($protectedRoutes === []) {
            return [[
                'type' => 'route_coverage',
                'message' => 'No protected admin routes were discovered from the current route collection.',
            ]];
        }

        $resourceRouteNames = DB::table('admin_api_resources')
            ->where('status', 1)
            ->pluck('route_name')
            ->filter(static fn ($routeName): bool => is_string($routeName) && $routeName !== '')
            ->map(fn (string $routeName): string => $this->normalizeAdminRouteName($routeName))
            ->all();

        $resourceRouteNameSet = array_fill_keys($resourceRouteNames, true);
        $issues = [];

        foreach ($protectedRoutes as $route) {
            if (! isset($resourceRouteNameSet[$route['name']])) {
                $issues[] = [
                    'type' => 'route_coverage',
                    'message' => sprintf(
                        'Protected route `%s %s` (`%s`) has no active row in `admin_api_resources`.',
                        $route['method'],
                        $route['uri'],
                        $route['name'],
                    ),
                ];
            }
        }

        return $issues;
    }

    /**
     * @return list<array{type: string, message: string}>
     */
    private function checkPermissionResourceBindings(): array
    {
        $rows = DB::table('admin_api_resources as ar')
            ->leftJoin('admin_api_resource_bindings as arb', 'arb.api_resource_id', '=', 'ar.id')
            ->select('ar.code', 'ar.route_name', DB::raw('count(arb.id) as binding_count'))
            ->where('ar.status', 1)
            ->where('ar.auth_mode', 'permission_required')
            ->groupBy('ar.id', 'ar.code', 'ar.route_name')
            ->havingRaw('count(arb.id) = 0')
            ->orderBy('ar.code')
            ->get();

        $issues = [];
        foreach ($rows as $row) {
            $issues[] = [
                'type' => 'resource_bindings',
                'message' => sprintf(
                    'API resource `%s` (`%s`) requires permission but has no row in `admin_api_resource_bindings`.',
                    (string) $row->code,
                    (string) $row->route_name,
                ),
            ];
        }

        return $issues;
    }

    /**
     * @return list<array{name: string, method: string, uri: string}>
     */
    private function protectedAdminRoutes(): array
    {
        $routes = [];

        /** @var IlluminateRoute $route */
        foreach (Route::getRoutes() as $route) {
            $middleware = $route->gatherMiddleware();
            if (! in_array('lottery.admin', $middleware, true)
                && ! in_array('App\Http\Middleware\EnsureAdminApi', $middleware, true)
            ) {
                continue;
            }

            $name = $route->getName();
            if (! is_string($name) || $name === '') {
                continue;
            }

            $methods = array_values(array_filter(
                $route->methods(),
                static fn (string $method): bool => $method !== 'HEAD',
            ));

            $routes[] = [
                'name' => $this->normalizeAdminRouteName($name),
                'method' => $methods[0] ?? 'GET',
                'uri' => '/'.ltrim($route->uri(), '/'),
            ];
        }

        usort($routes, static fn (array $a, array $b): int => [$a['uri'], $a['method']] <=> [$b['uri'], $b['method']]);

        return $routes;
    }

    private function normalizeAdminRouteName(string $routeName): string
    {
        return preg_replace('/^(api\.v1\.admin\.)+/', 'api.v1.admin.', $routeName) ?? $routeName;
    }
}
