<?php

declare(strict_types=1);

final class MyronTemplateDefinitionRepository
{
    public function __construct(
        private readonly PDO $pdo,
    ) {
    }

    public function countTemplates(): int
    {
        return (int) $this->pdo->query('SELECT COUNT(*) FROM template_definitions')->fetchColumn();
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listTemplates(?string $bundleId = null): array
    {
        $sql = 'SELECT * FROM template_definitions';
        $params = [];

        if ($bundleId !== null && trim($bundleId) !== '') {
            $sql .= ' WHERE bundle_id = :bundle_id';
            $params[':bundle_id'] = trim($bundleId);
        }

        $sql .= ' ORDER BY bundle_id ASC, template_id ASC';
        $statement = $this->pdo->prepare($sql);
        $statement->execute($params);
        $rows = $statement->fetchAll();

        if (!is_array($rows)) {
            return [];
        }

        return array_map(fn (array $row): array => $this->hydrateTemplate($row), $rows);
    }

    public function findTemplateById(string $templateId): ?array
    {
        $statement = $this->pdo->prepare('SELECT * FROM template_definitions WHERE template_id = :template_id LIMIT 1');
        $statement->execute([':template_id' => trim($templateId)]);
        $row = $statement->fetch();

        return is_array($row) ? $this->hydrateTemplate($row) : null;
    }

    public static function templateClassForTemplateId(string $templateId): string
    {
        $normalized = trim($templateId);
        if ($normalized === '') {
            return '';
        }

        $customMarkerPosition = strpos($normalized, '::custom::');
        if ($customMarkerPosition !== false) {
            $normalized = substr($normalized, 0, $customMarkerPosition);
        }

        $parts = explode('.', $normalized);
        if (($parts[0] ?? '') === 'template') {
            array_shift($parts);
        }

        if ($parts !== [] && preg_match('/^v[0-9]+$/', (string) end($parts)) === 1) {
            array_pop($parts);
        }

        if (count($parts) > 1) {
            array_pop($parts);
        }

        $class = implode('.', $parts);
        return $class !== '' ? $class : $normalized;
    }

    /**
     * @return array{key:string,label:string}
     */
    public static function templatePurposeForTemplateId(string $templateId): array
    {
        $class = self::templateClassForTemplateId($templateId);
        $role = $class;
        if (str_starts_with($role, 'funnel.')) {
            return ['key' => 'funnel-conversion', 'label' => 'Funnel / Conversion'];
        }

        $purposeMap = [
            'home' => ['homepage', 'Home'],
            'about' => ['about-story', 'About / Story'],
            'team' => ['about-story', 'About / Story'],
            'studio-statement' => ['about-story', 'About / Story'],
            'contact' => ['contact-lead', 'Contact / Lead Capture'],
            'reservation' => ['contact-lead', 'Contact / Lead Capture'],
            'location' => ['contact-lead', 'Contact / Lead Capture'],
            'services' => ['services-offers', 'Services / Offers'],
            'features' => ['services-offers', 'Services / Offers'],
            'featured-offers' => ['services-offers', 'Services / Offers'],
            'pricing' => ['services-offers', 'Services / Offers'],
            'blog-index' => ['editorial', 'Blog / Editorial'],
            'post-single' => ['editorial', 'Blog / Editorial'],
            'category-archive' => ['editorial', 'Blog / Editorial'],
            'resources' => ['editorial', 'Blog / Editorial'],
            'search' => ['editorial', 'Blog / Editorial'],
            'shop' => ['commerce', 'Commerce'],
            'product-single' => ['commerce', 'Commerce'],
            'cart' => ['commerce', 'Commerce'],
            'checkout' => ['commerce', 'Commerce'],
            'account' => ['commerce', 'Commerce'],
            'course-single' => ['learning', 'Learning'],
            'courses-index' => ['learning', 'Learning'],
            'instructor-index' => ['learning', 'Learning'],
            'instructor-single' => ['learning', 'Learning'],
            'portfolio-index' => ['portfolio', 'Portfolio / Work'],
            'project-single' => ['portfolio', 'Portfolio / Work'],
            'legal' => ['utility', 'Legal / Utility'],
            '404' => ['utility', 'Legal / Utility'],
            'coming-soon' => ['utility', 'Legal / Utility'],
            'menu' => ['utility', 'Legal / Utility'],
            'all-sections-inspection' => ['design-inspection', 'Design Inspection'],
        ];

        if (isset($purposeMap[$role])) {
            [$key, $label] = $purposeMap[$role];
            return ['key' => $key, 'label' => $label];
        }

        return ['key' => 'other', 'label' => 'Other'];
    }

    public static function remediationStatusLabel(string $status): string
    {
        return match (trim($status)) {
            'governed' => 'Ready',
            'sectionKey bridge' => 'Needs Mapping',
            'pending components' => 'Pending Components',
            default => ucwords(str_replace(['-', '_'], ' ', trim($status))),
        };
    }

    /**
     * @param array<string, mixed> $template
     */
    public static function remediationStatusForTemplate(array $template): string
    {
        $registry = is_array($template['_registry'] ?? null) ? $template['_registry'] : [];
        $bodySections = is_array($template['bodySections'] ?? null) ? $template['bodySections'] : self::bodySectionsForRecord($template);
        $pendingCount = (int) ($registry['pendingSectionCount'] ?? 0);
        $missingSectionKeyCount = 0;

        foreach ($bodySections as $section) {
            if (!is_array($section)) {
                continue;
            }

            if (trim((string) ($section['componentStatus'] ?? '')) === 'pending') {
                $pendingCount++;
            }

            if (trim((string) ($section['sectionKey'] ?? '')) === '') {
                $missingSectionKeyCount++;
            }
        }

        if ($pendingCount > 0) {
            return 'pending components';
        }

        if ($missingSectionKeyCount > 0) {
            return 'sectionKey bridge';
        }

        return 'governed';
    }

    /**
     * @param array<string, mixed> $template
     */
    public static function chromeSummaryForTemplate(array $template): string
    {
        $chrome = is_array($template['chrome'] ?? null) ? $template['chrome'] : self::chromeForTemplateRecord($template);
        $header = is_array($chrome['header'] ?? null) ? $chrome['header'] : [];
        $footer = is_array($chrome['footer'] ?? null) ? $chrome['footer'] : [];
        $headerVariant = trim((string) ($header['variant'] ?? ''));
        $footerVariant = trim((string) ($footer['variant'] ?? ''));

        return 'Header: ' . ($headerVariant !== '' ? $headerVariant : 'global')
            . ' / Footer: ' . ($footerVariant !== '' ? $footerVariant : 'global');
    }

    /**
     * @param array<string, mixed> $record
     * @param array<string, list<string>>|null $projectedRoleScope
     * @return array<string, mixed>
     */
    public function saveTemplate(array $record, string $sourcePath, ?array $projectedRoleScope = null): array
    {
        $templateId = trim((string) ($record['id'] ?? ''));
        if ($templateId === '') {
            throw new InvalidArgumentException('Template records require a non-empty `id`.');
        }

        $bundleId = trim((string) ($record['bundleId'] ?? ''));
        if ($bundleId === '') {
            throw new InvalidArgumentException('Template `' . $templateId . '` is missing `bundleId`.');
        }

        $siteType = trim((string) ($record['siteType'] ?? ''));
        $tier = (int) ($record['tier'] ?? 0);
        if (!is_array($record['sections'] ?? [])) {
            throw new InvalidArgumentException('Template `' . $templateId . '` must expose sections as a list.');
        }
        $sections = self::bodySectionsForRecord($record);

        $pendingSectionCount = 0;
        foreach ($sections as $section) {
            if (!is_array($section)) {
                continue;
            }

            if (trim((string) ($section['componentStatus'] ?? '')) === 'pending') {
                $pendingSectionCount++;
            }
        }

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

        // STN-012-017: Preserve defaultContent across re-ingestion. The JSONM template
        // definitions on disk do not carry defaultContent; that field is authored as
        // separate seed content and ingested by a development-only loader. Without
        // preservation, every PHP request triggers seedFromDefinitions() which would
        // clobber the loaded boilerplate. We treat defaultContent as DB-only state
        // that survives re-ingestion as long as the row exists.
        if (!array_key_exists('defaultContent', $record) && $this->recordExists($templateId)) {
            $existingRecordJson = (string) $this->fetchExistingRecordJson($templateId);
            if ($existingRecordJson !== '') {
                $existingRecord = json_decode($existingRecordJson, true, 512, JSON_THROW_ON_ERROR);
                if (is_array($existingRecord) && isset($existingRecord['defaultContent']) && is_array($existingRecord['defaultContent'])) {
                    $record['defaultContent'] = $existingRecord['defaultContent'];
                }
            }
        }

        $payload = json_encode($record, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
        $roleScopeJson = $roleScope === null
            ? null
            : json_encode($roleScope, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
        $projectedRoleScopeJson = $projectedRoleScope === null
            ? null
            : json_encode($projectedRoleScope, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
        $chrome = self::chromeForTemplateRecord($record);
        $headerCapabilities = self::headerCapabilitiesForTemplateRecord($record, $chrome);
        $chromeJson = json_encode($chrome, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
        $headerCapabilitiesJson = json_encode($headerCapabilities, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);

        $now = myronNow();

        if ($this->recordExists($templateId)) {
            $statement = $this->pdo->prepare(
                'UPDATE template_definitions
                 SET bundle_id = :bundle_id,
                     site_type = :site_type,
                     tier = :tier,
                     source_path = :source_path,
                     role_scope_json = :role_scope_json,
                     projected_role_scope_json = :projected_role_scope_json,
                     chrome_json = :chrome_json,
                     header_capabilities_json = :header_capabilities_json,
                     section_count = :section_count,
                     pending_section_count = :pending_section_count,
                     record_json = :record_json,
                     updated_at = :updated_at
                 WHERE template_id = :template_id'
            );
            $statement->execute([
                ':bundle_id' => $bundleId,
                ':site_type' => $siteType,
                ':tier' => $tier,
                ':source_path' => trim($sourcePath),
                ':role_scope_json' => $roleScopeJson,
                ':projected_role_scope_json' => $projectedRoleScopeJson,
                ':chrome_json' => $chromeJson,
                ':header_capabilities_json' => $headerCapabilitiesJson,
                ':section_count' => count($sections),
                ':pending_section_count' => $pendingSectionCount,
                ':record_json' => $payload,
                ':updated_at' => $now,
                ':template_id' => $templateId,
            ]);
        } else {
            $statement = $this->pdo->prepare(
                'INSERT INTO template_definitions (
                    template_id,
                    bundle_id,
                    site_type,
                    tier,
                    source_path,
                    role_scope_json,
                    projected_role_scope_json,
                    chrome_json,
                    header_capabilities_json,
                    section_count,
                    pending_section_count,
                    record_json,
                    seeded_at,
                    updated_at
                 ) VALUES (
                    :template_id,
                    :bundle_id,
                    :site_type,
                    :tier,
                    :source_path,
                    :role_scope_json,
                    :projected_role_scope_json,
                    :chrome_json,
                    :header_capabilities_json,
                    :section_count,
                    :pending_section_count,
                    :record_json,
                    :seeded_at,
                    :updated_at
                 )'
            );
            $statement->execute([
                ':template_id' => $templateId,
                ':bundle_id' => $bundleId,
                ':site_type' => $siteType,
                ':tier' => $tier,
                ':source_path' => trim($sourcePath),
                ':role_scope_json' => $roleScopeJson,
                ':projected_role_scope_json' => $projectedRoleScopeJson,
                ':chrome_json' => $chromeJson,
                ':header_capabilities_json' => $headerCapabilitiesJson,
                ':section_count' => count($sections),
                ':pending_section_count' => $pendingSectionCount,
                ':record_json' => $payload,
                ':seeded_at' => $now,
                ':updated_at' => $now,
            ]);
        }

        $saved = $this->findTemplateById($templateId);
        if ($saved === null) {
            throw new RuntimeException('Failed to reload template `' . $templateId . '` after save.');
        }

        return $saved;
    }

    /**
     * @param list<string> $templateIds
     */
    public function deleteMissingTemplates(array $templateIds): void
    {
        $normalized = $this->normalizeIds($templateIds);
        if ($normalized === []) {
            $this->pdo->exec('DELETE FROM template_definitions');
            return;
        }

        [$placeholders, $params] = $this->buildInClause($normalized, 'template_id');
        $statement = $this->pdo->prepare(
            'DELETE FROM template_definitions WHERE template_id NOT IN (' . implode(', ', $placeholders) . ')'
        );
        $statement->execute($params);
    }

    private function recordExists(string $templateId): bool
    {
        $statement = $this->pdo->prepare('SELECT template_id FROM template_definitions WHERE template_id = :template_id LIMIT 1');
        $statement->execute([':template_id' => $templateId]);

        return $statement->fetchColumn() !== false;
    }

    /**
     * STN-012-017: Read the existing record_json blob for a templateId without hydration.
     * Used by saveTemplate to preserve defaultContent across re-ingestion.
     */
    private function fetchExistingRecordJson(string $templateId): string
    {
        $statement = $this->pdo->prepare('SELECT record_json FROM template_definitions WHERE template_id = :template_id LIMIT 1');
        $statement->execute([':template_id' => $templateId]);
        $value = $statement->fetchColumn();
        return is_string($value) ? $value : '';
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function hydrateTemplate(array $row): array
    {
        $record = $this->decodeJsonField((string) ($row['record_json'] ?? ''), 'template record');
        $chrome = isset($row['chrome_json']) && trim((string) $row['chrome_json']) !== ''
            ? $this->decodeJsonField((string) $row['chrome_json'], 'template chrome declaration')
            : self::chromeForTemplateRecord($record);
        $headerCapabilities = isset($row['header_capabilities_json']) && trim((string) $row['header_capabilities_json']) !== ''
            ? $this->decodeJsonListField((string) $row['header_capabilities_json'], 'template header capabilities')
            : self::headerCapabilitiesForTemplateRecord($record, $chrome);

        $chrome['headerCapabilities'] = $headerCapabilities;
        $record['chrome'] = $chrome;
        $record['headerCapabilities'] = $headerCapabilities;
        $record['bodySections'] = self::bodySectionsForRecord($record);
        $record['roleScope'] = $row['role_scope_json'] === null
            ? null
            : $this->decodeJsonField((string) $row['role_scope_json'], 'template role scope');
        $record['projectedRoleScope'] = $row['projected_role_scope_json'] === null
            ? null
            : $this->decodeJsonField((string) $row['projected_role_scope_json'], 'template projected role scope');

        // STN-012-014: Surface guidance_json (Pass 1 architectural guidance) when present.
        // Returned as $record['guidance'] for in-Studio rendering. Null when no guidance has
        // been ingested yet (template_definitions row exists but guidance_json is null/empty).
        $guidanceJson = isset($row['guidance_json']) ? (string) $row['guidance_json'] : '';
        $record['guidance'] = trim($guidanceJson) === ''
            ? null
            : $this->decodeJsonField($guidanceJson, 'template guidance');

        $record['_registry'] = [
            'bundleId' => (string) ($row['bundle_id'] ?? ''),
            'siteType' => (string) ($row['site_type'] ?? ''),
            'tier' => (int) ($row['tier'] ?? 0),
            'sourcePath' => (string) ($row['source_path'] ?? ''),
            'chrome' => $chrome,
            'headerCapabilities' => $headerCapabilities,
            'sectionCount' => (int) ($row['section_count'] ?? 0),
            'pendingSectionCount' => (int) ($row['pending_section_count'] ?? 0),
            'seededAt' => (string) ($row['seeded_at'] ?? ''),
            'updatedAt' => (string) ($row['updated_at'] ?? ''),
            'hasGuidance' => $record['guidance'] !== null,
        ];

        return $record;
    }

    /**
     * @param array<string, mixed> $record
     * @return array{header:array<string,string>,footer:array<string,string>,headerCapabilities:list<string>}
     */
    public static function chromeForTemplateRecord(array $record): array
    {
        $explicit = is_array($record['chrome'] ?? null) ? $record['chrome'] : [];
        $templateId = trim((string) ($record['id'] ?? ''));
        $bundleId = trim((string) ($record['bundleId'] ?? ''));
        $sections = is_array($record['sections'] ?? null) ? $record['sections'] : [];

        $headerVariant = '';
        $footerVariant = '';
        foreach ($sections as $section) {
            if (!is_array($section)) {
                continue;
            }

            $componentId = trim((string) ($section['componentId'] ?? ''));
            $variant = trim((string) ($section['variant'] ?? ''));
            if ($componentId === 'header' && $variant !== '') {
                $headerVariant = $variant;
            }
            if ($componentId === 'footer' && $variant !== '') {
                $footerVariant = $variant;
            }
        }

        if (str_contains($templateId, '.checkout.')) {
            $headerVariant = 'checkout-safe/minimal';
            $footerVariant = 'checkout-safe/minimal';
        } elseif ($bundleId === 'site-type.mkt' && str_contains($templateId, '.funnel.')) {
            $headerVariant = $headerVariant !== '' ? $headerVariant : 'campaign-minimal';
            $footerVariant = $footerVariant !== '' && $footerVariant !== 'minimal' ? $footerVariant : 'campaign-minimal';
        }

        $header = self::normalizeChromeSide(is_array($explicit['header'] ?? null) ? $explicit['header'] : [], $headerVariant);
        $footer = self::normalizeChromeSide(is_array($explicit['footer'] ?? null) ? $explicit['footer'] : [], $footerVariant);
        $headerCapabilities = self::headerCapabilitiesForTemplateRecord($record, [
            'header' => $header,
            'footer' => $footer,
            'headerCapabilities' => [],
        ]);

        return [
            'header' => $header,
            'footer' => $footer,
            'headerCapabilities' => $headerCapabilities,
        ];
    }

    /**
     * @param array<string, mixed> $record
     * @param array<string, mixed>|null $chrome
     * @return list<string>
     */
    public static function headerCapabilitiesForTemplateRecord(array $record, ?array $chrome = null): array
    {
        $capabilities = [];
        $explicit = is_array($record['headerCapabilities'] ?? null) ? $record['headerCapabilities'] : [];
        if ($chrome !== null && is_array($chrome['headerCapabilities'] ?? null)) {
            $explicit = array_merge($explicit, $chrome['headerCapabilities']);
        }

        foreach ($explicit as $capability) {
            $key = myronNormalizeSlug((string) $capability);
            if ($key !== '') {
                $capabilities[$key] = true;
            }
        }

        $templateId = trim((string) ($record['id'] ?? ''));
        $bundleId = trim((string) ($record['bundleId'] ?? ''));
        $sections = is_array($record['sections'] ?? null) ? $record['sections'] : [];

        if (
            $bundleId === 'site-type.ecom'
            || str_contains($templateId, '.shop.')
            || str_contains($templateId, '.product-single.')
            || str_contains($templateId, '.cart.')
            || str_contains($templateId, '.checkout.')
            || str_contains($templateId, '.account.')
        ) {
            $capabilities['commerce'] = true;
        }

        foreach ($sections as $section) {
            if (!is_array($section)) {
                continue;
            }

            if (
                trim((string) ($section['componentId'] ?? '')) === 'header'
                && myronNormalizeSlug((string) ($section['variant'] ?? '')) === 'commerce'
            ) {
                $capabilities['commerce'] = true;
            }
        }

        return array_keys($capabilities);
    }

    /**
     * @param array<string, mixed> $record
     * @return list<array<string, mixed>>
     */
    public static function bodySectionsForRecord(array $record): array
    {
        $sections = is_array($record['sections'] ?? null) ? $record['sections'] : [];
        $bodySections = [];

        foreach ($sections as $section) {
            if (!is_array($section)) {
                continue;
            }

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

            $bodySections[] = $section;
        }

        return $bodySections;
    }

    /**
     * @param array<string, mixed> $side
     * @return array{inherit:string,variant:string}
     */
    private static function normalizeChromeSide(array $side, string $legacyVariant): array
    {
        $inherit = trim((string) ($side['inherit'] ?? $side['source'] ?? 'global'));
        $variant = trim((string) ($side['variant'] ?? $legacyVariant));

        return [
            'inherit' => $inherit !== '' ? $inherit : 'global',
            'variant' => $variant,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeJsonField(string $json, string $label): array
    {
        $decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new RuntimeException('Stored ' . $label . ' could not be decoded into an array payload.');
        }

        return $decoded;
    }

    /**
     * @return list<string>
     */
    private function decodeJsonListField(string $json, string $label): array
    {
        $decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new RuntimeException('Stored ' . $label . ' could not be decoded into a list payload.');
        }

        return array_values(array_map('strval', $decoded));
    }

    /**
     * @param list<string> $ids
     * @return list<string>
     */
    private function normalizeIds(array $ids): array
    {
        $normalized = [];
        foreach ($ids as $id) {
            $value = trim((string) $id);
            if ($value === '') {
                continue;
            }

            $normalized[$value] = true;
        }

        return array_keys($normalized);
    }

    /**
     * @param list<string> $values
     * @return array{0: list<string>, 1: array<string, string>}
     */
    private function buildInClause(array $values, string $prefix): array
    {
        $placeholders = [];
        $params = [];

        foreach (array_values($values) as $index => $value) {
            $placeholder = ':' . $prefix . '_' . $index;
            $placeholders[] = $placeholder;
            $params[$placeholder] = $value;
        }

        return [$placeholders, $params];
    }
}
