<?php

declare(strict_types=1);

require_once __DIR__ . '/TemplateDefinitionRepository.php';
require_once dirname(__DIR__, 2) . '/design/registry/ComponentRegistryRepository.php';
require_once dirname(__DIR__, 3) . '/system/definitions/DefinitionsPath.php';
require_once dirname(__DIR__, 2) . '/identity/RoleScopeProjector.php';

final class MyronTemplateDefinitionIngestionService
{
    private const RESERVED_BUNDLE_IDS = [
        'starter.default',
        'site-type.universal',
    ];

    public function __construct(
        private readonly MyronDefinitionsPath $definitionsPath,
        private readonly MyronTemplateDefinitionRepository $templates,
        private readonly MyronComponentRegistryRepository $components,
        private readonly MyronSectionRegistryRepository $sections,
    ) {
    }

    /**
     * @return array{count: int, templateIds: list<string>}
     */
    public function seedFromDefinitions(): array
    {
        $this->assertCatalogReconciliationReady();

        $componentIds = array_fill_keys($this->components->listComponentIds(), true);
        $bundleIds = array_fill_keys($this->loadManifestIds($this->definitionsPath->siteTypePaths(), 'site-type bundle'), true);
        $backlog = null;

        $savedIds = [];

        foreach ($this->definitionsPath->templatePaths() as $path) {
            $record = $this->decodeJsonFile($path, 'template definition');
            $templateId = trim((string) ($record['id'] ?? ''));

            if ($templateId === '') {
                throw new RuntimeException('Template definition `' . $path . '` is missing `id`.');
            }

            $bundleId = trim((string) ($record['bundleId'] ?? ''));
            if ($bundleId === '' || (!$this->isReservedBundleId($bundleId) && !isset($bundleIds[$bundleId]))) {
                throw new RuntimeException('Template `' . $templateId . '` references unknown bundle `' . $bundleId . '`.');
            }

            $sections = $record['sections'] ?? null;
            if (!is_array($sections)) {
                throw new RuntimeException('Template `' . $templateId . '` must expose sections as a list.');
            }

            foreach ($sections as $index => $section) {
                if (!is_array($section)) {
                    throw new RuntimeException('Template `' . $templateId . '` contains a non-object section at index ' . $index . '.');
                }

                $componentId = trim((string) ($section['componentId'] ?? ''));
                if ($componentId === '') {
                    throw new RuntimeException('Template `' . $templateId . '` section ' . $index . ' is missing `componentId`.');
                }

                $this->validateChromeBridgeBoundary($templateId, $section, $index);
                $this->validateGovernedSectionContract($templateId, $section, $index);

                $componentStatus = trim((string) ($section['componentStatus'] ?? 'active'));
                $hasCanonicalComponent = isset($componentIds[$componentId]);
                $needsBacklogLookup = $componentStatus === 'pending' || !$hasCanonicalComponent;
                if ($needsBacklogLookup && $backlog === null) {
                    $backlog = $this->loadBacklogIndex();
                }
                $hasBacklogComponent = $backlog !== null && isset($backlog[$componentId]);
                $isBacklogResolved = $componentStatus === 'pending' || (!$hasCanonicalComponent && $hasBacklogComponent);

                if ($isBacklogResolved) {
                    $backlogRef = trim((string) ($section['backlogRef'] ?? ''));
                    if ($backlog === null || !isset($backlog[$componentId])) {
                        throw new RuntimeException('Template `' . $templateId . '` pending component `' . $componentId . '` is missing from backlog definitions.');
                    }

                    $resolvedBacklogPath = $backlogRef === ''
                        ? $backlog[$componentId]['absolutePath']
                        : $this->definitionsPath->resolveDevPrepRelativePath($backlogRef);
                    $expectedPath = $backlog[$componentId]['sourcePath'];
                    $actualPath = $this->definitionsPath->relativeToProject($resolvedBacklogPath);
                    if ($expectedPath !== $actualPath) {
                        throw new RuntimeException(
                            'Template `' . $templateId . '` section `' . $componentId . '` points to `' . $actualPath . '`, expected `' . $expectedPath . '`.'
                        );
                    }

                    continue;
                }

                if (!isset($componentIds[$componentId])) {
                    throw new RuntimeException('Template `' . $templateId . '` references unknown component `' . $componentId . '`.');
                }
            }

            $roleScope = $record['roleScope'] ?? null;
            if ($roleScope !== null && !is_array($roleScope)) {
                throw new RuntimeException('Template `' . $templateId . '` has an invalid roleScope payload.');
            }

            $this->templates->saveTemplate(
                $this->withChromeSectionBridgeMetadata($record),
                $this->definitionsPath->relativeToProject($path),
                $roleScope === null ? null : MyronRoleScopeProjector::projectRoleScope($roleScope)
            );
            $savedIds[] = $templateId;
        }

        $this->templates->deleteMissingTemplates($savedIds);

        return [
            'count' => count($savedIds),
            'templateIds' => $savedIds,
        ];
    }

    /**
     * @param array<string, mixed> $section
     */
    private function validateChromeBridgeBoundary(string $templateId, array $section, int $index): void
    {
        $componentId = trim((string) ($section['componentId'] ?? ''));
        if (!in_array($componentId, ['header', 'footer'], true)) {
            return;
        }

        if (trim((string) ($section['sectionKey'] ?? '')) !== '') {
            throw new RuntimeException(
                'Template `' . $templateId . '` section ' . $index . ' declares chrome component `' . $componentId . '` with a governed sectionKey. Chrome must be bridged through `chrome`, not body sections.'
            );
        }
    }

    /**
     * @param array<string, mixed> $record
     * @return array<string, mixed>
     */
    private function withChromeSectionBridgeMetadata(array $record): array
    {
        $sections = is_array($record['sections'] ?? null) ? $record['sections'] : [];
        $legacyChromeSectionCount = 0;
        foreach ($sections as $section) {
            if (!is_array($section)) {
                continue;
            }

            if (in_array(trim((string) ($section['componentId'] ?? '')), ['header', 'footer'], true)) {
                $legacyChromeSectionCount++;
            }
        }

        $existingBridge = is_array($record['_bridge'] ?? null) ? $record['_bridge'] : [];
        $record['_bridge'] = $existingBridge + [
            'normalizedBodySections' => true,
            'legacyChromeSectionCount' => $legacyChromeSectionCount,
            'bodySectionCount' => count(MyronTemplateDefinitionRepository::bodySectionsForRecord($record)),
        ];

        return $record;
    }

    /**
     * @param array<string, mixed> $section
     */
    private function validateGovernedSectionContract(string $templateId, array $section, int $index): void
    {
        $sectionKey = trim((string) ($section['sectionKey'] ?? ''));
        if ($sectionKey === '') {
            return;
        }

        try {
            $this->sections->assertGovernedSectionKey($sectionKey, $templateId);
        } catch (InvalidArgumentException $exception) {
            throw new RuntimeException(
                'Template `' . $templateId . '` section ' . $index . ' failed governed section validation: ' . $exception->getMessage(),
                0,
                $exception
            );
        }

        $componentId = trim((string) ($section['componentId'] ?? ''));
        if (in_array($componentId, MyronSectionRegistryRepository::forbiddenBodySectionKeys(), true)) {
            throw new RuntimeException(
                'Template `' . $templateId . '` section ' . $index . ' may not use non-section component `' . $componentId . '` as governed body composition.'
            );
        }
    }

    private function isReservedBundleId(string $bundleId): bool
    {
        return in_array($bundleId, self::RESERVED_BUNDLE_IDS, true);
    }

    private function assertCatalogReconciliationReady(): void
    {
        $payload = $this->decodeJsonFile($this->definitionsPath->catalogReconciliationPath(), 'template catalog reconciliation');
        $summary = $payload['summary'] ?? null;
        if (!is_array($summary)) {
            throw new RuntimeException('Template catalog reconciliation summary is missing or invalid.');
        }

        $unresolvedCount = (int) ($summary['unresolvedCount'] ?? -1);
        if ($unresolvedCount !== 0) {
            throw new RuntimeException('Template catalog reconciliation is not clean: unresolvedCount=' . $unresolvedCount . '.');
        }
    }

    /**
     * @param list<string> $paths
     * @return list<string>
     */
    private function loadManifestIds(array $paths, string $label): array
    {
        $ids = [];

        foreach ($paths as $path) {
            $payload = $this->decodeJsonFile($path, $label);
            $id = trim((string) ($payload['id'] ?? ''));
            if ($id === '') {
                throw new RuntimeException('Definition `' . $path . '` is missing `id`.');
            }

            $ids[] = $id;
        }

        return $ids;
    }

    /**
     * @return array<string, array{id: string, sourcePath: string, absolutePath: string}>
     */
    private function loadBacklogIndex(): array
    {
        $backlog = [];

        foreach ($this->definitionsPath->backlogPaths() as $path) {
            $payload = $this->decodeJsonFile($path, 'backlog specification');
            $id = trim((string) ($payload['id'] ?? ''));
            if ($id === '') {
                throw new RuntimeException('Backlog specification `' . $path . '` is missing `id`.');
            }

            $backlog[$id] = [
                'id' => $id,
                'sourcePath' => $this->definitionsPath->relativeToProject($path),
                'absolutePath' => $path,
            ];
        }

        return $backlog;
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeJsonFile(string $path, string $label): array
    {
        $raw = file_get_contents($path);
        if (!is_string($raw)) {
            throw new RuntimeException('Failed to read ' . $label . ' at `' . $path . '`.');
        }

        $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new RuntimeException('Expected ' . $label . ' at `' . $path . '` to decode into an object.');
        }

        return $decoded;
    }
}
