<?php

declare(strict_types=1);

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

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

    /**
     * @return list<string>
     */
    public function listComponentIds(): array
    {
        $statement = $this->pdo->query('SELECT component_id FROM component_registry ORDER BY component_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 listComponents(?string $classification = null): array
    {
        $sql = 'SELECT * FROM component_registry';
        $params = [];

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

        $sql .= ' ORDER BY classification ASC, component_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->hydrateComponent($row), $rows);
    }

    public function findComponentById(string $componentId): ?array
    {
        $statement = $this->pdo->prepare('SELECT * FROM component_registry WHERE component_id = :component_id LIMIT 1');
        $statement->execute([':component_id' => trim($componentId)]);
        $row = $statement->fetch();

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

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

        $classification = trim((string) ($record['classification'] ?? ''));
        if (!in_array($classification, ['atom', 'primitive'], true)) {
            throw new InvalidArgumentException('Component `' . $componentId . '` has an unsupported classification.');
        }

        $registryAddress = trim((string) ($record['registryAddress'] ?? ''));
        if ($registryAddress === '') {
            throw new InvalidArgumentException('Component `' . $componentId . '` is missing `registryAddress`.');
        }

        $variants = $record['variants'] ?? [];
        if (!is_array($variants)) {
            throw new InvalidArgumentException('Component `' . $componentId . '` must expose variants as a list.');
        }

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

        $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);

        $now = myronNow();

        if ($this->recordExists($componentId)) {
            $statement = $this->pdo->prepare(
                'UPDATE component_registry
                 SET classification = :classification,
                     registry_address = :registry_address,
                     source_path = :source_path,
                     role_scope_json = :role_scope_json,
                     projected_role_scope_json = :projected_role_scope_json,
                     variant_count = :variant_count,
                     record_json = :record_json,
                     updated_at = :updated_at
                 WHERE component_id = :component_id'
            );
            $statement->execute([
                ':classification' => $classification,
                ':registry_address' => $registryAddress,
                ':source_path' => trim($sourcePath),
                ':role_scope_json' => $roleScopeJson,
                ':projected_role_scope_json' => $projectedRoleScopeJson,
                ':variant_count' => count($variants),
                ':record_json' => $payload,
                ':updated_at' => $now,
                ':component_id' => $componentId,
            ]);
        } else {
            $statement = $this->pdo->prepare(
                'INSERT INTO component_registry (
                    component_id,
                    classification,
                    registry_address,
                    source_path,
                    role_scope_json,
                    projected_role_scope_json,
                    variant_count,
                    record_json,
                    seeded_at,
                    updated_at
                 ) VALUES (
                    :component_id,
                    :classification,
                    :registry_address,
                    :source_path,
                    :role_scope_json,
                    :projected_role_scope_json,
                    :variant_count,
                    :record_json,
                    :seeded_at,
                    :updated_at
                 )'
            );
            $statement->execute([
                ':component_id' => $componentId,
                ':classification' => $classification,
                ':registry_address' => $registryAddress,
                ':source_path' => trim($sourcePath),
                ':role_scope_json' => $roleScopeJson,
                ':projected_role_scope_json' => $projectedRoleScopeJson,
                ':variant_count' => count($variants),
                ':record_json' => $payload,
                ':seeded_at' => $now,
                ':updated_at' => $now,
            ]);
        }

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

        return $saved;
    }

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

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

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

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

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function hydrateComponent(array $row): array
    {
        $record = $this->decodeJsonField((string) ($row['record_json'] ?? ''), 'component record');
        $record['roleScope'] = $row['role_scope_json'] === null
            ? null
            : $this->decodeJsonField((string) $row['role_scope_json'], 'component role scope');
        $record['projectedRoleScope'] = $row['projected_role_scope_json'] === null
            ? null
            : $this->decodeJsonField((string) $row['projected_role_scope_json'], 'component projected role scope');
        if (!is_array($record['editableTokens'] ?? null)) {
            $record['editableTokens'] = $this->deriveEditableTokens($record);
        }
        if (!array_key_exists('creativeLayer', $record)) {
            $record['creativeLayer'] = null;
        }
        $record['_registry'] = [
            'classification' => (string) ($row['classification'] ?? ''),
            'registryAddress' => (string) ($row['registry_address'] ?? ''),
            'sourcePath' => (string) ($row['source_path'] ?? ''),
            'variantCount' => (int) ($row['variant_count'] ?? 0),
            'seededAt' => (string) ($row['seeded_at'] ?? ''),
            'updatedAt' => (string) ($row['updated_at'] ?? ''),
        ];

        return $record;
    }

    /**
     * @param array<string, mixed> $record
     * @return list<array<string, string>>
     */
    private function deriveEditableTokens(array $record): array
    {
        $tokens = is_array($record['tokenDependencies'] ?? null) ? $record['tokenDependencies'] : [];
        $editable = [];
        foreach ($tokens as $token) {
            $cssVar = trim((string) $token);
            if ($cssVar === '' || !preg_match('/^--myron-[a-z0-9_-]+$/', $cssVar)) {
                continue;
            }

            $editable[] = [
                'token' => $cssVar,
                'cssVar' => $cssVar,
                'label' => ucwords(str_replace(['--myron-', '-', '_'], ['', ' ', ' '], $cssVar)),
                'controlType' => $this->controlTypeForToken($cssVar),
            ];
        }

        return $editable;
    }

    private function controlTypeForToken(string $cssVar): string
    {
        if (str_contains($cssVar, 'color')) {
            return 'color';
        }
        if (str_contains($cssVar, 'opacity')) {
            return 'opacity';
        }
        if (str_contains($cssVar, 'font')) {
            return 'fontRole';
        }
        if (str_contains($cssVar, 'space') || str_contains($cssVar, 'size')) {
            return 'scale';
        }
        if (str_contains($cssVar, 'radius') || str_contains($cssVar, 'border')) {
            return 'lengthPx';
        }

        return 'text';
    }

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