<?php

declare(strict_types=1);

trait MyronContentPagesTrait
{
    /**
     * @return list<array<string, mixed>>
     */
    public function listPages(): array
    {
        $statement = $this->pdo->query(
            'SELECT
                pages.id,
                pages.template_id,
                pages.parent_page_id,
                pages.slug,
                pages.path,
                pages.title,
                pages.source_template_id,
                pages.source_bundle_id,
                pages.indexable_override,
                pages.include_in_nav,
                pages.publication_state,
                pages.current_draft_version_id,
                pages.current_published_version_id,
                pages.created_at,
                pages.updated_at,
                page_templates.name AS template_name,
                page_templates.template_key,
                page_templates.source_bundle_id AS template_source_bundle_id,
                parent_pages.title AS parent_title,
                draft.version_number AS draft_version_number,
                published.version_number AS published_version_number,
                published.robots_override AS robots_override,
                published.published_at AS published_at
             FROM pages
             INNER JOIN page_templates ON page_templates.id = pages.template_id
             LEFT JOIN pages AS parent_pages ON parent_pages.id = pages.parent_page_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 CASE WHEN pages.path = \'/\' THEN 0 ELSE 1 END, pages.path ASC'
        );

        $pages = $statement->fetchAll();
        $rolesByPage = $this->pageRolesByPage();
        foreach ($pages as &$page) {
            $page['preview_url'] = myronBuildPublicPageUrl((string) $page['path']);
            $page['roles'] = $rolesByPage[(string) $page['id']] ?? [];
            $page['page_roles'] = $page['roles'];
            $page['is_home'] = in_array('home', $page['roles'], true) || (string) $page['path'] === '/';
            $page['includeInNav'] = ((int) ($page['include_in_nav'] ?? 1)) === 1;
        }
        unset($page);

        return $pages;
    }

    /**
     * @return array<string, int>
     */
    public function countPagesByPublicationState(): array
    {
        $statement = $this->pdo->query(
            'SELECT publication_state, COUNT(*) AS page_count
             FROM pages
             GROUP BY publication_state'
        );

        $counts = ['draft' => 0, 'published' => 0];
        foreach ($statement->fetchAll(PDO::FETCH_ASSOC) ?: [] as $row) {
            $state = trim((string) ($row['publication_state'] ?? 'draft'));
            if ($state === '') {
                $state = 'draft';
            }
            $counts[$state] = (int) ($row['page_count'] ?? 0);
        }

        ksort($counts);
        return $counts;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listPagesForManager(): array
    {
        $pages = $this->listPages();
        $childrenByParent = [];
        $rootPages = [];

        foreach ($pages as $page) {
            $parentId = trim((string) ($page['parent_page_id'] ?? ''));
            if ($parentId === '') {
                $rootPages[] = $page;
                continue;
            }

            $childrenByParent[$parentId][] = $page;
        }

        $ordered = [];
        $walk = function (array $page, int $depth) use (&$walk, &$ordered, $childrenByParent): void {
            $page['depth'] = $depth;
            $page['manager_version_number'] = $page['draft_version_number'] ?? $page['published_version_number'] ?? null;
            $ordered[] = $page;

            foreach ($childrenByParent[(string) $page['id']] ?? [] as $child) {
                $walk($child, $depth + 1);
            }
        };

        foreach ($rootPages as $page) {
            $walk($page, 0);
        }

        return $ordered;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listPageOptions(?string $excludePageId = null): array
    {
        $statement = $this->pdo->query(
            'SELECT
                pages.id,
                pages.title,
                pages.path,
                pages.source_template_id,
                pages.source_bundle_id,
                pages.indexable_override,
                page_templates.template_key,
                page_templates.source_template_id AS template_source_template_id,
                page_templates.source_bundle_id AS template_source_bundle_id
             FROM pages
             INNER JOIN page_templates ON page_templates.id = pages.template_id
             ORDER BY CASE WHEN path = \'/\' THEN 0 ELSE 1 END, path ASC'
        );

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

            if ($excludePageId !== null && (string) $row['id'] === $excludePageId) {
                continue;
            }

            $roleSourceId = $this->pageOptionRoleSourceId($row);
            $options[] = [
                'id' => (string) $row['id'],
                'title' => (string) $row['title'],
                'path' => (string) $row['path'],
                'source_template_id' => (string) ($row['source_template_id'] ?? ''),
                'source_bundle_id' => (string) ($row['source_bundle_id'] ?? ''),
                'template_source_template_id' => (string) ($row['template_source_template_id'] ?? ''),
                'template_source_bundle_id' => (string) ($row['template_source_bundle_id'] ?? ''),
                'template_class' => MyronTemplateDefinitionRepository::templateClassForTemplateId($roleSourceId),
                'source_site_type' => $this->pageOptionSourceSiteType($roleSourceId),
            ];
        }

        return $options;
    }

    public function getEditablePage(string $pageId): ?array
    {
        $statement = $this->pdo->prepare(
            'SELECT
                pages.*,
                page_templates.name AS template_name,
                page_templates.template_key,
                page_templates.source_bundle_id AS template_source_bundle_id,
                page_templates.current_version_id AS template_current_version_id,
                parent_pages.title AS parent_title,
                page_template_versions.schema_json,
                page_template_versions.id AS active_template_version_id,
                draft.version_number AS draft_version_number,
                draft.template_version_id AS draft_template_version_id,
                draft.title AS draft_title,
                draft.seo_title AS draft_seo_title,
                draft.meta_description AS draft_meta_description,
                draft.canonical_override AS draft_canonical_override,
                draft.robots_override AS draft_robots_override,
                draft.og_title AS draft_og_title,
                draft.og_description AS draft_og_description,
                draft.og_image AS draft_og_image,
                draft.twitter_title AS draft_twitter_title,
                draft.twitter_description AS draft_twitter_description,
                draft.twitter_image AS draft_twitter_image,
                draft.jsonld_mode AS draft_jsonld_mode,
                draft.jsonld_type AS draft_jsonld_type,
                draft.jsonld_custom AS draft_jsonld_custom,
                draft.content_json AS draft_content_json,
                draft.created_at AS draft_created_at,
                published.version_number AS published_version_number,
                published.template_version_id AS published_template_version_id,
                published.title AS published_title,
                published.seo_title AS published_seo_title,
                published.meta_description AS published_meta_description,
                published.canonical_override AS published_canonical_override,
                published.robots_override AS published_robots_override,
                published.og_title AS published_og_title,
                published.og_description AS published_og_description,
                published.og_image AS published_og_image,
                published.twitter_title AS published_twitter_title,
                published.twitter_description AS published_twitter_description,
                published.twitter_image AS published_twitter_image,
                published.jsonld_mode AS published_jsonld_mode,
                published.jsonld_type AS published_jsonld_type,
                published.jsonld_custom AS published_jsonld_custom,
                published.content_json AS published_content_json,
                published.published_at
             FROM pages
             INNER JOIN page_templates ON page_templates.id = pages.template_id
             LEFT JOIN pages AS parent_pages ON parent_pages.id = pages.parent_page_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
             LEFT JOIN page_template_versions
                ON page_template_versions.id = COALESCE(
                    draft.template_version_id,
                    published.template_version_id,
                    page_templates.current_version_id
                )
             WHERE pages.id = :page_id
             LIMIT 1'
        );
        $statement->execute([':page_id' => $pageId]);
        $row = $statement->fetch();

        if (!is_array($row)) {
            return null;
        }

        $content = $row['draft_content_json'] ?: $row['published_content_json'] ?: '{}';
        $row['content'] = json_decode((string) $content, true, 512, JSON_THROW_ON_ERROR);
        $row['template_schema'] = json_decode((string) $row['schema_json'], true, 512, JSON_THROW_ON_ERROR);
        $row['active_version_id'] = (string) (($row['current_draft_version_id'] ?? '') ?: ($row['current_published_version_id'] ?? ''));
        $row['active_version_number'] = (int) (($row['draft_version_number'] ?? 0) ?: ($row['published_version_number'] ?? 0));
        $row['template_version_id'] = (string) (($row['active_template_version_id'] ?? '') ?: ($row['template_current_version_id'] ?? ''));
        $row['seo_title'] = (string) (($row['draft_seo_title'] ?? '') ?: ($row['published_seo_title'] ?? '') ?: ($row['title'] ?? ''));
        $row['meta_description'] = (string) (($row['draft_meta_description'] ?? '') ?: ($row['published_meta_description'] ?? ''));
        foreach ($this->seoOverrideFieldNames() as $field) {
            $row[$field] = (string) (($row['draft_' . $field] ?? '') ?: ($row['published_' . $field] ?? ''));
        }
        $row['jsonld_mode'] = (string) ($row['jsonld_mode'] ?: 'auto');
        $row['jsonld_type'] = (string) ($row['jsonld_type'] ?: 'Article');
        $row['preview_url'] = myronBuildPublicPageUrl((string) $row['path']);
        $row['roles'] = $this->pageRolesByPage()[(string) $row['id']] ?? [];
        $row['page_roles'] = $row['roles'];
        $row['is_home'] = in_array('home', $row['roles'], true) || (string) $row['path'] === '/';
        $row['includeInNav'] = ((int) ($row['include_in_nav'] ?? 1)) === 1;
        $row['menu_assignments'] = $this->listMenuAssignmentsForPage((string) $row['id']);

        return $row;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function findPageBySourceTemplateId(string $sourceTemplateId): ?array
    {
        $statement = $this->pdo->prepare(
            'SELECT *
             FROM pages
             WHERE source_template_id = :source_template_id
             LIMIT 1'
        );
        $statement->execute([':source_template_id' => trim($sourceTemplateId)]);
        $row = $statement->fetch();

        return is_array($row) ? $row : null;
    }

    /**
     * @param array<string, mixed> $row
     */
    private function pageOptionRoleSourceId(array $row): string
    {
        foreach (['source_template_id', 'template_source_template_id', 'template_key', 'id'] as $field) {
            $candidate = trim((string) ($row[$field] ?? ''));
            if ($candidate !== '') {
                return $candidate;
            }
        }

        return '';
    }

    private function pageOptionSourceSiteType(string $templateId): string
    {
        $normalized = trim($templateId);
        if ($normalized === '') {
            return '';
        }

        $parts = explode('.', $normalized);
        if (($parts[0] ?? '') === 'template') {
            array_shift($parts);
        }

        if ($parts !== [] && preg_match('/^v[0-9]+$/', (string) end($parts)) === 1) {
            array_pop($parts);
        }

        return $parts !== [] ? (string) end($parts) : '';
    }

    public function createPage(
        string $templateId,
        string $pageName,
        string $slugInput,
        ?string $parentPageId,
        string $seoTitle,
        string $metaDescription,
        array $content,
        bool $publishNow,
        ?string $publishedAt = null,
        ?string $sourceTemplateId = null,
        ?string $sourceBundleId = null,
        array $seoOverrides = [],
        ?bool $includeInNav = null
    ): string {
        $normalizedName = trim($pageName);
        if ($normalizedName === '') {
            throw new InvalidArgumentException('Page name is required.');
        }

        $normalizedSlug = $this->resolveSlug($normalizedName, $slugInput);
        $parentId = $this->normalizeParentPageId($parentPageId);
        $generatedPath = $this->buildPath($normalizedSlug, $parentId, null);

        $template = $this->requireTemplate($templateId);
        $includeInNavValue = $includeInNav ?? $this->defaultIncludeInNavForTemplate($template, $sourceTemplateId);
        if ($publishNow) {
            $this->qualityReportService()->assertPublishableInput([
                'pageId' => '',
                'templateId' => $templateId,
                'title' => $normalizedName,
                'slug' => $slugInput,
                'content' => $content,
            ]);
        }

        $now = myronNow();
        $pageId = myronGenerateId('page');
        $versionId = myronGenerateId('pagever');

        $this->pdo->beginTransaction();

        try {
            $insertPage = $this->pdo->prepare(
                'INSERT INTO pages (
                    id, template_id, parent_page_id, slug, path, title, publication_state,
                    current_draft_version_id, current_published_version_id,
                    source_template_id, source_bundle_id, indexable_override, include_in_nav, created_at, updated_at
                 ) VALUES (
                    :id, :template_id, :parent_page_id, :slug, :path, :title, :publication_state,
                    :current_draft_version_id, :current_published_version_id,
                    :source_template_id, :source_bundle_id, :indexable_override, :include_in_nav, :created_at, :updated_at
                 )'
            );

            $insertPage->execute([
                ':id' => $pageId,
                ':template_id' => $templateId,
                ':parent_page_id' => $parentId,
                ':slug' => $normalizedSlug,
                ':path' => $generatedPath,
                ':title' => $normalizedName,
                ':publication_state' => $publishNow ? 'published' : 'draft',
                ':current_draft_version_id' => $versionId,
                ':current_published_version_id' => $publishNow ? $versionId : null,
                ':source_template_id' => trim((string) $sourceTemplateId) !== '' ? trim((string) $sourceTemplateId) : null,
                ':source_bundle_id' => trim((string) $sourceBundleId) !== '' ? trim((string) $sourceBundleId) : null,
                ':indexable_override' => $this->normalizeIndexableOverride($seoOverrides['indexable_override'] ?? null),
                ':include_in_nav' => $includeInNavValue ? 1 : 0,
                ':created_at' => $now,
                ':updated_at' => $now,
            ]);

            $this->insertPageVersion(
                $versionId,
                $pageId,
                (string) $template['current_version_id'],
                1,
                $normalizedName,
                $seoTitle,
                $metaDescription,
                $content,
                $publishNow ? ($publishedAt ?? $now) : null,
                $seoOverrides
            );

            $this->pdo->commit();
        } catch (Throwable $exception) {
            $this->pdo->rollBack();
            throw $exception;
        }

        myronInvalidatePublicPageCache('page created');

        return $pageId;
    }

    public function setPageIncludeInNav(string $pageId, bool $includeInNav): void
    {
        $pageId = trim($pageId);
        if ($pageId === '') {
            throw new InvalidArgumentException('pageId is required.');
        }

        $this->requirePage($pageId);
        $statement = $this->pdo->prepare(
            'UPDATE pages
             SET include_in_nav = :include_in_nav,
                 updated_at = :updated_at
             WHERE id = :page_id'
        );
        $statement->execute([
            ':include_in_nav' => $includeInNav ? 1 : 0,
            ':updated_at' => myronNow(),
            ':page_id' => $pageId,
        ]);
        myronInvalidatePublicPageCache('page navigation visibility changed');
    }

    public function savePage(
        string $pageId,
        string $templateId,
        string $pageName,
        string $slugInput,
        ?string $parentPageId,
        string $seoTitle,
        string $metaDescription,
        array $content,
        bool $publishNow,
        ?string $publishedAt = null,
        array $seoOverrides = []
    ): void {
        $page = $this->requirePage($pageId);
        $effectiveTemplateId = trim((string) ($page['source_template_id'] ?? '')) !== ''
            ? (string) $page['template_id']
            : $templateId;
        $template = $this->requireTemplate($effectiveTemplateId);
        $templateVersionId = $this->templateVersionIdForPageSave($page, $effectiveTemplateId, $template);
        $normalizedName = trim($pageName);
        if ($normalizedName === '') {
            throw new InvalidArgumentException('Page name is required.');
        }

        if ($publishNow) {
            $this->qualityReportService()->assertPublishableInput([
                'pageId' => $pageId,
                'templateId' => $effectiveTemplateId,
                'title' => $normalizedName,
                'slug' => $slugInput,
                'content' => $content,
            ]);
        }

        $normalizedSlug = $this->resolveSlug($normalizedName, $slugInput);
        $parentId = $this->normalizeParentPageId($parentPageId);
        $this->assertParentIsValid($parentId, $pageId);
        $generatedPath = $this->buildPath($normalizedSlug, $parentId, $pageId);

        $now = myronNow();
        $versionNumber = $this->nextVersionNumber($pageId);
        $versionId = myronGenerateId('pagever');

        $this->pdo->beginTransaction();

        try {
            $updatePage = $this->pdo->prepare(
                'UPDATE pages
                 SET template_id = :template_id,
                     parent_page_id = :parent_page_id,
                     slug = :slug,
                     path = :path,
                     title = :title,
                     publication_state = :publication_state,
                     current_draft_version_id = :current_draft_version_id,
                     current_published_version_id = :current_published_version_id,
                     indexable_override = :indexable_override,
                     updated_at = :updated_at
                 WHERE id = :id'
            );

            $currentPublishedVersionId = $publishNow ? $versionId : $page['current_published_version_id'];

            $updatePage->execute([
                ':template_id' => $effectiveTemplateId,
                ':parent_page_id' => $parentId,
                ':slug' => $normalizedSlug,
                ':path' => $generatedPath,
                ':title' => $normalizedName,
                ':publication_state' => $publishNow ? 'published' : ($page['current_published_version_id'] ? 'published' : 'draft'),
                ':current_draft_version_id' => $versionId,
                ':current_published_version_id' => $currentPublishedVersionId,
                ':indexable_override' => $this->normalizeIndexableOverride($seoOverrides['indexable_override'] ?? ($page['indexable_override'] ?? null)),
                ':updated_at' => $now,
                ':id' => $pageId,
            ]);

            $this->insertPageVersion(
                $versionId,
                $pageId,
                $templateVersionId,
                $versionNumber,
                $normalizedName,
                $seoTitle,
                $metaDescription,
                $content,
                $publishNow ? ($publishedAt ?? $now) : null,
                $seoOverrides
            );

            $this->pdo->commit();
        } catch (Throwable $exception) {
            $this->pdo->rollBack();
            throw $exception;
        }
        myronInvalidatePublicPageCache('page saved');
    }

    public function deletionPolicy(string $pageId): array
    {
        $page = $this->requirePage($pageId);
        $pageCount = (int) $this->pdo->query('SELECT COUNT(*) FROM pages')->fetchColumn();

        if ($pageCount <= 1) {
            return [
                'allowed' => false,
                'reason' => 'The last remaining page cannot be deleted in this slice.',
                'page' => $page,
            ];
        }

        $roles = $this->pageRolesByPage()[$pageId] ?? [];
        if ($roles !== []) {
            return [
                'allowed' => false,
                'reason' => 'This page holds the ' . implode(', ', $roles) . ' page role. Move or clear the role before deletion.',
                'page' => $page,
            ];
        }

        if ((string) $page['path'] === '/') {
            return [
                'allowed' => false,
                'reason' => 'The home page cannot be deleted in this slice.',
                'page' => $page,
            ];
        }

        $childCountStatement = $this->pdo->prepare('SELECT COUNT(*) FROM pages WHERE parent_page_id = :page_id');
        $childCountStatement->execute([':page_id' => $pageId]);
        if ((int) $childCountStatement->fetchColumn() > 0) {
            return [
                'allowed' => false,
                'reason' => 'Pages with child pages must be restructured before deletion.',
                'page' => $page,
            ];
        }

        return [
            'allowed' => true,
            'reason' => null,
            'page' => $page,
        ];
    }

    /** @return array<string, list<string>> */
    private function pageRolesByPage(): array
    {
        try {
            $rows = $this->pdo->query('SELECT role_key, page_id FROM site_page_roles ORDER BY role_key ASC')?->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable) {
            return [];
        }
        $roles = [];
        foreach ($rows as $row) {
            $pageId = (string) ($row['page_id'] ?? '');
            $role = (string) ($row['role_key'] ?? '');
            if ($pageId !== '' && $role !== '') {
                $roles[$pageId][] = $role;
            }
        }
        return $roles;
    }

    public function deletePage(string $pageId): array
    {
        $policy = $this->deletionPolicy($pageId);
        if (($policy['allowed'] ?? false) !== true) {
            throw new InvalidArgumentException((string) ($policy['reason'] ?? 'This page cannot be deleted.'));
        }

        /** @var array<string, mixed> $page */
        $page = $policy['page'];

        $this->pdo->beginTransaction();

        try {
            $deleteMenuItems = $this->pdo->prepare('DELETE FROM menu_items WHERE page_id = :page_id');
            $deleteMenuItems->execute([':page_id' => $pageId]);

            $deleteVersions = $this->pdo->prepare('DELETE FROM page_versions WHERE page_id = :page_id');
            $deleteVersions->execute([':page_id' => $pageId]);

            $deletePage = $this->pdo->prepare('DELETE FROM pages WHERE id = :page_id');
            $deletePage->execute([':page_id' => $pageId]);

            $this->pdo->commit();
        } catch (Throwable $exception) {
            $this->pdo->rollBack();
            throw $exception;
        }

        myronInvalidatePublicPageCache('page deleted');

        return [
            'id' => $pageId,
            'title' => (string) $page['title'],
            'path' => (string) $page['path'],
        ];
    }

    private function qualityReportService(): MyronQualityReportService
    {
        return new MyronQualityReportService($this->pdo);
    }

    /**
     * @param array<string, mixed> $page
     * @param array<string, mixed> $template
     */
    private function templateVersionIdForPageSave(array $page, string $effectiveTemplateId, array $template): string
    {
        $existingTemplateId = trim((string) ($page['template_id'] ?? ''));
        if ($existingTemplateId !== '' && $existingTemplateId === $effectiveTemplateId) {
            foreach (['current_draft_version_id', 'current_published_version_id'] as $field) {
                $versionId = trim((string) ($page[$field] ?? ''));
                if ($versionId === '') {
                    continue;
                }

                $statement = $this->pdo->prepare(
                    'SELECT template_version_id
                     FROM page_versions
                     WHERE id = :version_id
                     LIMIT 1'
                );
                $statement->execute([':version_id' => $versionId]);
                $templateVersionId = trim((string) $statement->fetchColumn());
                if ($templateVersionId !== '') {
                    return $templateVersionId;
                }
            }
        }

        $currentTemplateVersionId = trim((string) ($template['current_version_id'] ?? ''));
        if ($currentTemplateVersionId === '') {
            throw new InvalidArgumentException('Selected template does not have a current version.');
        }

        return $currentTemplateVersionId;
    }

    /**
     * @param array<string, mixed> $template
     */
    private function defaultIncludeInNavForTemplate(array $template, ?string $sourceTemplateId): bool
    {
        foreach ([
            (string) $sourceTemplateId,
            (string) ($template['source_template_id'] ?? ''),
            (string) ($template['template_key'] ?? ''),
            (string) ($template['id'] ?? ''),
        ] as $candidate) {
            $candidate = trim($candidate);
            if ($candidate === '') {
                continue;
            }

            if (MyronTemplateDefinitionRepository::templateClassForTemplateId($candidate) === 'post-single') {
                return false;
            }
        }

        return true;
    }

    /**
     * @return list<string>
     */
    private function seoOverrideFieldNames(): array
    {
        return [
            'canonical_override',
            'robots_override',
            'og_title',
            'og_description',
            'og_image',
            'twitter_title',
            'twitter_description',
            'twitter_image',
            'jsonld_mode',
            'jsonld_type',
            'jsonld_custom',
        ];
    }

    private function normalizeIndexableOverride(mixed $value): ?int
    {
        if ($value === null || $value === '') {
            return null;
        }

        if (is_bool($value)) {
            return $value ? 1 : 0;
        }

        $normalized = strtolower(trim((string) $value));
        if (in_array($normalized, ['default', 'null', 'inherit', ''], true)) {
            return null;
        }
        if (in_array($normalized, ['1', 'true', 'include', 'yes', 'on'], true)) {
            return 1;
        }
        if (in_array($normalized, ['0', 'false', 'exclude', 'no', 'off'], true)) {
            return 0;
        }

        return null;
    }
}
