<?php

declare(strict_types=1);

/**
 * High-level search indexer for pages and other entities.
 *
 * Encapsulates the "read entity, extract searchable text, build payload,
 * call SearchIndexService" pipeline so callers (publish hooks, bundle disable,
 * backfill migration) need only invoke a single method.
 *
 * Failures are logged via error_log and swallowed at this layer — write-through
 * indexing must never break the underlying publish/delete operation. The
 * backfill migration is the explicit recovery path.
 */
final class MyronSearchIndexer
{
    private PDO $pdo;
    private MyronSearchIndexService $indexService;
    private MyronSearchTextExtractor $extractor;

    public function __construct(
        PDO $pdo,
        MyronSearchIndexService $indexService,
        MyronSearchTextExtractor $extractor
    ) {
        $this->pdo = $pdo;
        $this->indexService = $indexService;
        $this->extractor = $extractor;
    }

    public function indexPageById(string $pageId): void
    {
        $pageId = trim($pageId);
        if ($pageId === '') {
            return;
        }
        if (!$this->indexService->isAvailable()) {
            return;
        }
        try {
            $row = $this->fetchPageRow($pageId);
            if ($row === null) {
                $this->indexService->removeEntity('page', $pageId);
                return;
            }
            $payload = $this->buildPagePayload($row);
            if ($payload === null) {
                $this->indexService->removeEntity('page', $pageId);
                return;
            }
            $this->indexService->indexEntity('page', $pageId, $payload);
        } catch (Throwable $e) {
            error_log('[search-indexer] indexPageById failed for ' . $pageId . ': ' . $e->getMessage());
        }
    }

    public function removePageById(string $pageId): void
    {
        $pageId = trim($pageId);
        if ($pageId === '') {
            return;
        }
        if (!$this->indexService->isAvailable()) {
            return;
        }
        try {
            $this->indexService->removeEntity('page', $pageId);
        } catch (Throwable $e) {
            error_log('[search-indexer] removePageById failed for ' . $pageId . ': ' . $e->getMessage());
        }
    }

    /**
     * @param list<string> $pageIds
     */
    public function removePagesByIds(array $pageIds): void
    {
        if (!$this->indexService->isAvailable()) {
            return;
        }
        foreach ($pageIds as $pageId) {
            $this->removePageById((string) $pageId);
        }
    }

    /**
     * @return list<string>
     */
    public function listAllPublishedPageIds(): array
    {
        if (!$this->indexService->isAvailable()) {
            return [];
        }
        try {
            $statement = $this->pdo->query(
                "SELECT id FROM pages
                 WHERE publication_state = 'published'
                   AND current_published_version_id IS NOT NULL
                   AND (disabled_by_bundle_removal_at IS NULL OR disabled_by_bundle_removal_at = '')
                 ORDER BY id ASC"
            );
            if ($statement === false) {
                return [];
            }
            $ids = [];
            foreach ($statement->fetchAll(PDO::FETCH_COLUMN) ?: [] as $id) {
                $ids[] = (string) $id;
            }
            return $ids;
        } catch (Throwable $e) {
            error_log('[search-indexer] listAllPublishedPageIds failed: ' . $e->getMessage());
            return [];
        }
    }

    /**
     * @return array<string, mixed>|null
     */
    private function fetchPageRow(string $pageId): ?array
    {
        $statement = $this->pdo->prepare(
            "SELECT
                pages.id AS page_id,
                pages.slug AS page_slug,
                pages.path AS page_path,
                pages.title AS page_title,
                pages.publication_state AS publication_state,
                pages.disabled_by_bundle_removal_at AS disabled_by_bundle_removal_at,
                pages.source_bundle_id AS page_source_bundle_id,
                page_templates.source_bundle_id AS template_source_bundle_id,
                page_templates.template_key AS template_key,
                published.seo_title AS published_seo_title,
                published.meta_description AS published_meta_description,
                published.og_title AS published_og_title,
                published.og_description AS published_og_description,
                published.twitter_title AS published_twitter_title,
                published.twitter_description AS published_twitter_description,
                published.title AS published_title,
                published.content_json AS published_content_json,
                published.published_at AS published_at
             FROM pages
             INNER 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 pages.id = :id
             LIMIT 1"
        );
        $statement->execute([':id' => $pageId]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $row : null;
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>|null Returns null if the page should NOT be indexed (unpublished or bundle-disabled).
     */
    private function buildPagePayload(array $row): ?array
    {
        $state = (string) ($row['publication_state'] ?? 'draft');
        if ($state !== 'published') {
            return null;
        }
        $disabled = trim((string) ($row['disabled_by_bundle_removal_at'] ?? ''));
        if ($disabled !== '') {
            return null;
        }
        if (($row['published_content_json'] ?? null) === null) {
            return null;
        }

        $title = trim((string) ($row['published_title'] ?? $row['page_title'] ?? ''));
        $path = myronNormalizePath((string) ($row['page_path'] ?? '/'));
        $bodyText = $this->extractor->extractFromJson((string) ($row['published_content_json'] ?? '{}'));
        $metaText = self::buildMetaText($row, $title);
        $bundleId = trim((string) ($row['page_source_bundle_id'] ?? ''));
        if ($bundleId === '') {
            $bundleId = trim((string) ($row['template_source_bundle_id'] ?? ''));
        }

        return [
            'title' => $title !== '' ? $title : 'Untitled',
            'path' => $path,
            'url' => self::absoluteUrl($path),
            'body_text' => $bodyText,
            'meta_text' => $metaText,
            'bundle_id' => $bundleId !== '' ? $bundleId : null,
            'site_type' => self::siteTypeFromBundle($bundleId),
            'published_at' => (string) ($row['published_at'] ?? '') ?: null,
        ];
    }

    /**
     * @param array<string, mixed> $row
     */
    private static function buildMetaText(array $row, string $title): string
    {
        $parts = [$title];
        foreach ([
            'published_seo_title',
            'published_meta_description',
            'published_og_title',
            'published_og_description',
            'published_twitter_title',
            'published_twitter_description',
        ] as $key) {
            $value = trim((string) ($row[$key] ?? ''));
            if ($value !== '') {
                $parts[] = $value;
            }
        }
        return implode(' ', array_filter(array_unique($parts), static fn (string $p): bool => $p !== ''));
    }

    private static function absoluteUrl(string $path): string
    {
        try {
            $appUrl = rtrim(myronAppUrl(), '/');
        } catch (Throwable) {
            $appUrl = '';
        }
        try {
            $publicPath = myronBuildPublicPageUrl($path);
        } catch (Throwable) {
            $publicPath = $path;
        }
        return $appUrl . $publicPath;
    }

    private static function siteTypeFromBundle(string $bundleId): ?string
    {
        if ($bundleId === '' || !str_starts_with($bundleId, 'site-type.')) {
            return null;
        }
        $tail = substr($bundleId, strlen('site-type.'));
        $tail = trim(strtoupper($tail));
        return $tail !== '' ? $tail : null;
    }
}

function myronSearchIndexer(): MyronSearchIndexer
{
    static $instance = null;
    if ($instance instanceof MyronSearchIndexer) {
        return $instance;
    }
    $instance = new MyronSearchIndexer(
        myronDatabase(),
        myronSearchIndex(),
        myronSearchTextExtractor()
    );
    return $instance;
}
