<?php

declare(strict_types=1);

final class MyronBundleBootstrapService
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly MyronContentRepository $content,
        private readonly MyronTemplateDefinitionRepository $templates,
        private readonly MyronSiteTypeRegistryRepository $siteTypes,
        private readonly MyronBundleActivationRepository $bundles,
        private readonly MyronTemplateInstantiationService $instantiator,
        private readonly ?MyronSiteSetupSelectionService $selectionService = null,
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function bootstrap(?string $primaryBundleId = null, array $options = []): array
    {
        $options = $this->normalizeBootstrapOptions($options);
        $bundleId = trim((string) ($primaryBundleId ?? ($this->bundles->getPrimaryBundleId() ?? '')));
        if ($bundleId === '') {
            throw new InvalidArgumentException('A primary bundle is required before bundle bootstrap can run.');
        }

        $siteType = $this->siteTypes->findSiteTypeById($bundleId);
        if (!is_array($siteType)) {
            throw new InvalidArgumentException('Bundle `' . $bundleId . '` does not exist.');
        }
        if ((string) ($siteType['bundleType'] ?? '') !== 'primary') {
            throw new InvalidArgumentException('Bundle `' . $bundleId . '` is not a primary bundle.');
        }

        $activeSubtypeSiteTypes = $this->activeSubtypeSiteTypes($bundleId);
        $manifestTemplateIds = $this->selectionService?->selectedTemplateIdsForBootstrap($bundleId) ?? [];
        $templateIds = $manifestTemplateIds !== []
            ? $manifestTemplateIds
            : $this->defaultTemplateIds($siteType, $activeSubtypeSiteTypes);
        $runtimeTemplates = [];
        $runtimePages = [];
        $missingTemplates = [];
        $preservedPages = [];
        $skippedPages = [];

        foreach ($templateIds as $templateId) {
            $definition = $this->templates->findTemplateById($templateId);
            if (!is_array($definition)) {
                $missingTemplates[] = $templateId;
                continue;
            }

            $runtimeTemplate = $this->instantiator->instantiateTemplate($definition, [
                'syncExistingTemplate' => $options['syncExistingTemplates'],
            ]);
            $runtimeTemplates[] = $runtimeTemplate;
            $pageResult = $this->instantiator->instantiatePageWithResult($definition, $runtimeTemplate, [
                'syncExistingPageIdentity' => $options['syncExistingPageIdentity'],
                'claimPathMatches' => $options['claimPathMatches'],
                'collisionPolicy' => $options['collisionPolicy'],
            ]);
            $runtimePage = $pageResult['page'] ?? null;
            $status = (string) ($pageResult['status'] ?? '');
            if (is_array($runtimePage)) {
                $runtimePages[] = $runtimePage;
                if ($status === 'existing_preserved') {
                    $preservedPages[] = $runtimePage;
                }
            } elseif ($status === 'skipped') {
                $skippedPages[] = [
                    'sourceTemplateId' => (string) ($pageResult['sourceTemplateId'] ?? $templateId),
                    'path' => (string) ($pageResult['path'] ?? ''),
                    'reason' => (string) ($pageResult['reason'] ?? 'skipped'),
                ];
            }
        }

        if ($options['syncMenus']) {
            $this->seedMenus($siteType, $activeSubtypeSiteTypes, $runtimePages);
        }
        $seedRecords = $options['writeSeedRecords']
            ? $this->writeSeedRecords($bundleId, $siteType, $runtimePages)
            : [];
        $funnelFlow = $bundleId === 'site-type.mkt'
            ? $this->seedMktTripwireFlow($runtimePages)
            : null;

        return [
            'bundleId' => $bundleId,
            'templateIds' => $templateIds,
            'selectionManifestApplied' => $manifestTemplateIds !== [],
            'runtimeTemplates' => $runtimeTemplates,
            'runtimePages' => $runtimePages,
            'preservedPages' => $preservedPages,
            'skippedPages' => $skippedPages,
            'seedRecords' => $seedRecords,
            'funnelFlow' => $funnelFlow,
            'missingTemplates' => $missingTemplates,
        ];
    }

    /**
     * @param array<string, mixed> $options
     * @return array{syncExistingTemplates:bool,syncExistingPageIdentity:bool,claimPathMatches:bool,collisionPolicy:string,syncMenus:bool,writeSeedRecords:bool}
     */
    private function normalizeBootstrapOptions(array $options): array
    {
        $collisionPolicy = trim((string) ($options['collisionPolicy'] ?? 'rename'));
        if (!in_array($collisionPolicy, ['rename', 'skip'], true)) {
            $collisionPolicy = 'rename';
        }

        return [
            'syncExistingTemplates' => ($options['syncExistingTemplates'] ?? true) !== false,
            'syncExistingPageIdentity' => ($options['syncExistingPageIdentity'] ?? true) !== false,
            'claimPathMatches' => ($options['claimPathMatches'] ?? true) !== false,
            'collisionPolicy' => $collisionPolicy,
            'syncMenus' => ($options['syncMenus'] ?? true) !== false,
            'writeSeedRecords' => ($options['writeSeedRecords'] ?? true) !== false,
        ];
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listSeedRecords(?string $bundleId = null): array
    {
        $sql = 'SELECT * FROM bundle_seed_records';
        $params = [];
        if ($bundleId !== null && trim($bundleId) !== '') {
            $sql .= ' WHERE bundle_id = :bundle_id';
            $params[':bundle_id'] = trim($bundleId);
        }
        $sql .= ' ORDER BY seed_type ASC, seed_key ASC';
        $statement = $this->pdo->prepare($sql);
        $statement->execute($params);
        $rows = $statement->fetchAll();

        $records = [];
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $payload = json_decode((string) ($row['payload_json'] ?? '{}'), true, 512, JSON_THROW_ON_ERROR);
            $records[] = [
                'seed_key' => (string) ($row['seed_key'] ?? ''),
                'bundle_id' => (string) ($row['bundle_id'] ?? ''),
                'seed_type' => (string) ($row['seed_type'] ?? ''),
                'payload' => is_array($payload) ? $payload : [],
                'created_at' => (string) ($row['created_at'] ?? ''),
                'updated_at' => (string) ($row['updated_at'] ?? ''),
            ];
        }

        return $records;
    }

    /**
     * @param array<string, mixed> $siteType
     * @param list<array<string, mixed>> $activeSubtypeSiteTypes
     * @return list<string>
     */
    private function defaultTemplateIds(array $siteType, array $activeSubtypeSiteTypes = []): array
    {
        $templateIds = [];
        foreach (($siteType['defaultTemplates'] ?? []) as $templateId) {
            $normalized = trim((string) $templateId);
            if ($normalized !== '') {
                $templateIds[$normalized] = true;
            }
        }
        foreach ($activeSubtypeSiteTypes as $subtype) {
            if (($subtype['autoInstantiateWithParent'] ?? false) !== true) {
                continue;
            }
            foreach (($subtype['defaultTemplates'] ?? []) as $templateId) {
                $normalized = trim((string) $templateId);
                if ($normalized !== '') {
                    $templateIds[$normalized] = true;
                }
            }
        }

        return array_keys($templateIds);
    }

    /**
     * @param array<string, mixed> $siteType
     * @param list<array<string, mixed>> $activeSubtypeSiteTypes
     * @param list<array<string, mixed>> $runtimePages
     */
    private function seedMenus(array $siteType, array $activeSubtypeSiteTypes, array $runtimePages): void
    {
        $menus = $this->content->listMenusWithItems();
        $menusByPlacement = [];
        foreach ($menus as $menu) {
            $placement = (string) ($menu['placement'] ?? '');
            if ($placement !== '') {
                $menusByPlacement[$placement] = $menu;
            }
        }

        $primaryLabels = $this->primaryNavLabels($siteType, $activeSubtypeSiteTypes);
        $primaryPaths = $this->menuPathsFromLabels($primaryLabels, $runtimePages);
        $labelsByPath = $this->labelsByPath($primaryLabels, $runtimePages);
        $footerPaths = $this->footerPaths($runtimePages);

        foreach (['primary', 'footer'] as $placement) {
            if (!isset($menusByPlacement[$placement])) {
                continue;
            }

            $menu = $menusByPlacement[$placement];
            $targetPaths = $placement === 'primary' ? $primaryPaths : $footerPaths;
            $orderedPages = $this->pagesByPaths($runtimePages, $targetPaths);
            $this->syncMenuToPages((string) $menu['id'], $orderedPages, $labelsByPath);
        }
    }

    /**
     * @param array<string, mixed> $siteType
     * @param list<array<string, mixed>> $runtimePages
     * @return list<array<string, mixed>>
     */
    private function writeSeedRecords(string $bundleId, array $siteType, array $runtimePages): array
    {
        $pages = array_map(
            static fn (array $page): array => [
                'id' => (string) ($page['id'] ?? ''),
                'title' => (string) ($page['title'] ?? ''),
                'path' => (string) ($page['path'] ?? ''),
                'sourceTemplateId' => (string) ($page['source_template_id'] ?? ''),
            ],
            $runtimePages
        );
        $primaryLabels = $this->primaryNavLabels($siteType, $this->activeSubtypeSiteTypes($bundleId));
        $primaryPaths = $this->menuPathsFromLabels($primaryLabels, $runtimePages);
        $footerPaths = $this->footerPaths($runtimePages);
        $navDefaults = is_array($siteType['navDefaults'] ?? null) ? $siteType['navDefaults'] : [];
        $headerActions = $this->headerActions($navDefaults);
        $homeCta = $this->homeCta($navDefaults, $headerActions);

        $records = [
            [
                'seed_key' => $bundleId . ':navigation:primary',
                'seed_type' => 'navigation',
                'payload' => [
                    'placement' => 'primary',
                    'source' => 'site-type.navDefaults.main',
                    'labels' => $primaryLabels,
                    'pages' => array_values(array_filter($pages, static fn (array $page): bool => in_array($page['path'], $primaryPaths, true))),
                ],
            ],
            [
                'seed_key' => $bundleId . ':navigation:header-right',
                'seed_type' => 'navigation',
                'payload' => [
                    'placement' => 'headerRight',
                    'source' => 'site-type.navDefaults.headerRight',
                    'actions' => $headerActions,
                ],
            ],
            [
                'seed_key' => $bundleId . ':navigation:footer',
                'seed_type' => 'footer',
                'payload' => [
                    'placement' => 'footer',
                    'source' => 'site-type.footerCluster',
                    'cluster' => is_array($siteType['footerCluster'] ?? null) ? $siteType['footerCluster'] : [],
                    'pages' => array_values(array_filter($pages, static fn (array $page): bool => in_array($page['path'], $footerPaths, true))),
                ],
            ],
            [
                'seed_key' => $bundleId . ':cta:home',
                'seed_type' => 'cta',
                'payload' => [
                    'enabled' => $homeCta['enabled'],
                    'label' => $homeCta['label'],
                    'href' => $homeCta['href'],
                    'source' => (string) ($siteType['id'] ?? $bundleId),
                    'contexts' => is_array($siteType['ctaContext'] ?? null) ? $siteType['ctaContext'] : [],
                ],
            ],
        ];

        foreach ($records as $record) {
            $this->upsertSeedRecord($bundleId, $record['seed_key'], $record['seed_type'], $record['payload']);
        }

        return $this->listSeedRecords($bundleId);
    }

    /**
     * @param list<mixed> $labels
     * @param list<array<string, mixed>> $runtimePages
     * @return list<string>
     */
    private function menuPathsFromLabels(array $labels, array $runtimePages = []): array
    {
        $paths = [];
        $routeAliases = $this->routeAliases($runtimePages);
        foreach ($labels as $label) {
            $normalized = strtolower(trim((string) $label));
            if ($normalized === '') {
                continue;
            }

            $paths[] = $routeAliases[$normalized] ?? ($normalized === 'home' ? '/' : '/' . myronNormalizeSlug($normalized));
        }

        return array_values(array_unique($paths));
    }

    /**
     * @param list<array<string, mixed>> $runtimePages
     * @return list<string>
     */
    private function footerPaths(array $runtimePages): array
    {
        $preferred = ['/', '/portfolio', '/shop', '/category', '/cart', '/checkout', '/account', '/reservation', '/blog', '/news', '/categories', '/studio', '/about', '/contact', '/legal', '/search', '/coming-soon'];
        $available = array_fill_keys(array_map(static fn (array $page): string => (string) ($page['path'] ?? ''), $runtimePages), true);

        return array_values(array_filter($preferred, static fn (string $path): bool => isset($available[$path])));
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function activeSubtypeSiteTypes(string $parentBundleId): array
    {
        $subtypes = [];
        foreach ($this->bundles->listActiveBundles() as $state) {
            $bundleId = (string) ($state['bundle_id'] ?? '');
            if ($bundleId === '' || $bundleId === $parentBundleId) {
                continue;
            }

            $siteType = $this->siteTypes->findSiteTypeById($bundleId);
            if (!is_array($siteType) || (string) ($siteType['bundleType'] ?? '') !== 'subtype') {
                continue;
            }
            if (!in_array($parentBundleId, $this->parentBundleIds($siteType), true)) {
                continue;
            }

            $subtypes[] = $siteType;
        }

        return $subtypes;
    }

    /**
     * @param array<string, mixed> $siteType
     * @param list<array<string, mixed>> $activeSubtypeSiteTypes
     * @return list<string>
     */
    private function primaryNavLabels(array $siteType, array $activeSubtypeSiteTypes): array
    {
        $labels = is_array($siteType['navDefaults']['main'] ?? null) ? array_values($siteType['navDefaults']['main']) : ['Home', 'About', 'Services', 'Contact'];
        foreach ($activeSubtypeSiteTypes as $subtype) {
            $additions = is_array($subtype['navDefaults']['addToMain'] ?? null) ? array_values($subtype['navDefaults']['addToMain']) : [];
            foreach (array_reverse($additions) as $addition) {
                $label = trim((string) $addition);
                if ($label === '' || in_array($label, $labels, true)) {
                    continue;
                }
                array_splice($labels, min(1, count($labels)), 0, [$label]);
            }
        }

        return array_values(array_map('strval', $labels));
    }

    /**
     * @param list<string> $labels
     * @param list<array<string, mixed>> $runtimePages
     * @return array<string, string>
     */
    private function labelsByPath(array $labels, array $runtimePages): array
    {
        $paths = $this->menuPathsFromLabels($labels, $runtimePages);
        $map = [];
        foreach ($paths as $index => $path) {
            if (isset($labels[$index])) {
                $map[$path] = (string) $labels[$index];
            }
        }

        return $map;
    }

    /**
     * @param list<array<string, mixed>> $runtimePages
     * @param list<string> $paths
     * @return list<array<string, mixed>>
     */
    private function pagesByPaths(array $runtimePages, array $paths): array
    {
        $pagesByPath = [];
        foreach ($runtimePages as $page) {
            $pagesByPath[(string) ($page['path'] ?? '')] = $page;
        }

        $ordered = [];
        foreach ($paths as $path) {
            if (isset($pagesByPath[$path])) {
                $ordered[] = $pagesByPath[$path];
            }
        }

        return $ordered;
    }

    /**
     * @param list<array<string, mixed>> $pages
     * @param array<string, string> $labelsByPath
     */
    private function syncMenuToPages(string $menuId, array $pages, array $labelsByPath): void
    {
        $targetPageIds = array_values(array_filter(array_map(static fn (array $page): string => (string) ($page['id'] ?? ''), $pages)));
        $targetLookup = array_fill_keys($targetPageIds, true);

        $existing = $this->pdo->prepare('SELECT id, page_id FROM menu_items WHERE menu_id = :menu_id');
        $existing->execute([':menu_id' => $menuId]);
        foreach ($existing->fetchAll() as $item) {
            if (!is_array($item)) {
                continue;
            }
            if (!isset($targetLookup[(string) ($item['page_id'] ?? '')])) {
                $this->content->removeMenuItem((string) $item['id']);
            }
        }

        $update = $this->pdo->prepare(
            'UPDATE menu_items
             SET label = :label,
                 sort_order = :sort_order,
                 parent_menu_item_id = NULL,
                 updated_at = :updated_at
             WHERE menu_id = :menu_id
               AND page_id = :page_id'
        );

        foreach ($pages as $index => $page) {
            $pageId = (string) ($page['id'] ?? '');
            if ($pageId === '') {
                continue;
            }

            $path = (string) ($page['path'] ?? '');
            $label = (string) ($labelsByPath[$path] ?? $page['title'] ?? 'Page');
            if (!$this->menuHasPage($menuId, $pageId)) {
                $this->content->addPageToMenu($menuId, $pageId, $label);
            }

            $update->execute([
                ':label' => $label,
                ':sort_order' => $index + 1,
                ':updated_at' => myronNow(),
                ':menu_id' => $menuId,
                ':page_id' => $pageId,
            ]);
        }
    }

    /**
     * @param list<array<string, mixed>> $runtimePages
     * @return array<string, string>
     */
    private function routeAliases(array $runtimePages): array
    {
        $pathsBySource = [];
        foreach ($runtimePages as $page) {
            $pathsBySource[(string) ($page['source_template_id'] ?? '')] = (string) ($page['path'] ?? '');
        }

        $aliases = [];
        if (($pathsBySource['template.portfolio-index.port.v1'] ?? '') !== '') {
            $aliases['work'] = $pathsBySource['template.portfolio-index.port.v1'];
            $aliases['portfolio'] = $pathsBySource['template.portfolio-index.port.v1'];
        }
        if (($pathsBySource['template.portfolio-index.agy.v1'] ?? '') !== '') {
            $aliases['work'] = $pathsBySource['template.portfolio-index.agy.v1'];
            $aliases['portfolio'] = $pathsBySource['template.portfolio-index.agy.v1'];
        }
        if (($pathsBySource['template.blog-index.agy.v1'] ?? '') !== '') {
            $aliases['insights'] = $pathsBySource['template.blog-index.agy.v1'];
            $aliases['blog'] = $pathsBySource['template.blog-index.agy.v1'];
            $aliases['articles'] = $pathsBySource['template.blog-index.agy.v1'];
        }
        if (($pathsBySource['template.studio-statement.crs.v1'] ?? '') !== '') {
            $aliases['studio'] = $pathsBySource['template.studio-statement.crs.v1'];
            $aliases['process'] = $pathsBySource['template.studio-statement.crs.v1'];
        }
        if (($pathsBySource['template.studio-statement.ps.v1'] ?? '') !== '') {
            $aliases['process'] = $pathsBySource['template.studio-statement.ps.v1'];
            $aliases['our process'] = $pathsBySource['template.studio-statement.ps.v1'];
        }
        if (($pathsBySource['template.blog-index.blog.v1'] ?? '') !== '') {
            $aliases['blog'] = $pathsBySource['template.blog-index.blog.v1'];
            $aliases['articles'] = $pathsBySource['template.blog-index.blog.v1'];
            $aliases['archive'] = $pathsBySource['template.blog-index.blog.v1'];
        }
        if (($pathsBySource['template.blog-index.saas.v1'] ?? '') !== '') {
            $aliases['blog'] = $pathsBySource['template.blog-index.saas.v1'];
            $aliases['articles'] = $pathsBySource['template.blog-index.saas.v1'];
        }
        if (($pathsBySource['template.category-archive.blog.v1'] ?? '') !== '') {
            $aliases['category'] = $pathsBySource['template.category-archive.blog.v1'];
            $aliases['categories'] = $pathsBySource['template.category-archive.blog.v1'];
        }
        if (($pathsBySource['template.shop.ecom.v1'] ?? '') !== '') {
            $aliases['shop'] = $pathsBySource['template.shop.ecom.v1'];
            $aliases['store'] = $pathsBySource['template.shop.ecom.v1'];
        }
        if (($pathsBySource['template.category-archive.ecom.v1'] ?? '') !== '') {
            $aliases['collections'] = $pathsBySource['template.category-archive.ecom.v1'];
            $aliases['collection'] = $pathsBySource['template.category-archive.ecom.v1'];
            $aliases['category'] = $pathsBySource['template.category-archive.ecom.v1'];
            $aliases['categories'] = $pathsBySource['template.category-archive.ecom.v1'];
        }
        if (($pathsBySource['template.blog-index.ecom.v1'] ?? '') !== '') {
            $aliases['journal'] = $pathsBySource['template.blog-index.ecom.v1'];
            $aliases['blog'] = $pathsBySource['template.blog-index.ecom.v1'];
            $aliases['stories'] = $pathsBySource['template.blog-index.ecom.v1'];
        }
        if (($pathsBySource['template.cart.ecom.v1'] ?? '') !== '') {
            $aliases['cart'] = $pathsBySource['template.cart.ecom.v1'];
        }
        if (($pathsBySource['template.checkout.ecom.v1'] ?? '') !== '') {
            $aliases['checkout'] = $pathsBySource['template.checkout.ecom.v1'];
        }
        if (($pathsBySource['template.account.ecom.v1'] ?? '') !== '') {
            $aliases['account'] = $pathsBySource['template.account.ecom.v1'];
        }
        if (($pathsBySource['template.reservation.bkg.v1'] ?? '') !== '') {
            $aliases['reservation'] = $pathsBySource['template.reservation.bkg.v1'];
            $aliases['booking'] = $pathsBySource['template.reservation.bkg.v1'];
            $aliases['book now'] = $pathsBySource['template.reservation.bkg.v1'];
        }
        if (($pathsBySource['template.blog-index.mkt.v1'] ?? '') !== '') {
            $aliases['blog'] = $pathsBySource['template.blog-index.mkt.v1'];
        }
        if (($pathsBySource['template.featured-offers.mkt.v1'] ?? '') !== '') {
            $aliases['work with me'] = $pathsBySource['template.featured-offers.mkt.v1'];
            $aliases['featured offers'] = $pathsBySource['template.featured-offers.mkt.v1'];
        }
        if (($pathsBySource['template.funnel.squeeze.mkt.v1'] ?? '') !== '') {
            $aliases['join the list'] = $pathsBySource['template.funnel.squeeze.mkt.v1'];
            $aliases['subscribe'] = $pathsBySource['template.funnel.squeeze.mkt.v1'];
        }

        return $aliases;
    }

    /**
     * @param list<array<string, mixed>> $runtimePages
     * @return array<string, mixed>|null
     */
    private function seedMktTripwireFlow(array $runtimePages): ?array
    {
        $stepsByTemplate = [
            'template.funnel.squeeze.mkt.v1' => 'squeeze',
            'template.funnel.bridge.mkt.v1' => 'bridge',
            'template.funnel.sales-long-form.mkt.v1' => 'sales',
            'template.funnel.checkout.mkt.v1' => 'checkout',
            'template.funnel.upsell.mkt.v1' => 'upsell',
            'template.funnel.downsell.mkt.v1' => 'downsell',
            'template.funnel.confirmation.mkt.v1' => 'confirmation',
            'template.funnel.expired.mkt.v1' => 'expired',
        ];
        $pagesBySource = [];
        foreach ($runtimePages as $page) {
            $pagesBySource[(string) ($page['source_template_id'] ?? '')] = $page;
        }

        $steps = [];
        foreach ($stepsByTemplate as $templateId => $stepKey) {
            if (!isset($pagesBySource[$templateId])) {
                return null;
            }

            $page = $pagesBySource[$templateId];
            $steps[] = [
                'stepKey' => $stepKey,
                'sourceTemplateId' => $templateId,
                'routePath' => (string) ($page['path'] ?? '/' . $stepKey),
                'pageId' => (string) ($page['id'] ?? ''),
            ];
        }

        $archetypeThemeId = 'bm';
        $statement = $this->pdo->query('SELECT archetype_theme_id FROM onboarding_state WHERE id = 1 LIMIT 1');
        $storedArchetype = $statement ? $statement->fetchColumn() : false;
        if (is_string($storedArchetype) && trim($storedArchetype) !== '') {
            $archetypeThemeId = trim($storedArchetype);
        }

        $repository = new MyronFunnelFlowRepository($this->pdo);
        return $repository->saveFlow(
            'tripwire-mkt-v1',
            'site-type.mkt',
            $archetypeThemeId,
            [
                'status' => 'active',
                'flowType' => 'tripwire',
                'deadlinePolicy' => 'individualized_persistent',
                'expiredStep' => 'expired',
                'sideEffects' => ['webhook' => 'test-audit-only', 'capi' => 'test-audit-only'],
            ],
            $steps
        );
    }

    /**
     * @param array<string, mixed> $navDefaults
     * @return list<array{label:string,href:string}>
     */
    private function headerActions(array $navDefaults): array
    {
        $actions = [];
        foreach (is_array($navDefaults['headerRight'] ?? null) ? $navDefaults['headerRight'] : [] as $action) {
            $label = '';
            $href = '';
            if (is_array($action)) {
                $label = trim((string) ($action['label'] ?? ''));
                $href = trim((string) (($action['href'] ?? '') ?: ($action['target'] ?? '')));
            } else {
                $label = trim((string) $action);
            }
            if ($label === '') {
                continue;
            }
            $actions[] = [
                'label' => $label,
                'href' => $href !== '' ? $href : $this->headerActionHref($label),
            ];
        }

        return $actions;
    }

    /**
     * @param array<string, mixed> $navDefaults
     * @param list<array{label:string,href:string}> $headerActions
     * @return array{enabled:bool,label:string,href:string}
     */
    private function homeCta(array $navDefaults, array $headerActions): array
    {
        $hasHeaderCta = array_key_exists('headerCta', $navDefaults);
        $headerCta = is_array($navDefaults['headerCta'] ?? null) ? $navDefaults['headerCta'] : null;
        if ($headerCta !== null) {
            return [
                'enabled' => true,
                'label' => trim((string) ($headerCta['label'] ?? 'Contact us')) ?: 'Contact us',
                'href' => trim((string) ($headerCta['target'] ?? '/contact')) ?: '/contact',
            ];
        }
        if ($hasHeaderCta) {
            return ['enabled' => false, 'label' => '', 'href' => ''];
        }
        if ($headerActions !== []) {
            $primary = end($headerActions);

            return [
                'enabled' => true,
                'label' => (string) ($primary['label'] ?? 'Contact us'),
                'href' => (string) ($primary['href'] ?? '/contact'),
            ];
        }

        return ['enabled' => true, 'label' => 'Contact us', 'href' => '/contact'];
    }

    private function headerActionHref(string $label): string
    {
        $normalized = myronNormalizeSlug($label);
        $map = [
            'login' => '/login',
            'log-in' => '/login',
            'sign-in' => '/login',
            'start-free-trial' => '/signup',
            'start-trial' => '/signup',
            'sign-up' => '/signup',
            'signup' => '/signup',
            'book-a-demo' => '/contact',
            'request-demo' => '/contact',
            'contact-sales' => '/contact',
            'search' => '/search',
            'account' => '/account',
            'cart' => '/cart',
            'checkout' => '/checkout',
            'book-now' => '/reservation',
        ];

        return $map[$normalized] ?? '/' . $normalized;
    }

    /**
     * @param array<string, mixed> $siteType
     * @return list<string>
     */
    private function parentBundleIds(array $siteType): array
    {
        $parents = [];
        foreach (is_array($siteType['parentBundles'] ?? null) ? $siteType['parentBundles'] : [] as $parentBundle) {
            $normalized = trim((string) $parentBundle);
            if ($normalized !== '') {
                $parents[$normalized] = true;
            }
        }

        $legacyParent = $siteType['parentBundle'] ?? null;
        if (is_array($legacyParent)) {
            foreach ($legacyParent as $parentBundle) {
                $normalized = trim((string) $parentBundle);
                if ($normalized !== '') {
                    $parents[$normalized] = true;
                }
            }
        } else {
            $normalized = trim((string) $legacyParent);
            if ($normalized !== '') {
                $parents[$normalized] = true;
            }
        }

        return array_keys($parents);
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function upsertSeedRecord(string $bundleId, string $seedKey, string $seedType, array $payload): void
    {
        $existing = $this->pdo->prepare('SELECT seed_key FROM bundle_seed_records WHERE seed_key = :seed_key LIMIT 1');
        $existing->execute([':seed_key' => $seedKey]);
        $now = myronNow();
        $payloadJson = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);

        if ($existing->fetchColumn() !== false) {
            $statement = $this->pdo->prepare(
                'UPDATE bundle_seed_records
                 SET bundle_id = :bundle_id,
                     seed_type = :seed_type,
                     payload_json = :payload_json,
                     updated_at = :updated_at
                 WHERE seed_key = :seed_key'
            );
            $statement->execute([
                ':bundle_id' => $bundleId,
                ':seed_type' => $seedType,
                ':payload_json' => $payloadJson,
                ':updated_at' => $now,
                ':seed_key' => $seedKey,
            ]);
            return;
        }

        $statement = $this->pdo->prepare(
            'INSERT INTO bundle_seed_records (
                seed_key,
                bundle_id,
                seed_type,
                payload_json,
                created_at,
                updated_at
             ) VALUES (
                :seed_key,
                :bundle_id,
                :seed_type,
                :payload_json,
                :created_at,
                :updated_at
             )'
        );
        $statement->execute([
            ':seed_key' => $seedKey,
            ':bundle_id' => $bundleId,
            ':seed_type' => $seedType,
            ':payload_json' => $payloadJson,
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);
    }

    private function menuHasPage(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;
    }
}
