<?php

declare(strict_types=1);

namespace App\Framework\Core;

use App\Framework\Attributes\Route;
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
use App\Framework\Router\CompiledPattern;
use App\Framework\Router\CompiledRoutes;
use App\Framework\Router\RouteData;
use App\Framework\Router\RouteNameInterface;
use App\Framework\Router\ValueObjects\MethodParameter;
use App\Framework\Router\ValueObjects\ParameterCollection;
use App\Framework\Router\ValueObjects\SubdomainPattern;

final readonly class RouteCompiler
{
    /** @var array<string, StaticRoute|DynamicRoute> */
    private array $named;

    /**
     * Compile routes directly from DiscoveredAttribute objects with subdomain support
     * @return array<string, array<string, array{static: array<string, StaticRoute>, dynamic: array<int, DynamicRoute>}>>
     */
    public function compile(DiscoveredAttribute ...$discoveredRoutes): array
    {
        $compiled = [];
        $named = [];

        foreach ($discoveredRoutes as $discoveredAttribute) {
            // Create actual Route attribute instance
            $routeAttribute = $discoveredAttribute->createAttributeInstance();
            if (! $routeAttribute instanceof Route) {
                continue;
            }

            // Extract route data directly from the Route attribute
            $method = strtoupper($routeAttribute->method->value);
            $path = $routeAttribute->getPathAsString();
            $routeName = $this->extractRouteName($routeAttribute->name);

            // Process subdomain patterns
            $subdomainPatterns = SubdomainPattern::fromInput($routeAttribute->subdomain);

            // If no subdomains specified, use default
            if (empty($subdomainPatterns)) {
                $subdomainPatterns = [new SubdomainPattern('')];
            }

            foreach ($subdomainPatterns as $subdomainPattern) {
                $subdomainKey = $subdomainPattern->getCompilationKey();

                $compiled[$method] ??= [];
                $compiled[$method][$subdomainKey] ??= ['static' => [], 'dynamic' => []];

                if (! str_contains($path, '{')) {
                    // Static route
                    $parameterCollection = $this->createParameterCollection($discoveredAttribute->additionalData['parameters'] ?? []);
                    $staticRoute = new StaticRoute(
                        controller         : $discoveredAttribute->className->getFullyQualified(),
                        action             : $discoveredAttribute->methodName?->toString() ?? '',
                        parameters         : $discoveredAttribute->additionalData['parameters'] ?? [],
                        name               : $routeName ?? '',
                        path               : $path,
                        attributes         : $discoveredAttribute->additionalData['attributes'] ?? [],
                        parameterCollection: $parameterCollection
                    );

                    $compiled[$method][$subdomainKey]['static'][$path] = $staticRoute;

                    if ($routeName) {
                        $named[$routeName] = $staticRoute;
                    }
                } else {
                    // Dynamic route
                    $paramNames = [];
                    $regex = $this->convertPathToRegex($path, $paramNames);
                    $parameterCollection = $this->createParameterCollection($discoveredAttribute->additionalData['parameters'] ?? []);

                    $dynamicRoute = new DynamicRoute(
                        regex              : $regex,
                        paramNames         : $paramNames,
                        controller         : $discoveredAttribute->className->getFullyQualified(),
                        action             : $discoveredAttribute->methodName?->toString() ?? '',
                        parameters         : $discoveredAttribute->additionalData['parameters'] ?? [],
                        paramValues        : [],
                        name               : $routeName ?? '',
                        path               : $path,
                        attributes         : $discoveredAttribute->additionalData['attributes'] ?? [],
                        parameterCollection: $parameterCollection
                    );

                    $compiled[$method][$subdomainKey]['dynamic'][] = $dynamicRoute;

                    if ($routeName) {
                        $named[$routeName] = $dynamicRoute;
                    }
                }
            }
        }

        if (! isset($this->named)) {
            $this->named = $named;
        }

        return $compiled;
    }

    /**
     * Convert legacy parameters array to ParameterCollection
     */
    private function createParameterCollection(array $parameters): ParameterCollection
    {
        $methodParameters = [];

        foreach ($parameters as $param) {
            $methodParameters[] = new MethodParameter(
                name: $param['name'],
                type: $param['type'],
                isBuiltin: $param['isBuiltin'],
                hasDefault: $param['hasDefault'],
                default: $param['default'] ?? null
            );
        }

        return new ParameterCollection(...$methodParameters);
    }

    /**
     * @param array<int, array{method: string, path: string, controller: class-string, handler: string}> $routes
     * @return array<string, StaticRoute|DynamicRoute>
     */
    public function compileNamedRoutes(array $routes): array
    {
        return $this->named;
    }

    /**
     * Konvertiert zB. /user/{id}/edit → ~^/user/([^/]+)/edit$~ und gibt ['id'] als Parameternamen zurück.
     *
     * @param string $path
     * @param array<string> &$paramNames
     * @return string
     */
    private function convertPathToRegex(string $path, array &$paramNames): string
    {
        $paramNames = [];
        $regex = preg_replace_callback('#\{(\w+)(\*)?}#', function ($matches) use (&$paramNames) {
            $paramNames[] = $matches[1];

            // Wenn {id*} dann erlaube Slashes, aber mache es non-greedy
            if (isset($matches[2]) && $matches[2] === '*') {
                return '(.+?)'; // Non-greedy: matcht so wenig wie möglich
            }

            return '([^/]+)'; // Keine Slashes
        }, $path);

        return '~^' . $regex . '$~';
    }

    public function getAttributeClass(): string
    {
        return Route::class;
    }

    /**
     * Compile optimized routes directly from DiscoveredAttribute objects with subdomain support
     */
    public function compileOptimized(DiscoveredAttribute ...$discoveredRoutes): CompiledRoutes
    {
        $compiled = $this->compile(...$discoveredRoutes);
        $optimizedStatic = [];
        $optimizedDynamic = [];

        foreach ($compiled as $method => $subdomainRoutes) {
            foreach ($subdomainRoutes as $subdomainKey => $routes) {
                $optimizedStatic[$method][$subdomainKey] = $routes['static'];

                if (! empty($routes['dynamic'])) {
                    $optimizedDynamic[$method][$subdomainKey] = $this->createCompiledPattern($routes['dynamic']);
                }
            }
        }

        return new CompiledRoutes($optimizedStatic, $optimizedDynamic, $this->named);
    }

    /**
     * Creates optimized compiled patterns with route prioritization
     * @param array<int, DynamicRoute> $dynamicRoutes
     */
    private function createCompiledPattern(array $dynamicRoutes): CompiledPattern
    {
        // Sort routes by priority (specific patterns first, then by path complexity)
        $prioritizedRoutes = $this->prioritizeRoutes($dynamicRoutes);

        // Group routes into batches for smaller regex patterns
        $routeBatches = $this->groupRoutesByComplexity($prioritizedRoutes);

        $compiledBatches = [];
        /** @var array<int, RouteData> $routeData */
        $routeData = [];
        $globalIndex = 0;

        foreach ($routeBatches as $batchName => $batch) {
            $patterns = [];
            $currentIndex = 1; // After full match (Index 0)

            foreach ($batch as $route) {
                $pattern = $this->stripAnchors($route->regex);
                $patterns[] = "({$pattern})";

                // This route's main capture group index
                $routeGroupIndex = $currentIndex;

                // Parameter-Mapping berechnen - parameters are INSIDE the route pattern
                // So their indices are routeGroupIndex + 1, routeGroupIndex + 2, etc.
                $paramMap = [];
                $paramOffset = 1; // Parameters start right after the route's capture group
                foreach ($route->paramNames as $paramName) {
                    $paramMap[$paramName] = $routeGroupIndex + $paramOffset++;
                }

                // Now advance currentIndex past ALL capture groups in this route's pattern
                // That's 1 (the route group) + count(paramNames) (the parameters inside it)
                $currentIndex += 1 + count($route->paramNames);

                $routeData[$globalIndex] = new RouteData(
                    route: $route,
                    paramMap: $paramMap,
                    routeGroupIndex: $routeGroupIndex,
                    batch: $batchName,
                    regex: '~^' . $this->stripAnchors($route->regex) . '$~' // Pre-compiled individual regex
                );

                $globalIndex++;
            }

            $compiledBatches[$batchName] = [
                'regex' => '~^(?:' . implode('|', $patterns) . ')$~',
                'routes' => array_slice($routeData, $globalIndex - count($batch), count($batch), true),
            ];
        }

        // Use the first batch's regex for backward compatibility, but store all batches
        $primaryRegex = ! empty($compiledBatches) ? reset($compiledBatches)['regex'] : '~^$~';

        return new CompiledPattern($primaryRegex, $routeData, $compiledBatches);
    }

    /**
     * Prioritize routes by specificity and expected frequency
     * @param array<DynamicRoute> $routes
     * @return array<DynamicRoute>
     */
    private function prioritizeRoutes(array $routes): array
    {
        usort($routes, function ($a, $b) {
            // 1. API routes first (typically more frequent)
            $aIsApi = str_starts_with($a->path, '/api/');
            $bIsApi = str_starts_with($b->path, '/api/');
            if ($aIsApi !== $bIsApi) {
                return $bIsApi <=> $aIsApi; // API routes first
            }

            // 2. Routes with fewer parameters first (more specific)
            $aParamCount = count($a->paramNames);
            $bParamCount = count($b->paramNames);
            if ($aParamCount !== $bParamCount) {
                return $aParamCount <=> $bParamCount;
            }

            // 3. Shorter paths first (typically more specific)
            $aLength = strlen($a->path);
            $bLength = strlen($b->path);

            return $aLength <=> $bLength;
        });

        return $routes;
    }

    /**
     * Group routes by complexity for optimized batch processing
     * @param array<DynamicRoute> $routes
     * @return array<string, array<DynamicRoute>>
     */
    private function groupRoutesByComplexity(array $routes): array
    {
        $groups = [
            'simple' => [],    // /api/{id}, /user/{id} - 1 parameter
            'medium' => [],    // /api/{type}/{id} - 2 parameters
            'complex' => [],   // 3+ parameters or wildcards
        ];

        foreach ($routes as $route) {
            $paramCount = count($route->paramNames);
            $hasWildcard = str_contains($route->path, '{*}');

            if ($hasWildcard || $paramCount >= 3) {
                $groups['complex'][] = $route;
            } elseif ($paramCount === 2) {
                $groups['medium'][] = $route;
            } else {
                $groups['simple'][] = $route;
            }
        }

        // Remove empty groups
        return array_filter($groups, fn ($group) => ! empty($group));
    }

    private function stripAnchors(string $regex): string
    {
        if (preg_match('/^~\^(.+)\$~$/', $regex, $matches)) {
            return $matches[1];
        }

        return $regex;
    }

    /**
     * Extract route name as string from enum or string
     */
    private function extractRouteName(RouteNameInterface|string|null $routeName): string
    {
        if ($routeName instanceof RouteNameInterface) {
            return $routeName->value;
        }

        return (string) ($routeName ?? '');
    }
}
