import { create, load, search } from '@orama/orama';
import fs from 'fs/promises';
import path from 'path';
import { paginateResults, DEFAULT_PAGE_SIZE } from '../../utils/search-helper.js';
import { endpointSchema } from './schema.js';
import { RESOURCE_ALIASES } from '../../synonyms.js';
import { getSynonyms } from '../../synonyms.js';
import { ACTION_KEYWORDS, ACTION_PRIORITY, matchesKeyword } from '../../intents.js';
import { ScoreCalculator } from '../../utils/ScoreCalculator.js';
export class OramaSearchService {
    indexPath;
    db;
    extractor;
    initPromise;
    knownResources = ['project', 'block-diagram', 'worksheet', 'failure-mode', 'action', 'block-node', 'division', 'user', 'document', 'file', 'api-key', 'fault-tree', 'oauth'];
    debugMode;
    scoreCalculator;
    lookup = new Map();
    constructor(indexPath, debugMode = false) {
        this.indexPath = indexPath;
        this.debugMode = debugMode;
        this.scoreCalculator = new ScoreCalculator();
        this.initPromise = this.initialize();
    }
    async initialize() {
        try {
            const data = await fs.readFile(this.indexPath, 'utf-8');
            // Initialize an empty DB with schema first and enable stemming
            this.db = await create({
                schema: endpointSchema,
                components: {
                    tokenizer: {
                        stemming: true,
                        language: 'english'
                    }
                }
            });
            // Load data into the DB instance
            await load(this.db, JSON.parse(data));
            // Load endpoint-lookup.json
            const lookupPath = path.join(path.dirname(this.indexPath), 'endpoint-lookup.json');
            try {
                const lookupData = await fs.readFile(lookupPath, 'utf-8');
                const lookupObj = JSON.parse(lookupData);
                for (const [key, value] of Object.entries(lookupObj)) {
                    this.lookup.set(key, value);
                }
                console.error(`✅ OramaSearchService: Loaded ${this.lookup.size} endpoint lookups.`);
            }
            catch (lookupError) {
                console.error('⚠️ OramaSearchService: endpoint-lookup.json not found, falling back to index-only mode.');
            }
            // Initialize embedding model (fastembed / onnxruntime-node)
            try {
                const { FlagEmbedding, EmbeddingModel } = await import('fastembed');
                const cacheDir = path.join(process.env.HOME || '~', '.cache', 'fastembed');
                this.extractor = await FlagEmbedding.init({ model: EmbeddingModel.BGESmallENV15, cacheDir });
                console.error('✅ OramaSearchService: Index loaded and model ready (hybrid mode).');
            }
            catch (embeddingError) {
                console.error('⚠️ OramaSearchService: Embedding model unavailable, using BM25 text search only.', embeddingError);
                this.extractor = null;
            }
        }
        catch (error) {
            console.error('❌ OramaSearchService initialization failed:', error);
        }
    }
    async search(query, filterMethod, filterVersion, page = 1, explicitPath) {
        await this.initPromise; // Ensure service is ready
        if (!this.db) {
            return { results: [], meta: { total: 0, page, totalPages: 0 }, warning: 'Search service not initialized' };
        }
        if (!query || query.trim() === '') {
            return { results: [], meta: { total: 0, page, totalPages: 0 } };
        }
        // [R19] Filter out queries consisting only of HTTP method names
        const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options']);
        const rawTokens = query.toLowerCase().split(/\s+/).filter(t => t.length > 0);
        if (rawTokens.length > 0 && rawTokens.every(t => HTTP_METHODS.has(t))) {
            return { results: [], meta: { total: 0, page, totalPages: 0 } };
        }
        try {
            // 1. Explicit Path Search (Exact/Contains) - High Priority
            const isPathQuery = query.startsWith('/') || query.startsWith('api/');
            if (explicitPath || isPathQuery) {
                const targetPath = explicitPath || query;
                console.error(`[Orama] Path Mode: "${targetPath}"`);
                const result = await search(this.db, {
                    term: targetPath,
                    properties: ['path'],
                    where: filterMethod ? { method: filterMethod } : undefined,
                    limit: 20
                });
                const hits = result.hits.filter(hit => hit.document.path.includes(targetPath));
                // Sort by path length to prioritize exact match (shortest) over children
                hits.sort((a, b) => a.document.path.length - b.document.path.length);
                const results = hits.map((hit) => {
                    const id = hit.document.id;
                    const sourceData = this.lookup.get(id) || {};
                    const result = {
                        path: hit.document.path,
                        method: hit.document.method,
                        summary: sourceData.summary || '',
                        description: hit.document.description,
                        score: 1.0, // Forced high score for explicit match
                    };
                    // Include additional fields in debug mode
                    if (this.debugMode) {
                        Object.assign(result, sourceData);
                    }
                    return result;
                });
                return {
                    results,
                    meta: { total: results.length, page: 1, totalPages: 1 }
                };
            }
            // 2. Vector/Hybrid Search
            // Truncate query for safety
            const safeQuery = query.substring(0, 500);
            // [R15] Query Expansion & Intent Detection - Moved UP
            const intent = this.detectIntent(query);
            if (intent.action)
                console.error(`[Orama] Detected Action: ${intent.action}`);
            if (intent.resource)
                console.error(`[Orama] Detected Resource: ${intent.resource}`);
            // Query-time synonym expansion (targeted, not exhaustive)
            let expandedQuery = safeQuery;
            // Resource name expansion from intent detection
            if (intent.resource) {
                const resourceNameStr = intent.resource.replace(/-/g, ' ');
                if (!expandedQuery.toLowerCase().includes(resourceNameStr)) {
                    expandedQuery += ` ${resourceNameStr}`;
                }
            }
            // Limited token-level synonym expansion:
            // Add only direct synonyms (max 3 per token) to preserve BM25 signal
            const tokens = safeQuery.toLowerCase().split(/\s+/).filter(t => t.length >= 3);
            const extraSynonyms = [];
            for (const token of tokens) {
                const syns = getSynonyms(token).filter(s => s !== token && !tokens.includes(s));
                extraSynonyms.push(...syns.slice(0, 3));
            }
            if (extraSynonyms.length > 0) {
                expandedQuery += ' ' + extraSynonyms.join(' ');
            }
            console.error(`[Orama] Expanded Query: "${expandedQuery}"`);
            // 3. Build Where Clauses
            const where = {};
            if (filterMethod)
                where.method = filterMethod;
            if (filterVersion)
                where.version = filterVersion;
            // 4. Search — hybrid (BM25 + vector) when embedding available, BM25-only otherwise
            let searchResult;
            if (this.extractor) {
                const embedding = await this.extractor.queryEmbed(safeQuery);
                searchResult = await search(this.db, {
                    term: expandedQuery,
                    tolerance: 2,
                    properties: ['description', 'path'],
                    boost: {
                        description: 5.0,
                        path: 10.0
                    },
                    vector: {
                        value: embedding,
                        property: 'embedding'
                    },
                    mode: 'hybrid',
                    similarity: 0,
                    where,
                    limit: 1000,
                    offset: 0
                });
            }
            else {
                // BM25-only fallback (no embedding model available)
                searchResult = await search(this.db, {
                    term: expandedQuery,
                    tolerance: 2,
                    properties: ['description', 'path'],
                    boost: {
                        description: 5.0,
                        path: 10.0
                    },
                    where,
                    limit: 1000,
                    offset: 0
                });
            }
            // 5. Threshold Filtering & Mapping with Structural Analysis
            const results = [];
            for (const hit of searchResult.hits) {
                let score = hit.score;
                // Structural Metadata from Index
                const docResource = hit.document.resourceType || 'unknown';
                const docAction = hit.document.actionType || 'other';
                // A. & B. Intent Scoring via ScoreCalculator
                const docVersion = hit.document.version || 'v1';
                score = this.scoreCalculator.calculate(score, intent, {
                    resourceType: docResource,
                    actionType: docAction,
                    score: score,
                    version: docVersion
                });
                // [Final Plan] Intent-Based Thresholding
                let THRESHOLD_SCORE = 0.05; // Default (Loose)
                if (intent.resource) {
                    THRESHOLD_SCORE = 0.2;
                }
                else if (intent.action) {
                    THRESHOLD_SCORE = 0.08;
                }
                else {
                    THRESHOLD_SCORE = 0.01;
                }
                let keep = false;
                const isShortQuery = safeQuery.length <= 2;
                if (score >= THRESHOLD_SCORE && !isShortQuery) {
                    keep = true;
                }
                else if (isShortQuery) {
                    // Short Query Logic: check description for token match
                    const descBag = (hit.document.description || '').toLowerCase();
                    const inputTokens = safeQuery.toLowerCase().split(/\s+/).filter(t => t.length > 0);
                    const queryTokens = new Set(inputTokens);
                    if (queryTokens.size > 0) {
                        const docTokens = new Set(descBag.split(/\s+/));
                        const match = Array.from(queryTokens).some(token => docTokens.has(token));
                        if (match)
                            keep = true;
                    }
                }
                else {
                    keep = false;
                }
                if (!keep)
                    continue;
                const id = hit.document.id;
                const sourceData = this.lookup.get(id) || {};
                const result = {
                    path: hit.document.path,
                    method: hit.document.method,
                    summary: sourceData.summary || '',
                    description: hit.document.description,
                    score: score,
                    resourceType: docResource,
                    actionType: docAction,
                    operationId: sourceData.operationId,
                    tags: sourceData.tags,
                    warning: sourceData['x-dependency-warning'] || sourceData.warning,
                };
                results.push(result);
            }
            // [R22][R30][R31] Path-component match boost for same-resource results
            // Two-tier approach:
            //   1. Strong boost: query tokens that match a UNIQUE leaf path segment (not shared by resource prefix)
            //      e.g. "breadcrumb" matching /scoring/criteria/{id}/breadcrumb
            //   2. Tie-breaker: within 0.005 score window, count token matches for ordering
            if (results.length > 1 && intent.resource) {
                const queryTokens = safeQuery.toLowerCase().split(/\s+/).filter(t => t.length >= 2);
                const maxScore = results[0]?.score || 0;
                // Collect all leaf segments shared across results (resource prefix segments)
                const allPathSegmentSets = results.map(r => new Set((r.path || '').split('/').filter(Boolean).filter((s) => !s.startsWith('{'))));
                // Segments that appear in ALL results are "common" (e.g. "api", "v2", "scoring", "criteria")
                const commonSegments = allPathSegmentSets.length > 0
                    ? allPathSegmentSets.reduce((acc, set) => {
                        const intersection = new Set();
                        for (const s of acc) {
                            if (set.has(s))
                                intersection.add(s);
                            // Also check sub-parts (e.g. "fm-checkpoint" -> "fm", "checkpoint")
                            for (const seg of set) {
                                if (seg.split('-').includes(s))
                                    intersection.add(s);
                            }
                        }
                        return intersection;
                    })
                    : new Set();
                for (const r of results) {
                    const pathSegments = (r.path || '').split('/').filter(Boolean);
                    const pathLeafSegments = pathSegments.filter((s) => !s.startsWith('{'));
                    // Find query tokens matching UNIQUE (non-common) leaf segments
                    const uniqueMatchCount = queryTokens.filter(token => pathLeafSegments.some((seg) => {
                        const isMatch = seg === token || seg.split('-').includes(token);
                        const isCommon = commonSegments.has(token);
                        return isMatch && !isCommon;
                    })).length;
                    if (uniqueMatchCount > 0) {
                        // Strong boost for unique path segment matches (can overcome score gaps)
                        r.score = (r.score || 0) + uniqueMatchCount * 0.1;
                    }
                    else if (maxScore - (r.score || 0) < 0.005) {
                        // Tie-breaker: count all token matches within close scores
                        const allMatchCount = queryTokens.filter(token => pathLeafSegments.some((seg) => seg === token || seg.split('-').includes(token))).length;
                        if (allMatchCount > 0) {
                            r.score = (r.score || 0) + allMatchCount * 0.001;
                        }
                        else {
                            r.score = (r.score || 0) - pathSegments.length * 0.0001;
                        }
                    }
                }
            }
            // Re-sort because scores changed
            results.sort((a, b) => (b.score || 0) - (a.score || 0));
            // 6. Pagination (Slicing)
            const filteredTotal = results.length;
            const { results: slice, meta } = paginateResults(results, page, DEFAULT_PAGE_SIZE);
            // 7. Warning System
            let warning = undefined;
            if (query.includes('/') && searchResult.count > 0 && !explicitPath && !isPathQuery) {
                warning = "Query contains path characters. For exact path matching, use the 'path' parameter.";
            }
            return {
                results: slice,
                meta: {
                    ...meta,
                    total: filteredTotal
                },
                warning
            };
        }
        catch (error) {
            console.error('Search execution failed:', error);
            return { results: [], meta: { total: 0, page, totalPages: 0 }, warning: 'Internal search error' };
        }
    }
    async getDetails(path, method) {
        await this.initPromise;
        if (!this.db)
            return null;
        try {
            // Try lookup first (fast path)
            if (method) {
                const id = `${method}:${path}`;
                const content = this.lookup.get(id);
                if (content) {
                    const metadata = {
                        resourceType: undefined,
                        actionType: undefined,
                        score: 1.0,
                        operationId: content.operationId,
                        tags: content.tags
                    };
                    // Get resourceType/actionType from index
                    const result = await search(this.db, {
                        term: path,
                        properties: ['path'],
                        limit: 20
                    });
                    for (const hit of result.hits) {
                        if (hit.document.path === path && hit.document.method === method) {
                            metadata.resourceType = hit.document.resourceType;
                            metadata.actionType = hit.document.actionType;
                            break;
                        }
                    }
                    return { content, metadata };
                }
            }
            // Fallback: search index
            const result = await search(this.db, {
                term: path,
                properties: ['path'],
                limit: 20
            });
            if (result.count > 0) {
                for (const hit of result.hits) {
                    if (hit.document.path === path && (!method || hit.document.method === method)) {
                        const id = hit.document.id;
                        const content = this.lookup.get(id) || { path, method: hit.document.method, description: hit.document.description };
                        const metadata = {
                            resourceType: hit.document.resourceType,
                            actionType: hit.document.actionType,
                            score: 1.0,
                            operationId: content.operationId,
                            tags: content.tags
                        };
                        return { content, metadata };
                    }
                }
            }
            return null;
        }
        catch (error) {
            console.error('getDetails failed:', error);
            return null;
        }
    }
    async listCategories() {
        await this.initPromise;
        if (!this.db)
            return [];
        const categories = new Set();
        // Use lookup map to aggregate tags
        for (const endpoint of this.lookup.values()) {
            if (endpoint.tags && Array.isArray(endpoint.tags)) {
                endpoint.tags.forEach((t) => categories.add(t));
            }
        }
        return Array.from(categories).map(c => ({ name: c, count: 0 }));
    }
    async listAll() {
        await this.initPromise;
        const endpoints = [];
        for (const key of this.lookup.keys()) {
            const colonIdx = key.indexOf(':');
            if (colonIdx === -1)
                continue;
            const method = key.substring(0, colonIdx);
            const path = key.substring(colonIdx + 1);
            endpoints.push({ method, path });
        }
        endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
        return { total: endpoints.length, endpoints };
    }
    detectIntent(query) {
        const lower = query.toLowerCase();
        // Action Detection - Use ACTION_PRIORITY for correct order
        let action = undefined;
        for (const actionType of ACTION_PRIORITY) {
            const keywords = ACTION_KEYWORDS[actionType];
            if (keywords.some(k => matchesKeyword(lower, k))) {
                action = actionType;
                break;
            }
        }
        // Resource Detection
        let resource = undefined;
        // [Round 13 Refactoring] Longest Match Strategy
        const potentialResources = [];
        // [Final Plan] Reverse Lookup from RESOURCE_ALIASES
        for (const [resType, aliases] of Object.entries(RESOURCE_ALIASES)) {
            // Check explicit resource type name first
            const typeName = resType.replace(/-/g, ' ');
            if (lower.includes(typeName)) {
                potentialResources.push({ type: resType, length: typeName.length });
            }
            // Check aliases
            for (const alias of aliases) {
                if (lower.includes(alias)) {
                    potentialResources.push({ type: resType, length: alias.length });
                }
            }
        }
        // Sort by length (desc) to prioritize compound terms like "block diagram" over "block"
        potentialResources.sort((a, b) => b.length - a.length);
        if (potentialResources.length > 0) {
            resource = potentialResources[0].type;
        }
        // [Heuristic] Compound resource detection:
        // "worksheet" + "template" + "excel" → prefer 'worksheet-template-excel' [R40]
        if (lower.includes('worksheet') && lower.includes('template') && lower.includes('excel')) {
            resource = 'worksheet-template-excel';
        }
        // "worksheet" + "excel" → prefer 'worksheet-excel' over either alone
        else if (lower.includes('worksheet') && lower.includes('excel')) {
            resource = 'worksheet-excel';
        }
        // [R19] "failure mode" + "diagram" (without "block") → prefer fmm-diagram
        if (lower.includes('failure mode') && lower.includes('diagram') && !lower.includes('block')) {
            resource = 'fmm-diagram';
        }
        // "worksheet" + "failure mode" → keep as 'worksheet' (not failure-mode)
        if (lower.includes('worksheet') && (lower.includes('failure mode') || lower.includes('failuremode'))) {
            resource = 'worksheet';
        }
        // [R23] "system definition" + export/import action → prefer 'system-definition-excel'
        if (resource === 'system-definition' && (action === 'export' || action === 'import' || lower.includes('excel'))) {
            resource = 'system-definition-excel';
        }
        // [R34] "synonym" + export/import/excel → prefer 'synonym-excel'
        if (resource === 'synonym' && (action === 'export' || action === 'import' || lower.includes('excel'))) {
            resource = 'synonym-excel';
        }
        // [R40] Resource + export/excel/xlsx/spreadsheet → prefer dedicated '-excel' resource type
        // Note: 'import' excluded to avoid false triggers (e.g. upload image → classification-excel)
        const excelResources = [
            'additional-info', 'api-key', 'apikey-log', 'classification',
            'division', 'evaluation', 'notice', 'process-symbol', 'user', 'worksheet-template',
        ];
        const hasExcelSignal = action === 'export' || lower.includes('excel') || lower.includes('xlsx') || lower.includes('spreadsheet');
        if (hasExcelSignal) {
            for (const base of excelResources) {
                if (resource === base) {
                    resource = `${base}-excel`;
                    break;
                }
            }
            // significance-criteria + excel signal → evaluation-excel
            if (resource === 'significance-criteria') {
                resource = 'evaluation-excel';
            }
        }
        // [R40] 'revision' resource is always a MONITOR operation (change/edit history)
        if (resource === 'revision') {
            action = 'MONITOR';
        }
        // [R19] "get X by Y ID" is a specific read, not a list
        if (action === 'list' && /\bby\b.*\bid\b/i.test(lower)) {
            action = 'read';
        }
        // [Heuristic] If resource is found but action is ambiguous, default to 'list'
        if (!action && resource) {
            action = 'list';
        }
        // [R39] Approval resource: override action for better sub-routing
        if (resource === 'approval') {
            const hasDecision = lower.includes('decision') || lower.includes('refuse') || lower.includes('reject') || lower.includes('batch');
            const hasRequest = lower.includes('request') || lower.includes('submit') || lower.includes('reviewer') || lower.includes('send');
            if (action === 'update' || hasDecision || hasRequest) {
                action = 'create'; // POST /approvals/decisions or /approvals/requests
            }
        }
        // [R39] Board resource: better differentiation between list/read/create
        if (resource === 'board') {
            const hasRepliesNoun = lower.includes('with replies') || lower.includes('replies') || lower.includes('thread');
            const hasReplyVerb = lower.startsWith('reply') || lower.includes(' reply to') || lower.includes('reply to');
            if (hasReplyVerb) {
                action = 'create'; // POST /board/{boardId}/replies
            }
            else if (hasRepliesNoun && action !== 'create') {
                action = 'read'; // GET /board/{boardId}
            }
        }
        // [R16] [R22] Heuristic for 'save work' — save block nodes or fmm-diagram
        if (!resource && lower.includes('save work')) {
            action = 'update';
            resource = 'block-node'; // Most likely: "Save block nodes" PUT endpoint
        }
        // [Fix] Auth Actions imply 'auth' resource, overriding 'user'
        if (['login', 'logout'].includes(action || '') && resource !== 'oauth') {
            resource = 'auth';
        }
        // [R22] "authenticate client" / "client auth" → api-key (API keys are the auth mechanism)
        if (lower.includes('client') && ['login', 'logout'].includes(action || '')) {
            resource = 'api-key';
            action = 'list';
        }
        return { action, resource };
    }
}
