<?php

declare(strict_types=1);

require_once __DIR__ . '/SiteSnapshotEnvelope.php';
require_once __DIR__ . '/SiteSnapshotValidationResult.php';
require_once __DIR__ . '/SiteSnapshotImportAuditRepository.php';
// DesignSystemRegistry loaded by the app-local JSONM runtime.

final class MyronSiteSnapshotImportService
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly MyronSiteSnapshotImportAuditRepository $audit,
    ) {
    }

    public function dryRun(string $snapshotJson): MyronSiteSnapshotValidationResult
    {
        $envelope = $this->validatedEnvelope($snapshotJson);
        return new MyronSiteSnapshotValidationResult(
            true,
            $this->entityCounts($envelope),
            $this->warnings($envelope),
            'Snapshot is valid. Complete replacement is available after confirmation.'
        );
    }

    /**
     * @return array<string, mixed>
     */
    public function apply(string $snapshotJson): array
    {
        $envelope = $this->validatedEnvelope($snapshotJson);
        $counts = $this->entityCounts($envelope);

        try {
            $this->pdo->beginTransaction();
            $data = $this->data($envelope);

            $this->replaceTable('site_settings', (array) ($data['siteSettings']['rows'] ?? []));
            $this->applyDesignDefinition((array) ($data['designDefinition'] ?? []));
            $this->replaceTable('custom_css_settings', (array) ($data['designDefinition']['customCssSettings'] ?? []));
            $this->replaceTable('token_patches', (array) ($data['designDefinition']['tokenPatches'] ?? []));
            $this->replaceTable('custom_section_templates', (array) ($data['customSectionTemplates']['rows'] ?? []));
            $this->replaceTable('page_template_versions', []);
            $this->replaceTable('page_templates', (array) ($data['pageTemplates']['templates'] ?? []));
            $this->insertRows('page_template_versions', (array) ($data['pageTemplates']['versions'] ?? []));
            $this->replaceTable('page_versions', []);
            $this->replaceTable('pages', (array) ($data['pages']['pages'] ?? []));
            $this->insertRows('page_versions', (array) ($data['pages']['versions'] ?? []));
            $this->replaceTable('site_page_roles', (array) ($data['pages']['roles'] ?? []));
            $this->replaceTable('menu_items', []);
            $this->replaceTable('menus', (array) ($data['navigation']['menus'] ?? []));
            $this->insertRows('menu_items', (array) ($data['navigation']['menuItems'] ?? []));
            $this->replaceTable('navigation_design_settings', (array) ($data['navigation']['navigationDesignSettings'] ?? []));
            $this->replaceTable('navigation_designs', (array) ($data['navigation']['navigationDesigns'] ?? []));

            $this->pdo->commit();
            $this->audit->append('import', 'success', 'Site snapshot import applied.', $counts);

            return [
                'applied' => true,
                'entityCounts' => $counts,
                'warnings' => $this->warnings($envelope),
            ];
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            $this->audit->append('import', 'failure', $e->getMessage(), $counts);
            throw $e;
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function validatedEnvelope(string $snapshotJson): array
    {
        $decoded = json_decode($snapshotJson, true);
        if (!is_array($decoded)) {
            $this->audit->append('validate', 'failure', 'Snapshot JSON could not be parsed.');
            throw new InvalidArgumentException('Snapshot JSON could not be parsed.');
        }

        $version = trim((string) ($decoded['snapshotVersion'] ?? ''));
        if ($version !== MyronSiteSnapshotEnvelope::SNAPSHOT_VERSION) {
            $this->audit->append('validate', 'failure', 'Snapshot version is incompatible: ' . $version);
            throw new InvalidArgumentException('Snapshot version is incompatible: ' . $version);
        }

        $declaredHash = strtolower(trim((string) ($decoded['sha256'] ?? '')));
        if (preg_match('/^[a-f0-9]{64}$/', $declaredHash) !== 1) {
            $this->audit->append('validate', 'failure', 'Snapshot sha256 is missing or malformed.');
            throw new InvalidArgumentException('Snapshot sha256 is missing or malformed.');
        }

        $actualHash = MyronSiteSnapshotEnvelope::hash($decoded);
        if (!hash_equals($declaredHash, $actualHash)) {
            $this->audit->append('validate', 'failure', 'Snapshot sha256 does not match the envelope body.');
            throw new InvalidArgumentException('Snapshot sha256 does not match the envelope body.');
        }

        $data = $this->data($decoded);
        foreach (MyronSiteSnapshotEnvelope::REQUIRED_DATA_SECTIONS as $section) {
            if (!array_key_exists($section, $data) || !is_array($data[$section])) {
                $this->audit->append('validate', 'failure', 'Snapshot section is missing or invalid: ' . $section);
                throw new InvalidArgumentException('Snapshot section is missing or invalid: ' . $section);
            }
        }

        return $decoded;
    }

    /**
     * @param array<string, mixed> $envelope
     * @return array<string, mixed>
     */
    private function data(array $envelope): array
    {
        $data = $envelope['data'] ?? null;
        if (!is_array($data)) {
            throw new InvalidArgumentException('Snapshot data object is missing.');
        }
        return $data;
    }

    /**
     * @param array<string, mixed> $envelope
     * @return array<string, int>
     */
    private function entityCounts(array $envelope): array
    {
        $counts = $envelope['entityCounts'] ?? null;
        if (is_array($counts)) {
            $normalised = [];
            foreach ($counts as $key => $value) {
                $normalised[(string) $key] = (int) $value;
            }
            return $normalised;
        }

        $data = $this->data($envelope);
        return [
            'siteSettings' => count((array) ($data['siteSettings']['rows'] ?? [])),
            'tokenPatches' => count((array) ($data['designDefinition']['tokenPatches'] ?? [])),
            'customCssSettings' => count((array) ($data['designDefinition']['customCssSettings'] ?? [])),
            'customSectionTemplates' => count((array) ($data['customSectionTemplates']['rows'] ?? [])),
            'pageTemplates' => count((array) ($data['pageTemplates']['templates'] ?? [])),
            'pageTemplateVersions' => count((array) ($data['pageTemplates']['versions'] ?? [])),
            'pages' => count((array) ($data['pages']['pages'] ?? [])),
            'pageVersions' => count((array) ($data['pages']['versions'] ?? [])),
            'pageRoles' => count((array) ($data['pages']['roles'] ?? [])),
            'menus' => count((array) ($data['navigation']['menus'] ?? [])),
            'menuItems' => count((array) ($data['navigation']['menuItems'] ?? [])),
            'navigationDesignSettings' => count((array) ($data['navigation']['navigationDesignSettings'] ?? [])),
            'navigationDesigns' => count((array) ($data['navigation']['navigationDesigns'] ?? [])),
        ];
    }

    /**
     * @param array<string, mixed> $envelope
     * @return list<string>
     */
    private function warnings(array $envelope): array
    {
        $warnings = ['Import is complete replacement only. Users, credentials, uploaded binaries, mail, and payment secrets are not included.'];
        $sourceUrl = trim((string) ($envelope['sourceUrl'] ?? ''));
        if ($sourceUrl !== '' && function_exists('myronAppUrl') && $sourceUrl === myronAppUrl()) {
            $warnings[] = 'Snapshot source URL matches this install.';
        }
        return $warnings;
    }

    /**
     * @param list<array<string, mixed>> $rows
     */
    private function replaceTable(string $table, array $rows): void
    {
        if (!$this->tableExists($table)) {
            return;
        }
        $this->pdo->exec('DELETE FROM ' . $table);
        $this->insertRows($table, $rows);
    }

    /**
     * @param list<array<string, mixed>> $rows
     */
    private function insertRows(string $table, array $rows): void
    {
        if (!$this->tableExists($table) || $rows === []) {
            return;
        }

        $columns = $this->columns($table);
        if ($columns === []) {
            return;
        }

        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $insert = array_intersect_key($row, array_fill_keys($columns, true));
            if ($insert === []) {
                continue;
            }
            $names = array_keys($insert);
            $placeholders = array_map(static fn (string $name): string => ':' . $name, $names);
            $statement = $this->pdo->prepare(
                'INSERT INTO ' . $table . ' (' . implode(', ', $names) . ') VALUES (' . implode(', ', $placeholders) . ')'
            );
            $params = [];
            foreach ($insert as $key => $value) {
                $params[':' . $key] = $value;
            }
            $statement->execute($params);
        }
    }

    /**
     * @param array<string, mixed> $design
     */
    private function applyDesignDefinition(array $design): void
    {
        $definition = $design['current']['definition'] ?? $design['definition'] ?? null;
        if (!is_array($definition)) {
            return;
        }

        $tempPath = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'myron-site-snapshot-definition-' . bin2hex(random_bytes(8)) . '.jsonm';
        file_put_contents($tempPath, MyronSiteSnapshotEnvelope::prettyJson($definition));
        try {
            $registry = new DesignSystemRegistry();
            if (method_exists($registry, 'ensureStorage')) {
                $registry->ensureStorage();
            }
            $registry->importDefinitionFile($tempPath, (string) ($definition['name'] ?? 'Imported Snapshot Design'), true);
        } finally {
            if (is_file($tempPath)) {
                @unlink($tempPath);
            }
        }
    }

    /**
     * @return list<string>
     */
    private function columns(string $table): array
    {
        $driver = (string) $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
        if ($driver === 'sqlite') {
            $rows = $this->pdo->query('PRAGMA table_info(' . $table . ')')?->fetchAll(PDO::FETCH_ASSOC) ?: [];
            return array_values(array_map(static fn (array $row): string => (string) ($row['name'] ?? ''), $rows));
        }

        $statement = $this->pdo->query('DESCRIBE ' . $table);
        $rows = $statement ? $statement->fetchAll(PDO::FETCH_ASSOC) : [];
        return array_values(array_map(static fn (array $row): string => (string) ($row['Field'] ?? ''), $rows ?: []));
    }

    private function tableExists(string $table): bool
    {
        try {
            $this->pdo->query('SELECT 1 FROM ' . $table . ' LIMIT 1');
            return true;
        } catch (Throwable) {
            return false;
        }
    }
}
