<?php
/**
 * Batch builder for the Bricks design-system index.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Index;

use LolaCoreForBricks\Support\BricksMetaKeys;

/**
 * Builds the persisted edge index in bounded batches.
 */
class IndexBuilder
{
    public const DEFAULT_BATCH_SIZE = 10;

    public const DEFER_ELEMENT_THRESHOLD = 500;

    public function __construct(
        private readonly ?IndexState $state = null,
        private readonly ?IndexRepository $repository = null,
        private readonly ?ElementWalker $walker = null
    ) {
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function buildBatch(array $params = []): array
    {
        $startedAt = microtime(true);
        $restart   = (bool) ($params['restart'] ?? false);
        $batchSize = max(1, (int) ($params['batch_size'] ?? self::DEFAULT_BATCH_SIZE));

        $stateStore = $this->state ?? new IndexState();
        $repository = $this->repository ?? new IndexRepository();
        $walker     = $this->walker ?? new ElementWalker();

        if ($restart || $stateStore->schemaMismatch() || !$this->tableReady()) {
            $this->restartBuild($stateStore, $repository);
        }

        $state = $stateStore->get();

        if (!($state['build_started_at'] ?? null)) {
            $state['build_started_at'] = current_time('mysql', true);
        }

        if (!($state['globals_indexed'] ?? false)) {
            $this->indexGlobalScopes($repository, $walker);
            $state['globals_indexed'] = true;
            $stateStore->save($state);
        }

        if (($state['post_ids'] ?? []) === []) {
            $state['post_ids']     = $this->discoverPostIds();
            $state['total_pages']  = count($state['post_ids']);
            $state['post_cursor']  = 0;
            $state['indexed_pages'] = 0;
            $state['finished']     = $state['total_pages'] === 0;
            $stateStore->save($state);
        }

        $postIds = $state['post_ids'];
        $cursor  = (int) ($state['post_cursor'] ?? 0);
        $slice   = array_slice($postIds, $cursor, $batchSize);

        foreach ($slice as $postId) {
            $this->indexPost((int) $postId, $repository, $walker);
        }

        $cursor += count($slice);
        $state['post_cursor']   = $cursor;
        $state['indexed_pages'] = min($cursor, (int) ($state['total_pages'] ?? 0));
        $state['finished']      = $cursor >= count($postIds);

        if ($state['finished']) {
            $state['built_at']     = current_time('mysql', true);
            $state['class_stamp']  = $this->currentClassStamp();
            $state['max_modified'] = $this->currentMaxModified();
        }

        $stateStore->save($state);

        $done = (int) ($state['globals_indexed'] ?? false) + (int) ($state['indexed_pages'] ?? 0);
        $total = 1 + (int) ($state['total_pages'] ?? 0);

        return [
            'success'    => true,
            'done'       => min($done, $total),
            'total'      => $total,
            'finished'   => (bool) $state['finished'],
            'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000),
            'coverage'   => (new IndexReader($stateStore, $repository))->coverage(),
        ];
    }

    public function indexPost(int $postId, ?IndexRepository $repository = null, ?ElementWalker $walker = null): void
    {
        if ($postId <= 0) {
            return;
        }

        $repository = $repository ?? $this->repository ?? new IndexRepository();
        $walker     = $walker ?? $this->walker ?? new ElementWalker();

        $repository->deleteForPost($postId);
        $edges = [];

        foreach (BricksMetaKeys::pageMetaKeyToArea() as $metaKey => $area) {
            $elements = get_post_meta($postId, $metaKey, true);
            if (!is_array($elements) || $elements === []) {
                continue;
            }

            $edges = array_merge($edges, $walker->walk($postId, $area, $elements));
        }

        $pageSettings = get_post_meta($postId, BricksMetaKeys::PAGE_SETTINGS, true);
        if (is_array($pageSettings) && $pageSettings !== []) {
            $edges = array_merge(
                $edges,
                $walker->walkSettings(
                    $postId,
                    BricksMetaKeys::AREA_PAGE_SETTINGS,
                    $pageSettings,
                    (string) $postId,
                    'page_settings'
                )
            );
        }

        $repository->insertEdges($edges);
    }

    public function indexGlobalScopes(?IndexRepository $repository = null, ?ElementWalker $walker = null): void
    {
        $repository = $repository ?? $this->repository ?? new IndexRepository();
        $walker     = $walker ?? $this->walker ?? new ElementWalker();

        $repository->deleteGlobalScope();
        $edges = [];

        $globalClasses = get_option(BricksMetaKeys::OPTION_GLOBAL_CLASSES, []);
        if (is_array($globalClasses)) {
            foreach ($globalClasses as $class) {
                if (!is_array($class)) {
                    continue;
                }

                $classId = isset($class['id']) ? (string) $class['id'] : '';
                $settings = is_array($class['settings'] ?? null) ? $class['settings'] : [];
                if ($classId === '' && $settings === []) {
                    continue;
                }

                $edges = array_merge(
                    $edges,
                    $walker->walkSettings(
                        0,
                        BricksMetaKeys::AREA_CLASS_SETTINGS,
                        $settings,
                        $classId !== '' ? $classId : null,
                        isset($class['name']) ? (string) $class['name'] : null
                    )
                );
            }
        }

        $themeStyles = get_option(BricksMetaKeys::OPTION_THEME_STYLES, []);
        if (is_array($themeStyles)) {
            foreach ($themeStyles as $style) {
                if (!is_array($style)) {
                    continue;
                }

                $styleId = isset($style['id']) ? (string) $style['id'] : '';
                $settings = is_array($style['settings'] ?? null) ? $style['settings'] : [];
                $edges = array_merge(
                    $edges,
                    $walker->walkSettings(
                        0,
                        BricksMetaKeys::AREA_THEME_STYLE,
                        $settings,
                        $styleId !== '' ? $styleId : null,
                        isset($style['name']) ? (string) $style['name'] : null
                    )
                );
            }
        }

        $globalElements = get_option(BricksMetaKeys::OPTION_GLOBAL_ELEMENTS, []);
        if (is_array($globalElements)) {
            foreach ($globalElements as $elementGroup) {
                if (!is_array($elementGroup)) {
                    continue;
                }

                $elements = $elementGroup['elements'] ?? $elementGroup;
                if (is_array($elements)) {
                    $edges = array_merge(
                        $edges,
                        $walker->walk(0, BricksMetaKeys::AREA_GLOBAL_ELEMENT, $elements)
                    );
                }
            }
        }

        $components = get_option(BricksMetaKeys::OPTION_COMPONENTS, []);
        if (is_array($components)) {
            foreach ($components as $component) {
                if (!is_array($component)) {
                    continue;
                }

                $componentId = isset($component['id']) ? (string) $component['id'] : '';
                $elements    = is_array($component['elements'] ?? null) ? $component['elements'] : [];
                if ($componentId !== '') {
                    $edges[] = [
                        'ref_type'     => BricksMetaKeys::REF_COMPONENT,
                        'ref_id'       => $componentId,
                        'post_id'      => 0,
                        'area'         => BricksMetaKeys::AREA_GLOBAL_ELEMENT,
                        'element_id'   => $componentId,
                        'element_name' => isset($component['name']) ? (string) $component['name'] : null,
                    ];
                }

                if ($elements !== []) {
                    $edges = array_merge(
                        $edges,
                        $walker->walk(0, BricksMetaKeys::AREA_GLOBAL_ELEMENT, $elements)
                    );
                }
            }
        }

        $repository->insertEdges($edges);
    }

    public function shouldDeferPostIndexing(int $postId): bool
    {
        $count = 0;
        foreach (BricksMetaKeys::pageMetaKeys() as $metaKey) {
            $elements = get_post_meta($postId, $metaKey, true);
            if (is_array($elements)) {
                $count += count($elements);
            }
        }

        return $count > self::DEFER_ELEMENT_THRESHOLD;
    }

    private function restartBuild(IndexState $stateStore, IndexRepository $repository): void
    {
        $repository->truncate();
        $stateStore->reset();
    }

    private function tableReady(): bool
    {
        return IndexSchema::tableExists();
    }

    /**
     * @return int[]
     */
    public function discoverPostIds(): array
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT DISTINCT post_id
                 FROM {$wpdb->postmeta}
                 WHERE meta_key IN (%s, %s, %s)
                 AND meta_value <> ''
                 ORDER BY post_id ASC",
                BricksMetaKeys::PAGE_CONTENT,
                BricksMetaKeys::PAGE_HEADER,
                BricksMetaKeys::PAGE_FOOTER
            )
        );

        if (!is_array($rows)) {
            return [];
        }

        return array_values(array_map('intval', $rows));
    }

    private function currentClassStamp(): ?string
    {
        $stamp = get_option(BricksMetaKeys::OPTION_CLASS_TIMESTAMP, null);

        if ($stamp === null || $stamp === false || $stamp === '') {
            return null;
        }

        return is_scalar($stamp) ? (string) $stamp : wp_json_encode($stamp);
    }

    private function currentMaxModified(): ?string
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $max = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT MAX(p.post_modified_gmt)
                 FROM {$wpdb->posts} p
                 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
                 WHERE pm.meta_key IN (%s, %s, %s)
                 AND pm.meta_value <> ''",
                BricksMetaKeys::PAGE_CONTENT,
                BricksMetaKeys::PAGE_HEADER,
                BricksMetaKeys::PAGE_FOOTER
            )
        );

        return is_string($max) && $max !== '' ? $max : null;
    }
}
