<?php

declare(strict_types=1);

namespace App\Framework\Auth;

use App\Framework\Auth\Attributes\IpAuth;
use App\Framework\Auth\ValueObjects\IpAuthPolicy;
use App\Framework\Auth\ValueObjects\NamespaceAccessPolicy;
use App\Framework\Config\TypedConfiguration;
use App\Framework\Http\IpAddress;
use App\Framework\Http\Request;
use App\Framework\Router\Exception\RouteNotFound;
use App\Framework\Router\RouteContext;

/**
 * Service for route-based authorization checks
 *
 * Handles:
 * - Legacy Auth attribute checks
 * - Namespace-based access blocking (with allowlist)
 * - Namespace-based IP restrictions
 * - Route-specific IP auth attributes
 */
final readonly class RouteAuthorizationService
{
    /**
     * @param TypedConfiguration $config
     * @param array<string, array{visibility?: string, access_policy?: NamespaceAccessPolicy}> $namespaceConfig
     */
    public function __construct(
        private TypedConfiguration $config,
        private array $namespaceConfig = []
    ) {
    }

    /**
     * Authorize access to a route
     *
     * @throws RouteNotFound If authorization fails (hides route existence)
     */
    public function authorize(Request $request, RouteContext $routeContext): void
    {
        $clientIp = $this->getClientIp($request);
        $controllerClass = $routeContext->match->route->controller;

        // 1. Check legacy Auth attribute (backward compatibility)
        $this->checkLegacyAuthAttribute($clientIp, $routeContext);

        // 2. Check namespace-based access blocking
        $this->checkNamespaceAccessPolicy($controllerClass, $routeContext->path);

        // 3. Check namespace-based IP restrictions
        $this->checkNamespaceIpRestrictions($clientIp, $controllerClass, $routeContext->path);

        // 4. Check route-specific IP auth attributes
        $this->checkRouteIpAuthAttribute($clientIp, $routeContext);
    }

    /**
     * Check legacy Auth attribute (backward compatibility)
     */
    private function checkLegacyAuthAttribute(IpAddress $clientIp, RouteContext $routeContext): void
    {
        if ($this->config->app->debug) {
            return; // Skip in debug mode
        }

        if (! in_array(Auth::class, $routeContext->match->route->attributes)) {
            return; // No Auth attribute
        }

        $wireguardIp = '172.20.0.1';
        if ($clientIp->value !== $wireguardIp) {
            throw new RouteNotFound($routeContext->path);
        }
    }

    /**
     * Check namespace-based access blocking
     */
    private function checkNamespaceAccessPolicy(string $controllerClass, string $path): void
    {
        $namespace = $this->extractNamespace($controllerClass);

        foreach ($this->namespaceConfig as $pattern => $config) {
            if (! $this->namespaceMatches($namespace, $pattern)) {
                continue;
            }

            // Check if config has access_policy
            $accessPolicy = $config['access_policy'] ?? null;
            if (! $accessPolicy instanceof NamespaceAccessPolicy) {
                continue;
            }

            if ($accessPolicy->isControllerBlocked($controllerClass)) {
                throw new RouteNotFound($path);
            }
        }
    }

    /**
     * Check namespace-based IP restrictions
     */
    private function checkNamespaceIpRestrictions(
        IpAddress $clientIp,
        string $controllerClass,
        string $path
    ): void {
        $namespacePolicy = $this->getNamespaceIpPolicy($controllerClass);

        if ($namespacePolicy && ! $namespacePolicy->isAllowed($clientIp)) {
            throw new RouteNotFound($path);
        }
    }

    /**
     * Check route-specific IP auth attributes
     */
    private function checkRouteIpAuthAttribute(IpAddress $clientIp, RouteContext $routeContext): void
    {
        $ipAuthAttribute = $this->getIpAuthAttribute($routeContext->match->route);

        if (! $ipAuthAttribute) {
            return;
        }

        $routePolicy = $ipAuthAttribute->createPolicy();
        if (! $routePolicy->isAllowed($clientIp)) {
            throw new RouteNotFound($routeContext->path);
        }
    }

    /**
     * Get client IP address from request
     */
    private function getClientIp(Request $request): IpAddress
    {
        return $request->server->getClientIp() ?? IpAddress::localhost();
    }

    /**
     * Get namespace-based IP policy
     */
    private function getNamespaceIpPolicy(string $controllerClass): ?IpAuthPolicy
    {
        $namespace = $this->extractNamespace($controllerClass);

        foreach ($this->namespaceConfig as $pattern => $config) {
            if ($this->namespaceMatches($namespace, $pattern)) {
                return $this->createIpPolicyFromConfig($config);
            }
        }

        return null;
    }

    /**
     * Extract namespace from class name
     */
    private function extractNamespace(string $className): string
    {
        $parts = explode('\\', $className);
        array_pop($parts); // Remove class name

        return implode('\\', $parts);
    }

    /**
     * Check if namespace matches pattern
     */
    private function namespaceMatches(string $namespace, string $pattern): bool
    {
        // Exact match
        if ($namespace === $pattern) {
            return true;
        }

        // Wildcard pattern (e.g., "App\Admin\*")
        if (str_ends_with($pattern, '*')) {
            $prefix = rtrim($pattern, '*');

            return str_starts_with($namespace, $prefix);
        }

        // Prefix match
        return str_starts_with($namespace, $pattern);
    }

    /**
     * Create IP policy from configuration
     */
    private function createIpPolicyFromConfig(array $config): IpAuthPolicy
    {
        $visibility = $config['visibility'] ?? 'public';

        return match ($visibility) {
            'admin' => IpAuthPolicy::adminOnly(),
            'local' => IpAuthPolicy::localOnly(),
            'development' => IpAuthPolicy::development(),
            'private' => IpAuthPolicy::localOnly(),
            'custom' => IpAuthPolicy::fromConfig($config),
            default => IpAuthPolicy::fromConfig([]) // No restrictions for 'public'
        };
    }

    /**
     * Get IpAuth attribute from route
     */
    private function getIpAuthAttribute($route): ?IpAuth
    {
        // Check if route has IpAuth attribute in its attributes array
        foreach ($route->attributes ?? [] as $attribute) {
            if ($attribute instanceof IpAuth) {
                return $attribute;
            }
        }

        // Also check via reflection for method-level attributes
        try {
            $reflection = new \ReflectionMethod($route->controller, $route->action);
            $attributes = $reflection->getAttributes(IpAuth::class);

            if (! empty($attributes)) {
                return $attributes[0]->newInstance();
            }

            // Check controller class for IpAuth attribute
            $classReflection = new \ReflectionClass($route->controller);
            $classAttributes = $classReflection->getAttributes(IpAuth::class);

            if (! empty($classAttributes)) {
                return $classAttributes[0]->newInstance();
            }
        } catch (\ReflectionException $e) {
            // Ignore reflection errors
        }

        return null;
    }

    /**
     * Create service with namespace configuration
     */
    public static function withNamespaceConfig(
        TypedConfiguration $config,
        array $namespaceConfig
    ): self {
        return new self($config, $namespaceConfig);
    }
}
