<?php

declare(strict_types=1);

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

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

    /**
     * Canonical user-facing site type names. Use these wherever a site type appears
     * in chrome (pills, headings, buttons, breadcrumbs) — never the lowercase code
     * (`agy`/`biz`) or the legacy paired phrases (`Marketer (Creator)`/`E-commerce / Retail`).
     *
     * Per STN-012 closeout direction: pairs collapse to the canonical name (left side):
     *   Creator / Marketer  →  Creator
     *   Retail / E-commerce →  Retail
     * etc. The canonical list comes from the operator's site-type list in the Studio.
     *
     * @return array<string, string>
     */
    public static function shortPrimarySiteTypeLabels(): array
    {
        return [
            'site-type.agy' => 'Agency',
            'site-type.biz' => 'Business',
            'site-type.blog' => 'Blog',
            'site-type.ecom' => 'Retail',
            'site-type.edu' => 'Education',
            'site-type.mkt' => 'Creator',
            'site-type.news' => 'News',
            'site-type.port' => 'Portfolio',
            'site-type.saas' => 'Software',
        ];
    }

    /**
     * Canonical user-facing names for the four subtype bundles (parents in the
     * Site Templates folder hierarchy: BIZ→RST, ECOM→BKG, AGY→PS, PORT→CRS).
     *
     * @return array<string, string>
     */
    public static function shortSubtypeLabels(): array
    {
        return [
            'site-type.rst' => 'Restaurant',
            'site-type.bkg' => 'Booking',
            'site-type.ps' => 'Professional Services',
            'site-type.crs' => 'Creative Studio',
        ];
    }

    /**
     * Resolves a friendly label for any site-type code or bundle id (primary or subtype).
     * Tolerant of multiple input forms: full bundle id (`site-type.agy`), bare code (`agy`),
     * upper-case code (`AGY`).
     *
     * Returns the canonical name from {@see shortPrimarySiteTypeLabels()} or
     * {@see shortSubtypeLabels()} when known, or the provided string title-cased as a fallback.
     */
    public static function friendlyLabelForCode(string $code): string
    {
        $trimmed = trim($code);
        if ($trimmed === '') {
            return '';
        }

        $normalizedBundle = str_starts_with($trimmed, 'site-type.')
            ? strtolower($trimmed)
            : 'site-type.' . strtolower($trimmed);

        $primary = self::shortPrimarySiteTypeLabels();
        if (isset($primary[$normalizedBundle])) {
            return $primary[$normalizedBundle];
        }

        $subtype = self::shortSubtypeLabels();
        if (isset($subtype[$normalizedBundle])) {
            return $subtype[$normalizedBundle];
        }

        // Unknown code — return title-cased fallback rather than the abbreviation.
        return ucwords(strtolower($trimmed));
    }

    /**
     * @param array<string, mixed> $siteType
     */
    public static function displayLabelForSiteType(array $siteType): string
    {
        $bundleId = (string) ($siteType['id'] ?? '');
        $shortLabels = self::shortPrimarySiteTypeLabels();
        if (isset($shortLabels[$bundleId])) {
            return $shortLabels[$bundleId];
        }
        $subtypeLabels = self::shortSubtypeLabels();
        if (isset($subtypeLabels[$bundleId])) {
            return $subtypeLabels[$bundleId];
        }

        return (string) ($siteType['name'] ?? $bundleId);
    }

    /**
     * @return list<string>
     */
    public function listBundleIds(): array
    {
        $statement = $this->pdo->query('SELECT bundle_id FROM site_type_registry ORDER BY bundle_id ASC');
        $rows = $statement ? $statement->fetchAll(PDO::FETCH_COLUMN) : [];

        return array_values(array_map('strval', is_array($rows) ? $rows : []));
    }

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

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

        $sql .= ' ORDER BY bundle_type ASC, code ASC, bundle_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->hydrateSiteType($row), $rows);
    }

    public function findSiteTypeById(string $bundleId): ?array
    {
        $statement = $this->pdo->prepare('SELECT * FROM site_type_registry WHERE bundle_id = :bundle_id LIMIT 1');
        $statement->execute([':bundle_id' => trim($bundleId)]);
        $row = $statement->fetch();

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

    /**
     * @param array<string, mixed> $record
     * @return array<string, mixed>
     */
    public function saveSiteType(array $record, string $sourcePath): array
    {
        $bundleId = trim((string) ($record['id'] ?? ''));
        if ($bundleId === '') {
            throw new InvalidArgumentException('Site-type records require a non-empty `id`.');
        }

        $code = trim((string) ($record['code'] ?? ''));
        if ($code === '') {
            throw new InvalidArgumentException('Site-type `' . $bundleId . '` is missing `code`.');
        }

        $bundleType = trim((string) ($record['bundleType'] ?? ''));
        if (!in_array($bundleType, ['primary', 'subtype'], true)) {
            throw new InvalidArgumentException('Site-type `' . $bundleId . '` has an unsupported bundleType.');
        }

        $defaultTemplates = $record['defaultTemplates'] ?? [];
        $optionalTemplates = $record['optionalTemplates'] ?? [];
        if (!is_array($defaultTemplates) || !is_array($optionalTemplates)) {
            throw new InvalidArgumentException('Site-type `' . $bundleId . '` must expose template references as lists.');
        }

        $parentBundleIds = $this->parentBundleIds($record);
        $parentBundleId = (string) ($parentBundleIds[0] ?? '');
        $indexable = array_key_exists('indexable', $record) ? (bool) $record['indexable'] : true;
        $payload = json_encode($record, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
        $now = myronNow();

        if ($this->recordExists($bundleId)) {
            $statement = $this->pdo->prepare(
                'UPDATE site_type_registry
                 SET code = :code,
                     name = :name,
                     bundle_type = :bundle_type,
                     parent_bundle_id = :parent_bundle_id,
                     indexable = :indexable,
                     source_path = :source_path,
                     default_template_count = :default_template_count,
                     optional_template_count = :optional_template_count,
                     record_json = :record_json,
                     updated_at = :updated_at
                 WHERE bundle_id = :bundle_id'
            );
            $statement->execute([
                ':code' => $code,
                ':name' => trim((string) ($record['name'] ?? '')),
                ':bundle_type' => $bundleType,
                ':parent_bundle_id' => $parentBundleId !== '' ? $parentBundleId : null,
                ':indexable' => $indexable ? 1 : 0,
                ':source_path' => trim($sourcePath),
                ':default_template_count' => count($defaultTemplates),
                ':optional_template_count' => count($optionalTemplates),
                ':record_json' => $payload,
                ':updated_at' => $now,
                ':bundle_id' => $bundleId,
            ]);
        } else {
            $statement = $this->pdo->prepare(
                'INSERT INTO site_type_registry (
                    bundle_id,
                    code,
                    name,
                    bundle_type,
                    parent_bundle_id,
                    indexable,
                    source_path,
                    default_template_count,
                    optional_template_count,
                    record_json,
                    seeded_at,
                    updated_at
                 ) VALUES (
                    :bundle_id,
                    :code,
                    :name,
                    :bundle_type,
                    :parent_bundle_id,
                    :indexable,
                    :source_path,
                    :default_template_count,
                    :optional_template_count,
                    :record_json,
                    :seeded_at,
                    :updated_at
                 )'
            );
            $statement->execute([
                ':bundle_id' => $bundleId,
                ':code' => $code,
                ':name' => trim((string) ($record['name'] ?? '')),
                ':bundle_type' => $bundleType,
                ':parent_bundle_id' => $parentBundleId !== '' ? $parentBundleId : null,
                ':indexable' => $indexable ? 1 : 0,
                ':source_path' => trim($sourcePath),
                ':default_template_count' => count($defaultTemplates),
                ':optional_template_count' => count($optionalTemplates),
                ':record_json' => $payload,
                ':seeded_at' => $now,
                ':updated_at' => $now,
            ]);
        }

        $saved = $this->findSiteTypeById($bundleId);
        if ($saved === null) {
            throw new RuntimeException('Failed to reload site-type `' . $bundleId . '` after save.');
        }

        return $saved;
    }

    public function getIndexableByBundleId(string $bundleId): bool
    {
        $normalized = trim($bundleId);
        if ($normalized === '') {
            return true;
        }

        $statement = $this->pdo->prepare('SELECT indexable FROM site_type_registry WHERE bundle_id = :bundle_id LIMIT 1');
        $statement->execute([':bundle_id' => $normalized]);
        $value = $statement->fetchColumn();

        return $value === false ? true : (int) $value === 1;
    }

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

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

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

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

    /**
     * @param array<string, mixed> $record
     * @return list<string>
     */
    private function parentBundleIds(array $record): array
    {
        $parents = [];
        $addParent = static function (mixed $parentBundle) use (&$parents): void {
            $normalized = trim((string) $parentBundle);
            if ($normalized !== '') {
                $parents[$normalized] = true;
            }
        };

        $legacyParent = $record['parentBundle'] ?? null;
        if (is_array($legacyParent)) {
            foreach ($legacyParent as $parentBundle) {
                $addParent($parentBundle);
            }
        } else {
            $addParent($legacyParent);
        }

        foreach (is_array($record['parentBundles'] ?? null) ? $record['parentBundles'] : [] as $parentBundle) {
            $addParent($parentBundle);
        }

        return array_keys($parents);
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function hydrateSiteType(array $row): array
    {
        $record = $this->decodeJsonField((string) ($row['record_json'] ?? ''), 'site-type record');
        $record['_registry'] = [
            'code' => (string) ($row['code'] ?? ''),
            'bundleType' => (string) ($row['bundle_type'] ?? ''),
            'parentBundleId' => (string) ($row['parent_bundle_id'] ?? ''),
            'indexable' => (int) ($row['indexable'] ?? 1) === 1,
            'sourcePath' => (string) ($row['source_path'] ?? ''),
            'defaultTemplateCount' => (int) ($row['default_template_count'] ?? 0),
            'optionalTemplateCount' => (int) ($row['optional_template_count'] ?? 0),
            'seededAt' => (string) ($row['seeded_at'] ?? ''),
            'updatedAt' => (string) ($row['updated_at'] ?? ''),
        ];

        return $record;
    }

    /**
     * @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;
    }

    /**
     * @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];
    }
}
