<?php

declare(strict_types=1);

final class MyronDefinitionsPath
{
    private const RUNTIME_DEFINITIONS_PROJECT_PREFIX = 'app/data/runtime-definitions';

    public function __construct(
        private readonly string $projectRoot = '',
    ) {
    }

    public function projectRoot(): string
    {
        if ($this->projectRoot !== '') {
            return rtrim($this->projectRoot, DIRECTORY_SEPARATOR);
        }

        return dirname(__DIR__, 3);
    }

    public function runtimeDefinitionsRoot(): string
    {
        return $this->requireDirectory(
            $this->projectRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, self::RUNTIME_DEFINITIONS_PROJECT_PREFIX),
            'Runtime definitions root'
        );
    }

    /**
     * @deprecated Runtime code should use runtimeDefinitionsRoot().
     */
    public function devPrepRoot(): string
    {
        return $this->runtimeDefinitionsRoot();
    }

    public function runtimeDefinitionsProjectPath(): string
    {
        return self::RUNTIME_DEFINITIONS_PROJECT_PREFIX;
    }

    /**
     * @deprecated Runtime code should use runtimeDefinitionsProjectPath().
     */
    public function devPrepProjectPath(): string
    {
        return self::RUNTIME_DEFINITIONS_PROJECT_PREFIX;
    }

    public function componentsAtomsPath(): string
    {
        return $this->preferredVersionedFile('components/components-atoms-v0.2.0.json', 'components/components-atoms-v0.1.0.json', 'Component atoms definition file');
    }

    public function componentsPrimitivesPath(): string
    {
        return $this->preferredVersionedFile('components/components-primitives-v0.2.0.json', 'components/components-primitives-v0.1.0.json', 'Component primitives definition file');
    }

    public function catalogReconciliationPath(): string
    {
        return $this->requireFile(
            $this->runtimeDefinitionsRoot() . DIRECTORY_SEPARATOR . 'templates/catalog-reconciliation-v0.1.0.json',
            'Template catalog reconciliation file'
        );
    }

    /**
     * @return list<string>
     */
    public function templatePaths(): array
    {
        return $this->sortedFilesForPattern(
            $this->requireDirectory($this->runtimeDefinitionsRoot() . DIRECTORY_SEPARATOR . 'templates', 'Template definitions directory')
            . DIRECTORY_SEPARATOR . 'template*.json'
        );
    }

    /**
     * @return list<string>
     */
    public function siteTypePaths(): array
    {
        return $this->sortedFilesForPattern(
            $this->requireDirectory($this->runtimeDefinitionsRoot() . DIRECTORY_SEPARATOR . 'site-types', 'Site-type definitions directory')
            . DIRECTORY_SEPARATOR . 'site-type*.json'
        );
    }

    /**
     * @return list<string>
     */
    public function backlogPaths(): array
    {
        $backlogDirectory = $this->runtimeDefinitionsRoot() . DIRECTORY_SEPARATOR . 'backlog';
        if (!is_dir($backlogDirectory)) {
            return [];
        }

        return $this->sortedFilesForPattern(
            $backlogDirectory . DIRECTORY_SEPARATOR . '*.json'
        );
    }

    public function resolveDevPrepRelativePath(string $path): string
    {
        return $this->requireFile(
            $this->runtimeDefinitionsRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $this->normalizeDevPrepReference($path)),
            'Definitions reference'
        );
    }

    public function canonicalDevPrepReference(string $path): string
    {
        return 'runtime-definitions/' . $this->normalizeDevPrepReference($path);
    }

    public function canonicalProjectSourcePath(string $path): string
    {
        return self::RUNTIME_DEFINITIONS_PROJECT_PREFIX . '/' . $this->normalizeDevPrepReference($path);
    }

    public function normalizeDevPrepReference(string $path): string
    {
        $trimmed = trim($path);
        if ($trimmed === '') {
            throw new InvalidArgumentException('A definitions-relative path is required.');
        }

        $normalized = str_replace('\\', '/', $trimmed);
        $root = str_replace('\\', '/', $this->projectRoot());

        if (preg_match('/^[a-z][a-z0-9+.-]*:/i', $normalized) === 1) {
            throw new InvalidArgumentException('Definitions references may not use URI schemes.');
        }

        if (str_starts_with($normalized, $root . '/')) {
            $normalized = substr($normalized, strlen($root) + 1);
        } elseif (str_starts_with($normalized, '/')) {
            throw new InvalidArgumentException('Definitions references must stay inside the project root.');
        }

        $normalized = ltrim($normalized, '/');
        if (str_starts_with($normalized, self::RUNTIME_DEFINITIONS_PROJECT_PREFIX . '/')) {
            $normalized = substr($normalized, strlen(self::RUNTIME_DEFINITIONS_PROJECT_PREFIX) + 1);
        }
        if (str_starts_with($normalized, 'runtime-definitions/')) {
            $normalized = substr($normalized, strlen('runtime-definitions/'));
        }

        $segments = [];
        foreach (explode('/', $normalized) as $segment) {
            if ($segment === '' || $segment === '.') {
                continue;
            }
            if ($segment === '..') {
                throw new InvalidArgumentException('Definitions references may not traverse outside the runtime definitions directory.');
            }
            $segments[] = $segment;
        }

        if ($segments === []) {
            throw new InvalidArgumentException('A definitions-relative path is required.');
        }

        return implode('/', $segments);
    }

    public function relativeToProject(string $path): string
    {
        $absolute = str_replace('\\', '/', $path);
        $root = str_replace('\\', '/', $this->projectRoot());

        if (str_starts_with($absolute, $root . '/')) {
            return substr($absolute, strlen($root) + 1);
        }

        return $absolute;
    }

    /**
     * @return list<string>
     */
    private function sortedFilesForPattern(string $pattern): array
    {
        $paths = glob($pattern);
        if ($paths === false) {
            throw new RuntimeException('Failed to enumerate JSON files for `' . $pattern . '`.');
        }

        $normalized = array_values(array_map('strval', $paths));
        sort($normalized, SORT_STRING);

        return $normalized;
    }

    private function requireDirectory(string $path, string $label): string
    {
        if (!is_dir($path)) {
            throw new RuntimeException($label . ' is missing at `' . $path . '`.');
        }

        return $path;
    }

    private function requireFile(string $path, string $label): string
    {
        if (!is_file($path)) {
            throw new RuntimeException($label . ' is missing at `' . $path . '`.');
        }

        return $path;
    }

    private function preferredVersionedFile(string $preferredPath, string $fallbackPath, string $label): string
    {
        $preferred = $this->runtimeDefinitionsRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $preferredPath);
        if (is_file($preferred)) {
            return $preferred;
        }

        return $this->requireFile(
            $this->runtimeDefinitionsRoot() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $fallbackPath),
            $label
        );
    }
}
