<?php

declare(strict_types=1);

namespace Zab\ZabSite\DataProcessing;

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Pagination\ArrayPaginator;
use TYPO3\CMS\Core\Pagination\SlidingWindowPagination;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;

/**
 * Server-side pagination, category filtering and search for the FAQs list block.
 *
 * Reads the raw rows already loaded by DatabaseQueryProcessor (faqsItems),
 * applies category filtering and pagination, then replaces faqsItems with
 * only the current page's rows so that ContentBlockRecordsProcessor only
 * has to decorate those.
 *
 * GET params (excluded from cHash via ext_localconf.php):
 *   tx_zabfaqs[page]          – current page number (default: 1)
 *   tx_zabfaqs[categories][]  – array of category UIDs to filter by (OR logic)
 *   tx_zabfaqs[search]        – text search term (default: '')
 *
 * TypoScript usage (between DatabaseQueryProcessor and ContentBlockRecordsProcessor):
 *   300 = Zab\ZabSite\DataProcessing\FaqsListProcessor
 *   300.itemsPerPage = 24
 */
class FaqsListProcessor implements DataProcessorInterface
{
    /** DB column that stores the comma-separated category UIDs on FAQ items */
    private const CATEGORIES_COLUMN = 'zab_recordfaqs_categories';

    /** DB column prefix for FAQs fields */
    private const COLUMN_LABEL = 'zab_recordfaqs_label';
    private const COLUMN_TEXT  = 'zab_recordfaqs_text';

    /** DB column for the category parent (used to build the grouped tree) */
    private const CAT_LABEL_COLUMN  = 'zab_recordfaqscategory_faqs_category';
    private const CAT_PARENT_COLUMN = 'zab_recordfaqscategory_parent';

    /**
     * Grouping mode for the FAQs list headings:
     *   'cats' – group by parent category
     *   'subs' – group by subcategory (child)
     */
    private const SHOW_TITLE_LABEL = 'cats';

    /**
     * Returns true if the FAQs page has never been indexed or if any FAQ
     * was modified after the last indexed mtime.
     */
    private function needsReindex(int $pageUid, array $allItems): bool
    {
        if ($pageUid <= 0) {
            return true;
        }

        // Find the most recent tstamp among all FAQ items
        $latestTstamp = 0;
        foreach ($allItems as $row) {
            $raw = is_array($row['data'] ?? null) ? $row['data'] : (is_array($row) ? $row : []);
            $latestTstamp = max($latestTstamp, (int)($raw['tstamp'] ?? 0));
        }

        // Check what mtime is stored in index_phash for this page
        $qb = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('index_phash');
        $indexedMtime = $qb
            ->select('item_mtime')
            ->from('index_phash')
            ->where($qb->expr()->eq('data_page_id', $qb->createNamedParameter($pageUid, \Doctrine\DBAL\ParameterType::INTEGER)))
            ->orderBy('item_mtime', 'DESC')
            ->setMaxResults(1)
            ->executeQuery()
            ->fetchOne();

        // Never indexed, or FAQs changed after last index
        return $indexedMtime === false || $latestTstamp > (int)$indexedMtime;
    }

    /**
     * Maps a translated category UID to its canonical default-language UID.
     * Editors sometimes pick a language-specific category node (e.g. in the FAQ
     * "categories" field or the category "parent" tree) instead of the default-
     * language one, which would otherwise break grouping/filtering by UID.
     */
    private function buildCanonicalCategoryUidMap(): array
    {
        $qb = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('tx_zab_faqs_category');
        $rows = $qb
            ->select('uid', 'l10n_parent')
            ->from('tx_zab_faqs_category')
            ->where($qb->expr()->gt('l10n_parent', $qb->createNamedParameter(0, \Doctrine\DBAL\ParameterType::INTEGER)))
            ->executeQuery()
            ->fetchAllAssociative();

        $map = [];
        foreach ($rows as $row) {
            $map[(int)$row['uid']] = (int)$row['l10n_parent'];
        }
        return $map;
    }

    private function canonicalizeCategoryUid(int $uid, array $canonicalUidMap): int
    {
        return $canonicalUidMap[$uid] ?? $uid;
    }

    public function process(
        ContentObjectRenderer $cObj,
        array $contentObjectConfiguration,
        array $processorConfiguration,
        array $processedData
    ): array {
        $request = $cObj->getRequest();
        $pluginParams = $request->getQueryParams()['tx_zabfaqs'] ?? [];

        $currentPage       = max(1, (int)($pluginParams['page'] ?? 1));
        $currentCategories = array_values(array_filter(array_map('intval', (array)($pluginParams['categories'] ?? []))));
        $currentSearch     = trim((string)($pluginParams['search'] ?? ''));

        // Read items-per-page from the raw tt_content row (ContentBlockData is
        // already an object at this point, so use $cObj->data instead).
        $ceItemsPerPage = (int)($cObj->data['zab_faqslist_items_per_page'] ?? 0);
        $itemsPerPage = max(1, $ceItemsPerPage ?: (int)($processorConfiguration['itemsPerPage'] ?? 24));
        $slidingWindowSize = max(1, (int)($processorConfiguration['slidingWindowSize'] ?? 3));

        // Use items already fetched by DatabaseQueryProcessor
        $allItems = $processedData['faqsItems'] ?? [];

        // Resolves per-language category UIDs (see buildCanonicalCategoryUidMap doc)
        $canonicalCategoryUidMap = $this->buildCanonicalCategoryUidMap();

        // Filter by categories (OR logic) — DatabaseQueryProcessor wraps each row as ['data' => $rawDbRow]
        if (!empty($currentCategories)) {
            $allItems = array_values(array_filter(
                $allItems,
                function ($row) use ($currentCategories, $canonicalCategoryUidMap): bool {
                    $raw = is_array($row['data'] ?? null) ? $row['data'] : (is_array($row) ? $row : []);
                    $value = (string)($raw[self::CATEGORIES_COLUMN] ?? '');
                    $cats = array_filter(array_map('intval', explode(',', $value)));
                    $cats = array_map(fn($uid) => $this->canonicalizeCategoryUid($uid, $canonicalCategoryUidMap), $cats);
                    return !empty(array_intersect($currentCategories, $cats));
                }
            ));
        }

        // Filter by search term (searches label and text fields)
        if ($currentSearch !== '') {
            $needle = mb_strtolower($currentSearch);
            $allItems = array_values(array_filter(
                $allItems,
                static function ($row) use ($needle): bool {
                    $raw = is_array($row['data'] ?? null) ? $row['data'] : (is_array($row) ? $row : []);
                    $haystack = mb_strtolower(
                        ($raw[self::COLUMN_LABEL] ?? '') . ' ' . strip_tags((string)($raw[self::COLUMN_TEXT] ?? ''))
                    );
                    return str_contains($haystack, $needle);
                }
            ));
        }

        // Build grouped categories tree (parents as labels, children as selectable)
        $rawCategories  = $processedData['faqsCategories'] ?? [];
        $parents        = [];
        $children       = [];
        $subcatByUid    = []; // child uid  => label
        $subcatOrder    = []; // ordered child UIDs (preserves DB sorting)
        $childToParent  = []; // child uid  => parent uid
        $parentByUid    = []; // parent uid => label
        $parentOrder    = []; // ordered parent UIDs (preserves DB sorting)
        foreach ($rawCategories as $row) {
            $raw       = is_array($row['data'] ?? null) ? $row['data'] : (is_array($row) ? $row : []);
            $uid       = (int)($raw['uid'] ?? 0);
            $parentUid = $this->canonicalizeCategoryUid((int)($raw[self::CAT_PARENT_COLUMN] ?? 0), $canonicalCategoryUidMap);
            $label     = (string)($raw[self::CAT_LABEL_COLUMN] ?? '');
            if ($parentUid === 0) {
                $parents[$uid]    = ['uid' => $uid, 'label' => $label, 'children' => []];
                $parentByUid[$uid] = $label;
                $parentOrder[]     = $uid;
            } else {
                $children[]          = ['uid' => $uid, 'label' => $label, 'parent' => $parentUid, 'selected' => in_array($uid, $currentCategories, true)];
                $subcatByUid[$uid]   = $label;
                $subcatOrder[]       = $uid;
                $childToParent[$uid] = $parentUid;
            }
        }
        foreach ($children as $child) {
            if (isset($parents[$child['parent']])) {
                $parents[$child['parent']]['children'][] = $child;
            }
        }

        // Map: parent category uid => labels of currently-selected (checked) child categories
        $activeChildLabelsByParent = [];
        foreach ($currentCategories as $catUid) {
            if (isset($childToParent[$catUid], $subcatByUid[$catUid])) {
                $activeChildLabelsByParent[$childToParent[$catUid]][] = $subcatByUid[$catUid];
            }
        }

        // Build an expanded flat list grouped by either parent category ('cats') or
        // subcategory ('subs'), controlled by self::SHOW_TITLE_LABEL.
        // An empty value disables grouping entirely (flat list, no headings).
        // FAQs belonging to multiple groups are duplicated once per group.
        // When a filter is active (child UIDs selected), a FAQ is only placed under
        // the groups that contain its selected categories.
        $itemsByGroup       = []; // groupUid => [row, ...]
        $uncategorizedItems = [];
        $groupOrder         = [];
        $groupLabelByUid    = [];

        if (self::SHOW_TITLE_LABEL === '') {
            // ── No grouping: flat list, no headings ─────────────────────────────
            $allExpandedItems = array_map(
                static fn($row) => ['row' => $row, 'subcatUid' => 0, 'subcatLabel' => ''],
                $allItems
            );
        } elseif (self::SHOW_TITLE_LABEL === 'subs') {
            // ── Group by subcategory (child) ────────────────────────────────────
            $groupOrder      = $subcatOrder;
            $groupLabelByUid = $subcatByUid;

            foreach ($allItems as $row) {
                $raw      = is_array($row['data'] ?? null) ? $row['data'] : (is_array($row) ? $row : []);
                $value    = (string)($raw[self::CATEGORIES_COLUMN] ?? '');
                $rowCats  = array_filter(array_map('intval', explode(',', $value)));
                $rowCats  = array_map(fn($uid) => $this->canonicalizeCategoryUid($uid, $canonicalCategoryUidMap), $rowCats);
                $assigned = false;

                foreach ($groupOrder as $groupUid) {
                    if (!in_array($groupUid, $rowCats, true)) {
                        continue;
                    }
                    if (!empty($currentCategories) && !in_array($groupUid, $currentCategories, true)) {
                        continue;
                    }
                    $itemsByGroup[$groupUid][] = $row;
                    $assigned                  = true;
                }

                if (!$assigned) {
                    $uncategorizedItems[] = $row;
                }
            }
        } else {
            // ── Group by parent category ────────────────────────────────────────
            $groupOrder      = $parentOrder;
            $groupLabelByUid = $parentByUid;

            foreach ($allItems as $row) {
                $raw      = is_array($row['data'] ?? null) ? $row['data'] : (is_array($row) ? $row : []);
                $value    = (string)($raw[self::CATEGORIES_COLUMN] ?? '');
                $rowCats  = array_filter(array_map('intval', explode(',', $value)));
                $rowCats  = array_map(fn($uid) => $this->canonicalizeCategoryUid($uid, $canonicalCategoryUidMap), $rowCats);
                $assigned = false;

                foreach ($groupOrder as $groupUid) {
                    $qualifies = false;
                    foreach ($rowCats as $catUid) {
                        // $catUid may reference a category no longer present in $parentByUid/$childToParent (e.g. deleted)
                        /* $resolvedParent = $childToParent[$catUid] ?? ($parentByUid[$catUid] !== null ? $catUid : 0); */
                        $resolvedParent = $childToParent[$catUid] ?? (isset($parentByUid[$catUid]) ? $catUid : 0);
                        if ($resolvedParent !== $groupUid) {
                            continue;
                        }
                        if (!empty($currentCategories) && !in_array($catUid, $currentCategories, true)) {
                            continue;
                        }
                        $qualifies = true;
                        break;
                    }
                    if ($qualifies) {
                        $itemsByGroup[$groupUid][] = $row;
                        $assigned                  = true;
                    }
                }

                if (!$assigned) {
                    $uncategorizedItems[] = $row;
                }
            }
        }

        // Flatten into a single ordered list (groups first, uncategorized last).
        // Skipped when SHOW_TITLE_LABEL = '' because $allExpandedItems is already built above.
        if (self::SHOW_TITLE_LABEL !== '') {
            $allExpandedItems = [];
            foreach ($groupOrder as $groupUid) {
                if (empty($itemsByGroup[$groupUid])) {
                    continue;
                }
                foreach ($itemsByGroup[$groupUid] as $row) {
                    $allExpandedItems[] = [
                        'row'         => $row,
                        'subcatUid'   => $groupUid,
                        'subcatLabel' => $groupLabelByUid[$groupUid],
                        'activeLabel' => implode(', ', $activeChildLabelsByParent[$groupUid] ?? []),
                    ];
                }
            }
            foreach ($uncategorizedItems as $row) {
                $allExpandedItems[] = ['row' => $row, 'subcatUid' => 0, 'subcatLabel' => ''];
            }
        }

        // Paginate the expanded list
        $paginator  = new ArrayPaginator($allExpandedItems, $currentPage, $itemsPerPage);
        $pagination = new SlidingWindowPagination($paginator, $slidingWindowSize);

        // Build faqsItems (raw rows for ContentBlockRecordsProcessor) and
        // faqsItemSubcats (per-index subcategory metadata for the template).
        // isFirstInGroup is true for the first item of each subcategory on the current page.
        $faqsItems      = [];
        $faqsItemSubcats = [];
        $prevSubcatUid  = null;
        foreach (iterator_to_array($paginator->getPaginatedItems()) as $idx => $item) {
            $faqsItems[]      = $item['row'];
            $faqsItemSubcats[$idx] = [
                'subcatUid'      => $item['subcatUid'],
                'subcatLabel'    => $item['subcatLabel'],
                'activeLabel'    => $item['activeLabel'] ?? '',
                'isFirstInGroup' => $item['subcatUid'] !== $prevSubcatUid,
            ];
            $prevSubcatUid = $item['subcatUid'];
        }

        // Replace faqsItems with only the current page slice so that the
        // ContentBlockRecordsProcessor (900) only decorates those rows.
        // Precompute first/last page link visibility so the shared Pagination partial
        // avoids arithmetic in Fluid. Passed as a single 'edges' array argument.
        $rangeStart = $pagination->getDisplayRangeStart();
        $rangeEnd   = $pagination->getDisplayRangeEnd();
        $lastPage   = $paginator->getNumberOfPages();

        $processedData['faqsAllItems']           = $allItems;
        $processedData['faqsItems']              = $faqsItems;
        $processedData['faqsItemSubcats']        = $faqsItemSubcats;
        $processedData['faqsPaginator']          = $paginator;
        $processedData['faqsPagination']         = $pagination;
        $processedData['faqsPaginationEdges']    = [
            'showFirst'         => $rangeStart > 1,
            'showFirstEllipsis' => $rangeStart > 2,
            'showLast'          => $rangeEnd < $lastPage,
            'showLastEllipsis'  => $rangeEnd < ($lastPage - 1),
        ];
        $processedData['faqsCurrentPage']        = $paginator->getCurrentPageNumber();
        $processedData['faqsCurrentCategories']  = $currentCategories;
        $processedData['faqsCurrentSearch']      = $currentSearch;
        $processedData['faqsCategoriesGrouped']  = array_values(array_filter($parents, fn($g) => !empty($g['children'])));
        $processedData['faqsNeedsReindex']       = $this->needsReindex((int)($cObj->data['pid'] ?? 0), $processedData['faqsAllItems']);

        return $processedData;
    }
}
