import * as fs from "fs/promises";
import * as path from "path";
import { getAllFiles } from "../../utils/fs-helper.js";
import { stemToken } from "../../utils/stemming.js";
import { getSynonyms } from "../../synonyms.js";
import { paginateResults, DEFAULT_PAGE_SIZE } from "../../utils/search-helper.js";
export class FileSearchService {
    endpointsDir;
    debugMode;
    constructor(endpointsDir, debugMode = false) {
        this.endpointsDir = endpointsDir;
        this.debugMode = debugMode;
    }
    async search(query, filterMethod, filterVersion, page = 1, _explicitPath) {
        const files = await getAllFiles(this.endpointsDir);
        let documents = [];
        // 1. Prepare Documents (Corpus)
        for (const filePath of files) {
            try {
                const content = await fs.readFile(filePath, "utf-8");
                const json = JSON.parse(content);
                const fileName = path.relative(this.endpointsDir, filePath);
                // Version Filtering
                if (filterVersion) {
                    const fileVersion = fileName.split(path.sep)[0];
                    if (fileVersion !== filterVersion && json.version !== filterVersion) {
                        continue;
                    }
                }
                if (json.endpoints && Array.isArray(json.endpoints)) {
                    for (const endpoint of json.endpoints) {
                        // Method Filtering
                        if (filterMethod && endpoint.method.toUpperCase() !== filterMethod) {
                            continue;
                        }
                        // Create searchable text blob
                        // Weighting: Summary (3x), OperationID (2x), Description (1x), Path (1x)
                        const searchableText = [
                            (endpoint.summary || "").toLowerCase().repeat(3),
                            (endpoint.operationId || "").toLowerCase().repeat(2),
                            (endpoint.description || "").toLowerCase(),
                            (endpoint.path || "").toLowerCase()
                        ].join(" ");
                        const tokens = searchableText.split(/\s+/).filter(t => t.length > 0);
                        // Stemming tokens for Indexing
                        const stemmedTokens = tokens.map(token => stemToken(token));
                        documents.push({
                            file: fileName,
                            ...endpoint,
                            // Use stemmed tokens for matching and BM25
                            tokens: stemmedTokens,
                            docLength: stemmedTokens.length
                        });
                    }
                }
            }
            catch (_e) {
                // Ignore parse errors
                console.error("FileSearchService.search: Error parsing endpoint file:", _e);
            }
        }
        const totalFound = documents.length;
        if (totalFound === 0) {
            return {
                results: [],
                meta: { total: 0, page, totalPages: 0 },
                message: `No results found for '${query}'.`
            };
        }
        // 2. Query Processing
        const rawQueryTokens = query.toLowerCase().split(/\s+/).filter(t => t.length > 0);
        // Check for Wildcard
        if (rawQueryTokens.length === 0 || (rawQueryTokens.length === 1 && rawQueryTokens[0] === "*")) {
            const finalResults = documents.map(({ tokens: _tokens, docLength: _docLength, ...rest }) => rest);
            const { results, meta } = paginateResults(finalResults, page, DEFAULT_PAGE_SIZE);
            return {
                results,
                meta
            };
        }
        // Filter Documents: AND Logic with Stemming + Synonyms
        // Step: Query Token -> Stem -> Synonyms -> Stem Synonyms -> Match Document Stems
        const filteredDocs = documents.filter(doc => {
            return rawQueryTokens.every(qToken => {
                // 1. Stem the query token first (e.g. "parsing" -> "pars")
                // But wait, synonyms often map "parse" -> "parsing".
                // If we stem query first, we might miss synonym keys.
                // BETTER STRATEGY: Get synonyms for RAW token, THEN stem everything.
                const synonyms = getSynonyms(qToken); // e.g. "upload" -> ["post", "send"...]
                // Stem the query token and all its synonyms
                // e.g. "uploaded" -> stem("uploaded")="upload"
                // synonyms("uploaded") might be empty if map uses root forms.
                // So we should try:
                // A. Synonyms of raw token
                // B. Synonyms of stemmed token ? - Maybe overkill, let's stick to A for now.
                const searchTerms = new Set();
                synonyms.forEach(s => searchTerms.add(stemToken(s)));
                // Also stem the query token itself regardless of synonyms
                searchTerms.add(stemToken(qToken));
                // Check if ANY of the stemmed search terms exist in the document's stemmed tokens
                return Array.from(searchTerms).some(term => doc.tokens.includes(term));
            });
        });
        if (filteredDocs.length === 0) {
            return {
                results: [],
                meta: { total: 0, page, totalPages: 0 },
                message: `No results found for '${query}'. Try fewer keywords or check spelling.`
            };
        }
        // 3. BM25 Calculation on Filtered Docs
        const k1 = 1.2;
        const b = 0.75;
        const avgdl = documents.reduce((acc, doc) => acc + doc.docLength, 0) / totalFound;
        // Calculate IDF (using full corpus) for stemmed terms
        const allQueryTerms = new Set();
        rawQueryTokens.forEach(t => {
            const synonyms = getSynonyms(t);
            synonyms.forEach(s => allQueryTerms.add(stemToken(s)));
            allQueryTerms.add(stemToken(t));
        });
        const idf = {};
        for (const term of allQueryTerms) {
            let n_q = 0;
            for (const doc of documents) {
                if (doc.tokens.includes(term))
                    n_q++;
            }
            idf[term] = Math.log((totalFound - n_q + 0.5) / (n_q + 0.5) + 1);
        }
        // Score Filtered Documents
        let scoredDocs = filteredDocs.map(doc => {
            let score = 0;
            for (const qToken of rawQueryTokens) {
                const synonyms = getSynonyms(qToken);
                const searchTerms = new Set();
                synonyms.forEach(s => searchTerms.add(stemToken(s)));
                searchTerms.add(stemToken(qToken));
                const presentTerms = Array.from(searchTerms).filter(term => doc.tokens.includes(term));
                for (const term of presentTerms) {
                    const f_q = doc.tokens.filter((t) => t === term).length;
                    const numerator = idf[term] * f_q * (k1 + 1);
                    const denominator = f_q + k1 * (1 - b + b * (doc.docLength / avgdl));
                    score += numerator / denominator;
                }
            }
            return { ...doc, score };
        });
        // Sort by score descending
        scoredDocs.sort((a, b) => b.score - a.score);
        // Pagination
        const totalHits = scoredDocs.length;
        // Pagination using helper
        const { results: slice, meta } = paginateResults(scoredDocs, page, DEFAULT_PAGE_SIZE);
        // Post-processing: Add warnings for V1 endpoints AND strip heavy fields
        const finalResults = await Promise.all(slice.map(async (item) => {
            const { score, tokens: _tokens, docLength: _docLength, parameters: _parameters, requestBody: _requestBody, responses: _responses, tags: _tags, file: _file, ...lightweightItem } = item;
            if (lightweightItem.path && lightweightItem.path.includes("/v1/")) {
                lightweightItem.warning = await this.generateDeprecationWarning(lightweightItem.path, lightweightItem.method);
            }
            // Include score in debug mode
            if (this.debugMode) {
                lightweightItem.score = score;
            }
            return lightweightItem;
        }));
        let warning = undefined;
        if (meta.totalPages > 1) {
            warning = `Found ${totalHits} results. Showing page ${meta.page} of ${meta.totalPages}.`;
        }
        return {
            results: finalResults,
            meta,
            warning
        };
    }
    async getDetails(apiPath, method) {
        const files = await getAllFiles(this.endpointsDir);
        for (const filePath of files) {
            try {
                const content = await fs.readFile(filePath, "utf-8");
                const json = JSON.parse(content);
                if (json.endpoints && Array.isArray(json.endpoints)) {
                    for (const endpoint of json.endpoints) {
                        if (endpoint.path === apiPath) {
                            if (method && endpoint.method.toUpperCase() !== method) {
                                continue;
                            }
                            const result = {
                                ...endpoint
                            };
                            // Check for V1 Deprecation
                            if (apiPath.includes("/v1/")) {
                                result.warning = await this.generateDeprecationWarning(apiPath, method);
                            }
                            return result;
                        }
                    }
                }
            }
            catch (_e) {
                // Ignore parse errors
                console.error("FileSearchService.getDetails: Error parsing endpoint file:", _e);
            }
        }
        return null;
    }
    async listCategories() {
        const categories = [];
        try {
            const topLevel = await fs.readdir(this.endpointsDir);
            for (const versionDir of topLevel) {
                const versionPath = path.join(this.endpointsDir, versionDir);
                const stat = await fs.stat(versionPath);
                if (stat.isDirectory() && !versionDir.startsWith('.')) {
                    // Look one level deeper for domains (e.g. v1/projects)
                    const domains = await fs.readdir(versionPath);
                    for (const domain of domains) {
                        const domainPath = path.join(versionPath, domain);
                        const domainStat = await fs.stat(domainPath);
                        if (domainStat.isDirectory()) {
                            categories.push({
                                category: `${versionDir}/${domain}`,
                                description: `API endpoints for ${domain} (${versionDir})`
                            });
                        }
                    }
                }
            }
        }
        catch (e) {
            console.error("Error listing categories:", e);
        }
        return {
            categories: categories,
            message: "Use 'search_apis' with a query to find specific endpoints, or 'get_api_details' to see full schemas."
        };
    }
    async listAll() {
        const files = await getAllFiles(this.endpointsDir);
        const endpoints = [];
        for (const filePath of files) {
            try {
                const content = await fs.readFile(filePath, "utf-8");
                const json = JSON.parse(content);
                if (json.endpoints && Array.isArray(json.endpoints)) {
                    for (const ep of json.endpoints) {
                        endpoints.push({ method: ep.method, path: ep.path });
                    }
                }
            }
            catch (_e) {
                // Ignore parse errors
            }
        }
        endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
        return { total: endpoints.length, endpoints };
    }
    // Efficiently check if an endpoint exists without reading files if content is not needed
    async findEndpointInFiles(files, apiPath, method) {
        for (const filePath of files) {
            try {
                const content = await fs.readFile(filePath, "utf-8");
                const json = JSON.parse(content);
                if (json.endpoints && Array.isArray(json.endpoints)) {
                    for (const ep of json.endpoints) {
                        if (ep.path === apiPath) {
                            if (method && ep.method.toUpperCase() !== method)
                                continue;
                            return true;
                        }
                    }
                }
            }
            catch (_e) {
                // Ignore parse errors
                console.error("FileSearchService.findEndpointInFiles: Error parsing endpoint file:", _e);
            }
        }
        return false;
    }
    // 3-Step Intelligent Warning Logic
    async generateDeprecationWarning(v1Path, method) {
        const files = await getAllFiles(this.endpointsDir);
        // Step 1: Direct Match (v1 -> v2)
        const v2Path = v1Path.replace("/v1/", "/v2/");
        const v2Exists = await this.findEndpointInFiles(files, v2Path, method);
        if (v2Exists) {
            return `DEPRECATED: Version v1 is deprecated. Please use v2 endpoint: ${v2Path}`;
        }
        // Step 2: Domain Hint (e.g. /api/v1/auth/login -> Check /api/v2/auth/)
        // Extract domain: /api/v1/projects/... -> projects, /api/v1/auth/... -> auth
        const match = v1Path.match(/\/api\/v1\/([^/]+)/);
        if (match && match[1]) {
            const domain = match[1];
            const v2DomainPathStart = `/api/v2/${domain}`;
            // Check if any v2 endpoint exists in this domain
            let domainExists = false;
            for (const filePath of files) {
                try {
                    const content = await fs.readFile(filePath, "utf-8");
                    const json = JSON.parse(content);
                    if (json.endpoints && Array.isArray(json.endpoints)) {
                        if (json.endpoints.some((ep) => ep.path && ep.path.startsWith(v2DomainPathStart))) {
                            domainExists = true;
                            break;
                        }
                    }
                }
                catch (_e) {
                    // Ignore parse errors
                    console.error("FileSearchService.generateDeprecationWarning: Error parsing endpoint file:", _e);
                }
            }
            if (domainExists) {
                return `LEGACY: Direct v2 replacement not found, but newer '${domain}' related features exist in v2. Please search for '${domain}' in v2.`;
            }
        }
        // Step 3: General Legacy Warning
        return "LEGACY: This v1 endpoint is deprecated and may be removed in the future.";
    }
}
