<?php
/**
 * SphinxSearch Wrapper for DokuWiki
 * Advanced Renderer: Slices DokuWiki blocks at the line level and re-renders snippets
 * to preserve GeSHi highlighting, file tabs, and media elements.
 */

class SphinxSearch
{
    private SphinxClient $_sphinx;
    private array $_result = [];
    private int $_offset = 0;
    private int $_snippetSize = 512;
    private int $_resultsPerPage = 10;
    private int $_titlePriority = 1;
    private int $_bodyPriority = 1;
    private int $_namespacePriority = 1;
    private int $_pagenamePriority = 1;
    private string $_query = '';
    private string $_index = '';
    private string $_host = '';
    private int $_port = 9312;

    public function __construct(string $host, int $port, string $index)
    {
        $this->_host = $host;
        $this->_port = $port;
        $this->_index = $index;
        $this->_sphinx = new SphinxClient();
        $this->_sphinx->SetServer($this->_host, $this->_port);
        $this->_sphinx->SetMatchMode(SPH_MATCH_EXTENDED2);
    }

    public function setSnippetSize(int $size): void { if ($size > 0) $this->_snippetSize = $size; }
    public function setIndexTags(string $tags): void { /* Handled by DokuWiki Parser */ }

    public function search(int $start, int $resultsPerPage = 10): bool
    {
        $this->_resultsPerPage = $resultsPerPage;
        $this->_sphinx->SetFieldWeights([
            'namespace' => $this->_namespacePriority, 
            'pagename' => $this->_pagenamePriority, 
            'title' => $this->_titlePriority, 
            'body' => $this->_bodyPriority
        ]);
        $this->_sphinx->SetLimits($start, $resultsPerPage + 50);
        $res = $this->_sphinx->Query($this->_query, $this->_index);
        $this->_result = $res ?: [];
        return !empty($this->_result['matches']);
    }

    public function getPages(string $keywords): array|false
    {
        if (empty($this->_result['matches'])) return false;
        $ids = $this->getPagesIds();
        $results = [];
        $cleanK = $this->removeStars($keywords);
        
        foreach ($ids as $id => $data) {
            $this->_offset++;
            if (auth_quickaclcheck($data['page']) >= AUTH_READ) {
                $results[$id] = [
                    'page'              => $data['page'],
                    'bodyExcerpt'       => $this->_generateDokuWikiSnippet($data['page'], $cleanK),
                    'titleTextExcerpt'  => !empty($data['title_text']) ? $data['title_text'] : $data['page'],
                    'hid'               => $data['hid'] ?? '',
                ];
                if (count($results) >= $this->_resultsPerPage) break;
            }
        }
        return $results;
    }

    /**
     * Mirrors DokuWiki blocks by slicing instructions and re-rendering them.
     * This preserves blue tabs, yellow borders, and syntax colors.
     */
    private function _generateDokuWikiSnippet(string $pageId, string $query): string
    {
        $rawText = rawWiki($pageId);
        if (empty($rawText)) return '';

        $instructions = p_get_instructions($rawText);
        $words = preg_split('/\s+/', $query, -1, PREG_SPLIT_NO_EMPTY);
        $matchInstruction = null;

        // 1. Find the instruction block containing keywords
        foreach ($instructions as $instr) {
            $type = $instr[0];
            $content = '';
            if ($type === 'code' || $type === 'file') $content = $instr[1][0];
            elseif ($type === 'cdata') $content = $instr[1][0];
            
            foreach ($words as $word) {
                if (mb_stripos($content, $word) !== false) {
                    $matchInstruction = $instr;
                    break 2;
                }
            }
        }

        if (!$matchInstruction) {
            // Fallback for simple text matches
            $clean = strip_tags(p_render('xhtml', $instructions, $info));
            return '<p>... ' . hsc(mb_substr($clean, 0, 250)) . ' ...</p>';
        }

        $type = $matchInstruction[0];
        $snippetWiki = '';

        // 2. Line-based slicing for Code/File blocks
        if ($type === 'code' || $type === 'file') {
            $codeText = $matchInstruction[1][0];
            $lang = $matchInstruction[1][1];
            $file = ($type === 'file') ? $matchInstruction[1][2] : '';

            $lines = explode("\n", $codeText);
            $matchIdx = 0;
            foreach ($lines as $idx => $line) {
                foreach ($words as $word) {
                    if (mb_stripos($line, $word) !== false) { $matchIdx = $idx; break 2; }
                }
            }

            // Slice window: 3 lines before, 10 lines after match
            $start = max(0, $matchIdx - 3);
            $slice = array_slice($lines, $start, 15);
            $finalCode = ($start > 0 ? "... \n" : "") . implode("\n", $slice) . ($start + 15 < count($lines) ? "\n ..." : "");

            if ($type === 'file') $snippetWiki = "<file $lang $file>\n$finalCode\n</file>";
            else $snippetWiki = "<code $lang>\n$finalCode\n</code>";
        } 
        // 3. Media Preservation
        elseif (in_array($type, ['internalmedia', 'externalmedia'])) {
            // If it's a video/image, we render the whole instruction
            $snippetWiki = p_render('xhtml', [$matchInstruction], $info);
            return $snippetWiki;
        }
        else {
            $snippetWiki = $matchInstruction[1][0];
        }

        // 4. Re-render the slice using DokuWiki engine
        $rendered = p_render('xhtml', p_get_instructions($snippetWiki), $info);

        // Safe Highlight
        foreach ($words as $word) {
            if (mb_strlen($word) < 2) continue;
            $q = preg_quote(hsc($word), '/');
            $rendered = preg_replace("/(?![^<]*>)$q/iu", '<strong>$0</strong>', $rendered);
        }

        return $rendered;
    }

    public function getExcerpt(array $data, string $query): array {
        $words = preg_split('/\s+/', $this->removeStars($query), -1, PREG_SPLIT_NO_EMPTY);
        $res = [];
        foreach ($data as $text) {
            $out = hsc($text);
            foreach ($words as $word) {
                if (mb_strlen($word) < 2) continue;
                $q = preg_quote(hsc($word), '/');
                $out = preg_replace("/($q)/iu", '<strong>$1</strong>', $out);
            }
            $res[] = $out;
        }
        return $res;
    }

    public function getPagesIds(): array { return (new PageMapper())->getByCrc(array_keys($this->_result['matches'] ?? [])); }
    public function removeStars(string $query): string { return trim(str_replace('*', '', $query)); }
    public function starQuery(string $query): string {
        $words = preg_split('/\s+/', $this->removeStars($query), -1, PREG_SPLIT_NO_EMPTY);
        $starred = [];
        foreach ($words as $w) $starred[] = (str_starts_with($w, '-') || mb_strlen($w) < 3 || str_contains($w, '"')) ? $w : "*$w*";
        return implode(" ", $starred);
    }
    public function getOffset(): int { return $this->_offset; }
    public function getError(): string { return $this->_sphinx->GetLastError(); }
    public function getTotalFound(): int { return (int)($this->_result['total_found'] ?? 0); }
    public function setNamespacePriority(int $p): void { $this->_namespacePriority = $p; }
    public function setPagenamePriority(int $p): void { $this->_pagenamePriority = $p; }
    public function setTitlePriority(int $p): void { $this->_titlePriority = $p; }
    public function setBodyPriority(int $p): void { $this->_bodyPriority = $p; }
    public function setSearchAllQuery(string $k, string $c): void {
        $esc = $this->_sphinx->EscapeString($k);
        $this->_query = "(@(body,title) $esc) | (@(namespace,pagename) " . $this->starQuery($esc) . ")";
    }
    public function setSearchAllQueryWithCategoryFilter(string $k, string $c): void {
        $esc = $this->_sphinx->EscapeString($k);
        $cat = $this->_sphinx->EscapeString($c);
        $f = str_starts_with($c, "-") ? '-"' . substr($cat, 1) . '"' : '"' . $cat . '"';
        $this->_query = "(@(namespace,pagename) $f) & ((@(body,title) $esc) | (@(namespace,pagename) " . $this->starQuery($esc) . "))";
    }
    public function setSearchOnlyPagename(): void { $this->_query = "(@pagename {$this->_query})"; }
}
