<?php

declare(strict_types=1);

final class MyronBundleEnableService
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly MyronSiteTypeRegistryRepository $siteTypes,
        private readonly MyronBundleActivationRepository $bundles,
        private readonly DisabledPageRedirectsRepository $redirects,
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function enable(string $bundleId, string $conflictResolution, MyronActionCallerContext $caller): array
    {
        $bundleId = trim($bundleId);
        $resolution = in_array($conflictResolution, ['skip', 'rename', 'abort'], true) ? $conflictResolution : 'skip';
        $this->assertEnableAllowed($bundleId);

        $disabledPages = $this->disabledPagesForBundle($bundleId);
        $conflicts = $this->detectConflicts($disabledPages);
        if ($conflicts !== [] && $resolution === 'abort') {
            return [
                'conflict' => true,
                'conflicts' => $conflicts,
                'summary' => $this->emptySummary(),
            ];
        }

        $disableAudit = $this->latestDisableAuditForBundle($bundleId);
        $auditPayload = is_array($disableAudit['after'] ?? null) ? $disableAudit['after'] : [];
        $menuItemsRemoved = is_array($auditPayload['menu_items_removed'] ?? null) ? $auditPayload['menu_items_removed'] : [];
        $beforeState = $this->bundles->findBundleState($bundleId);
        $auditId = myronGenerateId('baa');
        $summary = $this->emptySummary();

        $this->pdo->beginTransaction();
        $restoredPageIds = [];
        try {
            $skipPageIds = [];
            foreach ($disabledPages as $page) {
                $pageId = (string) ($page['id'] ?? '');
                if ($pageId === '') {
                    continue;
                }
                $conflict = $conflicts[$pageId] ?? null;
                if (!is_array($conflict)) {
                    $this->restorePage($pageId);
                    $summary['pages_restored']++;
                    $restoredPageIds[] = $pageId;
                    continue;
                }
                if ($resolution === 'rename') {
                    $newPath = $this->nextRestoredPath((string) $page['path']);
                    if ($newPath !== null) {
                        $this->restorePage($pageId, $newPath);
                        $summary['pages_restored']++;
                        $restoredPageIds[] = $pageId;
                        $summary['pages_renamed'][] = [
                            'page_id' => $pageId,
                            'original_slug' => (string) $page['path'],
                            'new_slug' => $newPath,
                        ];
                        continue;
                    }
                    $summary['pages_skipped'][] = [
                        'page_id' => $pageId,
                        'conflicting_slug' => (string) $page['path'],
                        'reason' => 'rename_exhausted',
                    ];
                } else {
                    $summary['pages_skipped'][] = [
                        'page_id' => $pageId,
                        'conflicting_slug' => (string) $page['path'],
                    ];
                }
                $skipPageIds[$pageId] = true;
            }

            foreach ($menuItemsRemoved as $item) {
                if (!is_array($item)) {
                    continue;
                }
                $pageId = (string) ($item['page_id'] ?? '');
                $menuId = (string) ($item['menu_id'] ?? '');
                if ($pageId === '' || isset($skipPageIds[$pageId])) {
                    $summary['menu_items_skipped'][] = ['page_id' => $pageId, 'reason' => 'page_skipped'];
                    continue;
                }
                if (!$this->menuExists($menuId)) {
                    $summary['menu_items_skipped'][] = ['page_id' => $pageId, 'reason' => 'menu_deleted'];
                    continue;
                }
                if ($this->menuItemExistsForPage($menuId, $pageId)) {
                    $summary['menu_items_skipped'][] = ['page_id' => $pageId, 'reason' => 'menu_item_exists'];
                    continue;
                }
                $parentId = trim((string) ($item['parent_menu_item_id'] ?? ''));
                if ($parentId !== '' && !$this->menuItemExists($parentId)) {
                    $parentId = '';
                }
                $this->insertMenuItem($item, $parentId !== '' ? $parentId : null);
                $summary['menu_items_restored']++;
            }

            $summary['redirects_removed'] = $this->redirects->deleteForBundle($bundleId);
            $statement = $this->pdo->prepare(
                'UPDATE bundle_activation_state
                 SET is_enabled = 1,
                     updated_at = :updated_at
                 WHERE bundle_id = :bundle_id
                   AND is_enabled = 0'
            );
            $statement->execute([':updated_at' => myronNow(), ':bundle_id' => $bundleId]);
            if ($statement->rowCount() !== 1) {
                throw new RuntimeException('Bundle state changed while enabling. Try again.');
            }

            $after = [
                'schema_version' => 1,
                'bundle_id' => $bundleId,
                'collision_resolution' => $resolution,
                'summary' => $summary,
            ];
            $this->insertAudit($auditId, (string) $caller->callerId, 'enable', $this->bundles->getPrimaryBundleId(), $beforeState ?? [], $after);
            $this->pdo->commit();
        } catch (Throwable $exception) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $exception;
        }

        if ($restoredPageIds !== [] && function_exists('myronSearchIndexer')) {
            $indexer = myronSearchIndexer();
            foreach ($restoredPageIds as $restoredId) {
                $indexer->indexPageById($restoredId);
            }
        }

        return [
            'audit_id' => $auditId,
            'summary' => $summary,
        ];
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function disabledPagesForBundle(string $bundleId): array
    {
        $statement = $this->pdo->prepare(
            'SELECT id, slug, path, title, publication_state
             FROM pages
             WHERE source_bundle_id = :bundle_id
               AND disabled_by_bundle_removal_at IS NOT NULL
             ORDER BY path ASC'
        );
        $statement->execute([':bundle_id' => trim($bundleId)]);
        return array_values(array_filter($statement->fetchAll(PDO::FETCH_ASSOC), 'is_array'));
    }

    /**
     * @param list<array<string, mixed>> $pages
     * @return array<string, array<string, mixed>>
     */
    public function detectConflicts(array $pages): array
    {
        $conflicts = [];
        $statement = $this->pdo->prepare(
            'SELECT id, path
             FROM pages
             WHERE path = :path
               AND id <> :page_id
               AND publication_state = \'published\'
               AND (disabled_by_bundle_removal_at IS NULL OR disabled_by_bundle_removal_at = \'\')
             LIMIT 1'
        );
        foreach ($pages as $page) {
            $pageId = (string) ($page['id'] ?? '');
            $path = (string) ($page['path'] ?? '');
            if ($pageId === '' || $path === '') {
                continue;
            }
            $statement->execute([':path' => $path, ':page_id' => $pageId]);
            $row = $statement->fetch(PDO::FETCH_ASSOC);
            if (is_array($row)) {
                $conflicts[$pageId] = [
                    'page_id' => $pageId,
                    'conflicting_slug' => $path,
                    'conflicting_page_id' => (string) ($row['id'] ?? ''),
                ];
            }
        }
        return $conflicts;
    }

    private function assertEnableAllowed(string $bundleId): void
    {
        if ($bundleId === '' || !is_array($this->siteTypes->findSiteTypeById($bundleId))) {
            throw new InvalidArgumentException('Bundle not found.');
        }
        $state = $this->bundles->findBundleState($bundleId);
        if (!is_array($state) || ($state['is_enabled'] ?? true) === true) {
            throw new RuntimeException('Bundle is already enabled.');
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function emptySummary(): array
    {
        return [
            'pages_restored' => 0,
            'pages_skipped' => [],
            'pages_renamed' => [],
            'menu_items_restored' => 0,
            'menu_items_skipped' => [],
            'redirects_removed' => 0,
        ];
    }

    private function restorePage(string $pageId, ?string $newPath = null): void
    {
        $fields = 'disabled_by_bundle_removal_at = NULL, updated_at = :updated_at';
        $params = [':updated_at' => myronNow(), ':page_id' => $pageId];
        if ($newPath !== null) {
            $fields .= ', path = :path, slug = :slug';
            $params[':path'] = $newPath;
            $params[':slug'] = myronSlugFromPath($newPath);
        }
        $statement = $this->pdo->prepare('UPDATE pages SET ' . $fields . ' WHERE id = :page_id');
        $statement->execute($params);
    }

    private function nextRestoredPath(string $path): ?string
    {
        $base = myronNormalizePath($path);
        $prefix = $base === '/' ? '/home-restored' : $base . '-restored';
        for ($i = 1; $i <= 10; $i++) {
            $candidate = $i === 1 ? $prefix : $prefix . '-' . $i;
            if (!$this->pathOrSlugExists($candidate, myronSlugFromPath($candidate))) {
                return $candidate;
            }
        }
        return null;
    }

    private function pathOrSlugExists(string $path, string $slug): bool
    {
        $statement = $this->pdo->prepare('SELECT id FROM pages WHERE path = :path OR slug = :slug LIMIT 1');
        $statement->execute([':path' => $path, ':slug' => $slug]);
        return $statement->fetchColumn() !== false;
    }

    private function menuExists(string $menuId): bool
    {
        if (trim($menuId) === '') {
            return false;
        }
        $statement = $this->pdo->prepare('SELECT id FROM menus WHERE id = :id LIMIT 1');
        $statement->execute([':id' => $menuId]);
        return $statement->fetchColumn() !== false;
    }

    private function menuItemExists(string $itemId): bool
    {
        $statement = $this->pdo->prepare('SELECT id FROM menu_items WHERE id = :id LIMIT 1');
        $statement->execute([':id' => $itemId]);
        return $statement->fetchColumn() !== false;
    }

    private function menuItemExistsForPage(string $menuId, string $pageId): bool
    {
        $statement = $this->pdo->prepare('SELECT id FROM menu_items WHERE menu_id = :menu_id AND page_id = :page_id LIMIT 1');
        $statement->execute([':menu_id' => $menuId, ':page_id' => $pageId]);
        return $statement->fetchColumn() !== false;
    }

    /**
     * @param array<string, mixed> $item
     */
    private function insertMenuItem(array $item, ?string $parentId): void
    {
        $id = (string) ($item['menu_item_id'] ?? '');
        if ($id === '' || $this->menuItemExists($id)) {
            $id = myronGenerateId('menuitem');
        }
        $statement = $this->pdo->prepare(
            'INSERT INTO menu_items (id, menu_id, page_id, label, sort_order, parent_menu_item_id, created_at, updated_at)
             VALUES (:id, :menu_id, :page_id, :label, :sort_order, :parent_menu_item_id, :created_at, :updated_at)'
        );
        $statement->execute([
            ':id' => $id,
            ':menu_id' => (string) ($item['menu_id'] ?? ''),
            ':page_id' => (string) ($item['page_id'] ?? ''),
            ':label' => (string) ($item['label'] ?? ''),
            ':sort_order' => (int) ($item['sort_order'] ?? 0),
            ':parent_menu_item_id' => $parentId,
            ':created_at' => (string) (($item['created_at'] ?? '') ?: myronNow()),
            ':updated_at' => myronNow(),
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    private function latestDisableAuditForBundle(string $bundleId): array
    {
        $statement = $this->pdo->prepare(
            'SELECT *
             FROM bundle_activation_audit
             WHERE context = \'disable\'
             ORDER BY created_at DESC, id DESC
             LIMIT 50'
        );
        $statement->execute();
        foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) {
            if (!is_array($row)) {
                continue;
            }
            $after = json_decode((string) ($row['after_json'] ?? '{}'), true);
            if (is_array($after) && (string) ($after['bundle_id'] ?? '') === $bundleId) {
                $row['before'] = json_decode((string) ($row['before_json'] ?? '{}'), true) ?: [];
                $row['after'] = $after;
                return $row;
            }
        }

        return [];
    }

    /**
     * @param array<string, mixed>|list<array<string, mixed>> $before
     * @param array<string, mixed>|list<array<string, mixed>> $after
     */
    private function insertAudit(string $id, string $actorId, string $context, ?string $primaryBundleId, array $before, array $after): void
    {
        $statement = $this->pdo->prepare(
            'INSERT INTO bundle_activation_audit (
                id, actor_id, context, primary_bundle_id, before_json, after_json, created_at
             ) VALUES (
                :id, :actor_id, :context, :primary_bundle_id, :before_json, :after_json, :created_at
             )'
        );
        $statement->execute([
            ':id' => $id,
            ':actor_id' => $actorId,
            ':context' => $context,
            ':primary_bundle_id' => $primaryBundleId,
            ':before_json' => json_encode($before, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES),
            ':after_json' => json_encode($after, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES),
            ':created_at' => myronNow(),
        ]);
    }
}
