<?php

declare(strict_types=1);

trait MyronContentNavigationTrait
{
    /**
     * @return list<array<string, mixed>>
     */
    public function listMenuPlacements(): array
    {
        $statement = $this->pdo->query(
            'SELECT
                menu_placements.placement_key,
                menu_placements.label,
                menu_placements.menu_id,
                menus.name AS menu_name,
                menus.menu_key
             FROM menu_placements
             LEFT JOIN menus ON menus.id = menu_placements.menu_id
             ORDER BY
                CASE menu_placements.placement_key
                    WHEN \'primary\' THEN 0
                    WHEN \'footer\' THEN 1
                    ELSE 2
                END,
                menu_placements.label ASC'
        );

        $placements = [];
        foreach ($statement->fetchAll() as $row) {
            if (!is_array($row)) {
                continue;
            }

            $placements[] = [
                'placement_key' => (string) $row['placement_key'],
                'label' => (string) $row['label'],
                'menu_id' => trim((string) ($row['menu_id'] ?? '')),
                'menu_name' => trim((string) ($row['menu_name'] ?? '')),
                'menu_key' => trim((string) ($row['menu_key'] ?? '')),
            ];
        }

        return $placements;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listMenusWithItems(): array
    {
        $menus = [];
        $placementsByMenu = [];
        foreach ($this->listMenuPlacements() as $placement) {
            $menuId = trim((string) ($placement['menu_id'] ?? ''));
            if ($menuId === '') {
                continue;
            }

            $placementsByMenu[$menuId][] = [
                'key' => (string) $placement['placement_key'],
                'label' => (string) $placement['label'],
            ];
        }

        $menuStatement = $this->pdo->query(
            'SELECT
                id,
                menu_key,
                name,
                created_at,
                updated_at
             FROM menus
             ORDER BY name ASC'
        );
        $menuRows = $menuStatement ? $menuStatement->fetchAll() : [];

        foreach ($menuRows as $row) {
            if (!is_array($row)) {
                continue;
            }

            $menuId = (string) $row['id'];
            $placements = $placementsByMenu[$menuId] ?? [];
            $menus[$menuId] = [
                'id' => $menuId,
                'menu_key' => (string) $row['menu_key'],
                'name' => (string) $row['name'],
                'placement' => (string) ($placements[0]['key'] ?? ''),
                'placement_label' => $this->describeMenuPlacements($placements),
                'placements' => $placements,
                'items' => [],
            ];
        }

        $itemsByMenu = [];
        $itemStatement = $this->pdo->query(
            'SELECT
                menu_items.id AS item_id,
                menu_items.menu_id,
                menu_items.label AS item_label,
                menu_items.sort_order,
                menu_items.parent_menu_item_id,
                pages.id AS page_id,
                pages.title AS page_title,
                pages.path AS page_path,
                pages.parent_page_id AS page_parent_id,
                pages.publication_state,
                page_templates.name AS template_name,
                draft.version_number AS draft_version_number,
                published.version_number AS published_version_number
             FROM menu_items
             INNER JOIN pages ON pages.id = menu_items.page_id
             LEFT JOIN page_templates ON page_templates.id = pages.template_id
             LEFT JOIN page_versions AS draft ON draft.id = pages.current_draft_version_id
             LEFT JOIN page_versions AS published ON published.id = pages.current_published_version_id
             ORDER BY menu_items.menu_id ASC, menu_items.parent_menu_item_id ASC, menu_items.sort_order ASC, menu_items.created_at ASC'
        );

        foreach (($itemStatement ? $itemStatement->fetchAll() : []) as $row) {
            if (!is_array($row)) {
                continue;
            }

            $menuId = (string) ($row['menu_id'] ?? '');
            if ($menuId === '' || !isset($menus[$menuId])) {
                continue;
            }

            $pagePath = (string) ($row['page_path'] ?? '/');
            $versionNumber = $row['draft_version_number'] ?? $row['published_version_number'] ?? null;

            $item = [
                'id' => (string) $row['item_id'],
                'label' => (string) $row['item_label'],
                'page_id' => (string) $row['page_id'],
                'page_title' => (string) $row['page_title'],
                'page_path' => $pagePath,
                'page_parent_id' => (string) ($row['page_parent_id'] ?? ''),
                'publication_state' => (string) ($row['publication_state'] ?? 'draft'),
                'template_name' => (string) ($row['template_name'] ?? ''),
                'manager_version_number' => $versionNumber !== null ? (int) $versionNumber : null,
                'preview_url' => myronBuildPublicPageUrl($pagePath),
                'parent_menu_item_id' => (string) ($row['parent_menu_item_id'] ?? ''),
                'sort_order' => (int) ($row['sort_order'] ?? 0),
                'children' => [],
            ];
            $menus[$menuId]['items'][] = $item;
            $itemsByMenu[$menuId][] = $item;
        }

        foreach ($menus as $menuId => &$menu) {
            $menu['items'] = $this->buildMenuTree($this->applyDefaultMenuParentsFromPageHierarchy($itemsByMenu[$menuId] ?? []));
        }

        return array_values($menus);
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listPublishedMenuItemsByPlacement(string $placement): array
    {
        $placementKey = trim($placement);
        $itemsByPlacement = $this->listPublishedMenuItemsByPlacements([$placementKey]);
        return $itemsByPlacement[$placementKey] ?? [];
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listPublishedMenuItemTreeByPlacement(string $placement, bool $includeDefaultPageDescendants = false): array
    {
        $placementKey = trim($placement);
        $itemsByPlacement = $this->listPublishedMenuItemTreesByPlacements(
            [$placementKey],
            $includeDefaultPageDescendants ? [$placementKey] : []
        );
        return $itemsByPlacement[$placementKey] ?? [];
    }

    /**
     * @param list<string> $placements
     * @return array<string, list<array<string, mixed>>>
     */
    public function listPublishedMenuItemsByPlacements(array $placements): array
    {
        $treesByPlacement = $this->listPublishedMenuItemTreesByPlacements($placements);
        $result = [];

        foreach ($treesByPlacement as $placement => $tree) {
            $result[$placement] = $this->flattenMenuTree($tree);
        }

        return $result;
    }

    /**
     * @param list<string> $treePlacements
     * @param list<string> $flatPlacements
     * @return array{trees: array<string, list<array<string, mixed>>>, flat: array<string, list<array<string, mixed>>>}
     */
    public function listPublishedMenuNavigationByPlacements(array $treePlacements, array $flatPlacements): array
    {
        $allPlacements = [];
        foreach (array_merge($treePlacements, $flatPlacements) as $placement) {
            $key = trim((string) $placement);
            if ($key !== '') {
                $allPlacements[$key] = true;
            }
        }

        $treesByPlacement = $this->listPublishedMenuItemTreesByPlacements(
            array_keys($allPlacements),
            $treePlacements
        );
        $result = [
            'trees' => [],
            'flat' => [],
        ];

        foreach ($treePlacements as $placement) {
            $key = trim((string) $placement);
            if ($key !== '') {
                $result['trees'][$key] = $treesByPlacement[$key] ?? [];
            }
        }

        foreach ($flatPlacements as $placement) {
            $key = trim((string) $placement);
            if ($key !== '') {
                $result['flat'][$key] = $this->flattenMenuTree($treesByPlacement[$key] ?? []);
            }
        }

        return $result;
    }

    /**
     * @param list<string> $placements
     * @param list<string> $includeDefaultPageDescendantsForPlacements
     * @return array<string, list<array<string, mixed>>>
     */
    public function listPublishedMenuItemTreesByPlacements(array $placements, array $includeDefaultPageDescendantsForPlacements = []): array
    {
        $normalizedPlacements = [];
        foreach ($placements as $placement) {
            $key = trim((string) $placement);
            if ($key !== '') {
                $normalizedPlacements[$key] = true;
            }
        }

        $includeDefaultPageDescendants = [];
        foreach ($includeDefaultPageDescendantsForPlacements as $placement) {
            $key = trim((string) $placement);
            if ($key !== '') {
                $includeDefaultPageDescendants[$key] = true;
            }
        }

        $result = [];
        foreach (array_keys($normalizedPlacements) as $placement) {
            $result[$placement] = [];
        }
        if ($result === []) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($result), '?'));
        $placementStatement = $this->pdo->prepare(
            'SELECT placement_key, menu_id
             FROM menu_placements
             WHERE placement_key IN (' . $placeholders . ')'
        );
        $placementStatement->execute(array_keys($result));

        $placementByMenuId = [];
        foreach ($placementStatement->fetchAll() as $row) {
            if (!is_array($row)) {
                continue;
            }

            $placementKey = trim((string) ($row['placement_key'] ?? ''));
            $menuId = trim((string) ($row['menu_id'] ?? ''));
            if ($placementKey === '' || $menuId === '' || !isset($result[$placementKey])) {
                continue;
            }

            $placementByMenuId[$menuId] = $placementKey;
        }

        if ($placementByMenuId === []) {
            return $result;
        }

        $disabledFilter = $this->tableHasColumn('pages', 'disabled_by_bundle_removal_at')
            ? ' AND pages.disabled_by_bundle_removal_at IS NULL'
            : '';
        $menuPlaceholders = implode(',', array_fill(0, count($placementByMenuId), '?'));
        $itemStatement = $this->pdo->prepare(
            'SELECT
                menu_items.id AS item_id,
                menu_items.menu_id,
                menu_items.label AS item_label,
                menu_items.sort_order,
                menu_items.parent_menu_item_id,
                pages.id AS page_id,
                pages.title AS page_title,
                pages.path AS page_path,
                pages.parent_page_id AS page_parent_id,
                pages.publication_state,
                page_templates.name AS template_name,
                published.version_number AS published_version_number
             FROM menu_items
             INNER JOIN pages ON pages.id = menu_items.page_id
             LEFT JOIN page_templates ON page_templates.id = pages.template_id
             LEFT JOIN page_versions AS published ON published.id = pages.current_published_version_id
             WHERE menu_items.menu_id IN (' . $menuPlaceholders . ')
               AND pages.publication_state = \'published\'
               AND pages.current_published_version_id IS NOT NULL'
            . $disabledFilter .
            ' ORDER BY menu_items.menu_id ASC, menu_items.parent_menu_item_id ASC, menu_items.sort_order ASC, menu_items.created_at ASC'
        );
        $itemStatement->execute(array_keys($placementByMenuId));

        $itemsByMenu = [];
        foreach ($itemStatement->fetchAll() as $row) {
            if (!is_array($row)) {
                continue;
            }

            $menuId = trim((string) ($row['menu_id'] ?? ''));
            if ($menuId === '' || !isset($placementByMenuId[$menuId])) {
                continue;
            }

            $pagePath = (string) ($row['page_path'] ?? '/');
            $label = trim((string) ($row['item_label'] ?? ''));
            $itemsByMenu[$menuId][] = [
                'id' => (string) $row['item_id'],
                'label' => $label !== '' ? $label : (string) ($row['page_title'] ?? ''),
                'page_id' => (string) $row['page_id'],
                'page_title' => (string) $row['page_title'],
                'page_parent_id' => (string) ($row['page_parent_id'] ?? ''),
                'page_path' => $pagePath,
                'path' => $pagePath,
                'url' => myronBuildPublicPageUrl($pagePath),
                'publication_state' => (string) ($row['publication_state'] ?? 'draft'),
                'template_name' => (string) ($row['template_name'] ?? ''),
                'manager_version_number' => isset($row['published_version_number']) ? (int) $row['published_version_number'] : null,
                'preview_url' => myronBuildPublicPageUrl($pagePath),
                'parent_menu_item_id' => (string) ($row['parent_menu_item_id'] ?? ''),
                'sort_order' => (int) ($row['sort_order'] ?? 0),
                'children' => [],
            ];
        }

        foreach ($placementByMenuId as $menuId => $placementKey) {
            $items = $this->applyDefaultMenuParentsFromPageHierarchy($itemsByMenu[$menuId] ?? []);
            if (isset($includeDefaultPageDescendants[$placementKey])) {
                $items = $this->appendPublishedPageDescendantsToMenuItems($items);
            }
            $result[$placementKey] = $this->buildMenuTree($items);
        }

        return $result;
    }

    public function createMenu(string $name): array
    {
        $normalizedName = trim($name);
        if ($normalizedName === '') {
            throw new InvalidArgumentException('Menu name is required.');
        }

        $menuKey = $this->nextMenuKey($normalizedName);
        $now = myronNow();
        $menuId = myronGenerateId('menu');

        $statement = $this->pdo->prepare(
            'INSERT INTO menus (id, menu_key, name, placement, created_at, updated_at)
             VALUES (:id, :menu_key, :name, :placement, :created_at, :updated_at)'
        );
        $statement->execute([
            ':id' => $menuId,
            ':menu_key' => $menuKey,
            ':name' => $normalizedName,
            ':placement' => '',
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);

        $menu = $this->findMenuById($menuId);
        if ($menu === null) {
            throw new RuntimeException('Failed to create the requested menu.');
        }

        myronInvalidatePublicPageCache('menu created');

        return $menu;
    }

    public function updatePlacementAssignments(array $assignments): void
    {
        $placements = $this->listMenuPlacements();
        $placementKeys = array_fill_keys(array_map(static fn (array $placement): string => (string) $placement['placement_key'], $placements), true);

        $update = $this->pdo->prepare(
            'UPDATE menu_placements
             SET menu_id = :menu_id,
                 updated_at = :updated_at
             WHERE placement_key = :placement_key'
        );

        foreach ($assignments as $placementKey => $menuId) {
            $normalizedPlacementKey = trim((string) $placementKey);
            if ($normalizedPlacementKey === '' || !isset($placementKeys[$normalizedPlacementKey])) {
                continue;
            }

            $normalizedMenuId = trim((string) $menuId);
            if ($normalizedMenuId !== '') {
                $this->requireMenu($normalizedMenuId);
            }

            $update->execute([
                ':menu_id' => $normalizedMenuId !== '' ? $normalizedMenuId : null,
                ':updated_at' => myronNow(),
                ':placement_key' => $normalizedPlacementKey,
            ]);
        }
        myronInvalidatePublicPageCache('menu placements changed');
    }

    public function addPageToMenu(string $menuId, string $pageId, string $label = ''): void
    {
        $menu = $this->requireMenu($menuId);
        $page = $this->requirePage($pageId);

        $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,
        ]);
        if (is_array($statement->fetch())) {
            throw new InvalidArgumentException('That page is already present in the selected menu.');
        }

        $parentMenuItemId = $this->defaultMenuParentItemIdForPage($menuId, $page);
        if ($parentMenuItemId === null) {
            $orderStatement = $this->pdo->prepare(
                'SELECT COALESCE(MAX(sort_order), 0)
                 FROM menu_items
                 WHERE menu_id = :menu_id
                   AND parent_menu_item_id IS NULL'
            );
            $orderStatement->execute([':menu_id' => $menuId]);
        } else {
            $orderStatement = $this->pdo->prepare(
                'SELECT COALESCE(MAX(sort_order), 0)
                 FROM menu_items
                 WHERE menu_id = :menu_id
                   AND parent_menu_item_id = :parent_menu_item_id'
            );
            $orderStatement->execute([
                ':menu_id' => $menuId,
                ':parent_menu_item_id' => $parentMenuItemId,
            ]);
        }
        $sortOrder = ((int) $orderStatement->fetchColumn()) + 1;
        $now = myronNow();

        $insert = $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
             )'
        );
        $insert->execute([
            ':id' => myronGenerateId('menuitem'),
            ':menu_id' => (string) $menu['id'],
            ':page_id' => (string) $page['id'],
            ':label' => trim($label) !== '' ? trim($label) : (string) $page['title'],
            ':sort_order' => $sortOrder,
            ':parent_menu_item_id' => $parentMenuItemId,
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);
        myronInvalidatePublicPageCache('menu item added');
    }

    public function listMenuAssignmentsForPage(string $pageId): array
    {
        $menus = $this->listMenusWithItems();
        $assignments = [];

        foreach ($menus as $menu) {
            $assignments[(string) $menu['id']] = [
                'menu_id' => (string) $menu['id'],
                'menu_name' => (string) $menu['name'],
                'placement' => (string) ($menu['placement_label'] ?? 'Unassigned'),
                'enabled' => false,
                'menu_item_id' => null,
                'label' => '',
            ];

            foreach ($this->flattenMenuTree($menu['items'] ?? []) as $item) {
                if ((string) ($item['page_id'] ?? '') !== $pageId) {
                    continue;
                }

                $assignments[(string) $menu['id']]['enabled'] = true;
                $assignments[(string) $menu['id']]['menu_item_id'] = (string) ($item['id'] ?? '');
                $assignments[(string) $menu['id']]['label'] = trim((string) ($item['label'] ?? ''));
                break;
            }
        }

        return array_values($assignments);
    }

    public function syncPageMenuAssignments(string $pageId, array $assignments): void
    {
        foreach ($assignments as $menuId => $assignment) {
            $enabled = ($assignment['enabled'] ?? false) === true;
            $label = trim((string) ($assignment['label'] ?? ''));

            $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,
            ]);
            $existing = $statement->fetch();

            if ($enabled && !is_array($existing)) {
                $this->addPageToMenu($menuId, $pageId, $label);
                continue;
            }

            if (!$enabled && is_array($existing)) {
                $this->removeMenuItem((string) $existing['id']);
                continue;
            }

            if ($enabled && is_array($existing)) {
                $update = $this->pdo->prepare(
                    'UPDATE menu_items
                     SET label = :label,
                         updated_at = :updated_at
                     WHERE id = :id'
                );
                $update->execute([
                    ':label' => $label,
                    ':updated_at' => myronNow(),
                    ':id' => (string) $existing['id'],
                ]);
            }
        }
        myronInvalidatePublicPageCache('page menu assignments changed');
    }

    public function removeMenuItem(string $menuItemId): void
    {
        $item = $this->requireMenuItem($menuItemId);
        $statement = $this->pdo->prepare('DELETE FROM menu_items WHERE id = :menu_item_id');
        $statement->execute([':menu_item_id' => $menuItemId]);
        $siblings = $this->listSiblingMenuItemIds(
            (string) $item['menu_id'],
            $this->normalizeMenuItemId((string) ($item['parent_menu_item_id'] ?? '')),
            $menuItemId
        );
        $this->writeSiblingOrder($siblings, $this->normalizeMenuItemId((string) ($item['parent_menu_item_id'] ?? '')));
        myronInvalidatePublicPageCache('menu item removed');
    }

    public function moveMenuItem(string $menuId, string $menuItemId, ?string $parentMenuItemId, ?string $afterMenuItemId): void
    {
        $this->requireMenu($menuId);
        $item = $this->requireMenuItem($menuItemId);

        if ((string) $item['menu_id'] !== $menuId) {
            throw new InvalidArgumentException('Menu item does not belong to the selected menu.');
        }

        $normalizedParentId = $this->normalizeMenuItemId($parentMenuItemId);
        $normalizedAfterId = $this->normalizeMenuItemId($afterMenuItemId);

        if ($normalizedParentId !== null) {
            $parentItem = $this->requireMenuItem($normalizedParentId);
            if ((string) $parentItem['menu_id'] !== $menuId) {
                throw new InvalidArgumentException('Menu nesting must stay within the same menu.');
            }
            $this->assertMenuParentIsValid($menuItemId, $normalizedParentId);
        }

        $oldParentId = $this->normalizeMenuItemId((string) ($item['parent_menu_item_id'] ?? ''));
        $this->pdo->beginTransaction();

        try {
            $newSiblings = $this->listSiblingMenuItemIds($menuId, $normalizedParentId, $menuItemId);
            if ($normalizedAfterId !== null && !in_array($normalizedAfterId, $newSiblings, true)) {
                throw new InvalidArgumentException('Menu drop target is invalid.');
            }

            $insertIndex = 0;
            if ($normalizedAfterId !== null) {
                $insertIndex = array_search($normalizedAfterId, $newSiblings, true);
                $insertIndex = ($insertIndex === false ? count($newSiblings) : ((int) $insertIndex + 1));
            }

            array_splice($newSiblings, $insertIndex, 0, [$menuItemId]);
            $this->writeSiblingOrder($newSiblings, $normalizedParentId);

            if ($oldParentId !== $normalizedParentId) {
                $oldSiblings = $this->listSiblingMenuItemIds($menuId, $oldParentId, $menuItemId);
                $this->writeSiblingOrder($oldSiblings, $oldParentId);
            }

            $this->pdo->commit();
        } catch (Throwable $exception) {
            $this->pdo->rollBack();
            throw $exception;
        }
        myronInvalidatePublicPageCache('menu item moved');
    }
}
