<?php

declare(strict_types=1);

final class MyronTokenPatchRepository
{
    private const PATCH_BLOCK_START = '/* MyronCMS token patches: begin */';
    private const PATCH_BLOCK_END = '/* MyronCMS token patches: end */';

    public function __construct(
        private readonly PDO $pdo,
        private readonly string $runtimeTokensPath,
    ) {
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    public function listPatchesByToken(): array
    {
        $statement = $this->pdo->query('SELECT * FROM token_patches ORDER BY token_name ASC');
        $rows = $statement ? $statement->fetchAll() : [];
        $patches = [];
        foreach (is_array($rows) ? $rows : [] as $row) {
            if (!is_array($row)) {
                continue;
            }
            $token = (string) ($row['token_name'] ?? '');
            if ($token !== '') {
                $patches[$token] = [
                    'tokenName' => $token,
                    'patchValue' => (string) ($row['patch_value'] ?? ''),
                    'note' => (string) ($row['note'] ?? ''),
                    'updatedAt' => (string) ($row['updated_at'] ?? ''),
                ];
            }
        }

        return $patches;
    }

    public function savePatch(string $tokenName, string $patchValue, string $note = '', ?string $userId = null): void
    {
        $tokenName = $this->normalizeTokenName($tokenName);
        $patchValue = trim($patchValue);
        if ($patchValue === '') {
            $this->deletePatch($tokenName);
            return;
        }

        $now = myronNow();
        $exists = $this->pdo->prepare('SELECT token_name FROM token_patches WHERE token_name = :token_name LIMIT 1');
        $exists->execute([':token_name' => $tokenName]);

        if ($exists->fetchColumn() !== false) {
            $statement = $this->pdo->prepare(
                'UPDATE token_patches
                 SET patch_value = :patch_value, note = :note, updated_by = :updated_by, updated_at = :updated_at
                 WHERE token_name = :token_name'
            );
            $statement->execute([
                ':patch_value' => $patchValue,
                ':note' => trim($note),
                ':updated_by' => $userId,
                ':updated_at' => $now,
                ':token_name' => $tokenName,
            ]);
        } else {
            $statement = $this->pdo->prepare(
                'INSERT INTO token_patches (
                    token_name, patch_value, note, updated_by, created_at, updated_at
                 ) VALUES (
                    :token_name, :patch_value, :note, :updated_by, :created_at, :updated_at
                 )'
            );
            $statement->execute([
                ':token_name' => $tokenName,
                ':patch_value' => $patchValue,
                ':note' => trim($note),
                ':updated_by' => $userId,
                ':created_at' => $now,
                ':updated_at' => $now,
            ]);
        }

        $this->compileRuntimeOverrides();
    }

    public function deletePatch(string $tokenName): void
    {
        $tokenName = $this->normalizeTokenName($tokenName);
        $statement = $this->pdo->prepare('DELETE FROM token_patches WHERE token_name = :token_name');
        $statement->execute([':token_name' => $tokenName]);
        $this->compileRuntimeOverrides();
    }

    public function clearAll(): void
    {
        $this->pdo->exec('DELETE FROM token_patches');
        $this->compileRuntimeOverrides();
    }

    /**
     * @param list<string> $validTokens
     */
    public function clearOrphanPatches(array $validTokens): int
    {
        $orphans = $this->orphanPatches($validTokens);
        if ($orphans === []) {
            return 0;
        }

        $statement = $this->pdo->prepare('DELETE FROM token_patches WHERE token_name = :token_name');
        foreach (array_keys($orphans) as $tokenName) {
            $statement->execute([':token_name' => $tokenName]);
        }

        $this->compileRuntimeOverrides();
        return count($orphans);
    }

    /**
     * @param list<string> $validTokens
     * @return array<string, array<string, mixed>>
     */
    public function orphanPatches(array $validTokens): array
    {
        $valid = array_fill_keys(array_values(array_map('strval', $validTokens)), true);
        $orphans = [];
        foreach ($this->listPatchesByToken() as $token => $patch) {
            if (!isset($valid[$token])) {
                $orphans[$token] = $patch;
            }
        }

        return $orphans;
    }

    public function compileRuntimeOverrides(): void
    {
        $current = is_file($this->runtimeTokensPath) ? (string) file_get_contents($this->runtimeTokensPath) : '';
        $base = $this->stripPatchBlock($current);
        $patches = $this->listPatchesByToken();
        $compiled = rtrim($base) . "\n";

        if ($patches !== []) {
            $compiled .= "\n" . self::PATCH_BLOCK_START . "\n";
            $compiled .= "@layer custom {\n  :root {\n";
            foreach ($patches as $token => $patch) {
                $compiled .= '    ' . $token . ': ' . trim((string) ($patch['patchValue'] ?? '')) . ";\n";
            }
            $compiled .= "  }\n}\n";
            $compiled .= self::PATCH_BLOCK_END . "\n";
        }

        $directory = dirname($this->runtimeTokensPath);
        if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
            throw new RuntimeException('Unable to create runtime token directory.');
        }

            file_put_contents($this->runtimeTokensPath, $compiled);
        myronInvalidatePublicPageCache('token patches changed');
        }

    private function normalizeTokenName(string $tokenName): string
    {
        $tokenName = trim($tokenName);
        if (!preg_match('/^--myron-[a-z0-9_-]+$/', $tokenName)) {
            throw new InvalidArgumentException('Token patches must target a valid --myron-* CSS variable.');
        }

        return $tokenName;
    }

    private function stripPatchBlock(string $css): string
    {
        return trim((string) preg_replace(
            '/' . preg_quote(self::PATCH_BLOCK_START, '/') . '.*?' . preg_quote(self::PATCH_BLOCK_END, '/') . '\s*/s',
            '',
            $css
        ));
    }
}
