<?php

declare(strict_types=1);

final class MyronExtensionManifest
{
    /**
     * @param list<string> $requiredScopes
     * @param list<array{key:string,path:string}> $adminRoutes
     */
    public function __construct(
        public readonly string $vendor,
        public readonly string $name,
        public readonly string $version,
        public readonly array $requiredScopes,
        public readonly bool $hasMigrations,
        public readonly array $adminRoutes,
        public readonly string $directory,
    ) {
    }

    public static function fromFile(string $manifestPath, string $expectedVendor): self
    {
        if (!is_file($manifestPath)) {
            throw new InvalidArgumentException('Extension manifest not found.');
        }

        $decoded = json_decode((string) file_get_contents($manifestPath), true);
        if (!is_array($decoded)) {
            throw new InvalidArgumentException('Extension manifest is not valid JSON.');
        }

        $vendor = trim((string) ($decoded['vendor'] ?? ''));
        if (!preg_match('/^[a-z][a-z0-9_]*$/', $vendor)) {
            throw new InvalidArgumentException('Extension vendor must be snake_case.');
        }
        if ($vendor !== $expectedVendor) {
            throw new InvalidArgumentException('Extension vendor must match its directory name.');
        }

        $name = trim((string) ($decoded['name'] ?? ''));
        if ($name === '') {
            throw new InvalidArgumentException('Extension name is required.');
        }

        $version = trim((string) ($decoded['version'] ?? ''));
        if ($version === '') {
            throw new InvalidArgumentException('Extension version is required.');
        }

        $requiredScopes = self::normalizeScopes($decoded['requiredScopes'] ?? []);
        $adminRoutes = self::normalizeAdminRoutes($decoded['adminRoutes'] ?? []);

        return new self(
            vendor: $vendor,
            name: $name,
            version: $version,
            requiredScopes: $requiredScopes,
            hasMigrations: (bool) ($decoded['hasMigrations'] ?? false),
            adminRoutes: $adminRoutes,
            directory: dirname($manifestPath),
        );
    }

    /**
     * @param mixed $rawScopes
     * @return list<string>
     */
    private static function normalizeScopes(mixed $rawScopes): array
    {
        if (!is_array($rawScopes)) {
            throw new InvalidArgumentException('Extension requiredScopes must be a list.');
        }

        $allowed = array_fill_keys(MyronApiScopes::ALL_BASE, true);
        $scopes = [];
        foreach ($rawScopes as $scope) {
            $scope = trim((string) $scope);
            if ($scope === '') {
                continue;
            }
            if (!isset($allowed[$scope])) {
                throw new InvalidArgumentException('Extension manifest declares unknown scope: ' . $scope . '.');
            }
            $scopes[] = $scope;
        }

        return array_values(array_unique($scopes));
    }

    /**
     * @param mixed $rawRoutes
     * @return list<array{key:string,path:string}>
     */
    private static function normalizeAdminRoutes(mixed $rawRoutes): array
    {
        if (!is_array($rawRoutes)) {
            throw new InvalidArgumentException('Extension adminRoutes must be a list.');
        }

        $routes = [];
        foreach ($rawRoutes as $route) {
            if (!is_array($route)) {
                throw new InvalidArgumentException('Extension adminRoutes entries must be objects.');
            }
            $key = trim((string) ($route['key'] ?? ''));
            $path = trim((string) ($route['path'] ?? ''));
            if (!preg_match('/^[a-z][a-z0-9_-]*$/', $key)) {
                throw new InvalidArgumentException('Extension admin route keys must be kebab-case.');
            }
            if ($path === '' || str_contains($path, '..') || str_starts_with($path, '/')) {
                throw new InvalidArgumentException('Extension admin route paths must be relative paths.');
            }
            $routes[] = ['key' => $key, 'path' => $path];
        }

        return $routes;
    }
}
