import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { getConfig } from '../config';
import { getActiveBrainProfile, findBrainFiles, logInfo, logError } from '../utils';
import { RetrievalOrchestrator } from '../retrieval';
import { getBrainTokenIndex, backfillBrainEmbeddings, backfillBrainChunkEmbeddings } from '../retrieval/brainIndex';
import { embedQuery, embedTexts } from '../retrieval/embeddings';
import {
    loadGoldenSet,
    runRetrievalEval,
    formatReportMarkdown,
    GOLDEN_TEMPLATE,
    GOLDEN_REL_JSONL,
} from '../retrieval/evalHarness';

/**
 * 검색 평가 명령 묶음 (Phase 1-나).
 *
 * `g1nation.eval.retrieval` — 활성 두뇌의 골든셋(.astra/eval/golden.jsonl)으로 검색
 * recall@k / MRR 를 측정해 마크다운 리포트를 남긴다. 골든셋이 없으면 템플릿을 만들어
 * 열어준다. 청킹(Phase 1-가) 도입 전/후를 같은 골든셋으로 돌려 개선을 *숫자로* 증명하는 것이 목적.
 */
export function registerEvalCommands(): vscode.Disposable[] {
    return [
        vscode.commands.registerCommand('g1nation.eval.retrieval', runRetrievalEvalCommand),
        vscode.commands.registerCommand('g1nation.embeddings.backfill', backfillEmbeddingsCommand),
    ];
}

const EVAL_KS = [1, 3, 5, 10];

async function runRetrievalEvalCommand(): Promise<void> {
    try {
        const brain = getActiveBrainProfile();
        if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) {
            vscode.window.showErrorMessage('활성 두뇌 폴더를 찾을 수 없습니다. 먼저 두뇌를 추가/선택하세요.');
            return;
        }

        // 1) 골든셋 로드 — 없으면 템플릿 스캐폴드 후 열어주고 종료.
        const { entries, sourcePath, parseErrors } = loadGoldenSet(brain.localBrainPath);
        if (entries.length === 0) {
            const goldenPath = path.join(brain.localBrainPath, GOLDEN_REL_JSONL);
            const created = await scaffoldGoldenSet(goldenPath, sourcePath, parseErrors);
            if (created) {
                const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(goldenPath));
                await vscode.window.showTextDocument(doc);
                vscode.window.showInformationMessage(
                    '골든셋 템플릿을 만들었습니다. 질문→기대문서 쌍을 채운 뒤 다시 "Astra: 검색 평가 실행"을 실행하세요.',
                );
            }
            return;
        }

        const config = getConfig();

        await vscode.window.withProgress(
            { location: vscode.ProgressLocation.Notification, title: 'Astra 검색 평가', cancellable: false },
            async (progress) => {
                // 2) 인덱스 워밍업 — 전체 brain 파일을 토크나이즈 인덱스에 로드 (backfill 의 전제).
                progress.report({ message: '인덱스 로드 중…' });
                const allFiles = findBrainFiles(brain.localBrainPath);
                getBrainTokenIndex(brain.localBrainPath, allFiles);

                // 3) 임베딩 backfill — 설정된 경우 dense 항이 공정하게 평가되도록 모든 파일 벡터를 채운다.
                const useEmbeddings = !!config.embeddingModel && (config.embeddingBlendAlpha ?? 0) > 0;
                if (useEmbeddings) {
                    progress.report({ message: `임베딩 채우는 중 (${config.embeddingModel})…` });
                    const embed = (texts: string[]) => embedTexts(texts, { baseUrl: config.ollamaUrl, model: config.embeddingModel });
                    try {
                        if (config.chunkLevelRetrieval === true) {
                            await backfillBrainChunkEmbeddings(brain.localBrainPath, allFiles, config.embeddingModel, embed, config.chunkTargetChars);
                        } else {
                            await backfillBrainEmbeddings(brain.localBrainPath, allFiles, config.embeddingModel, embed);
                        }
                    } catch (e: any) {
                        logInfo('Eval embedding backfill failed — continuing TF-IDF only.', { error: e?.message ?? String(e) });
                    }
                }

                // 4) 평가 실행. ranker 는 프로덕션과 동일한 scoring 경로를 쓰되 budget 적용 전 랭킹을 본다.
                const orchestrator = new RetrievalOrchestrator();
                let done = 0;
                const ranker = async (query: string): Promise<string[]> => {
                    done++;
                    progress.report({ message: `질의 ${done}/${entries.length} 평가 중…` });
                    let queryEmbedding: number[] | undefined;
                    if (useEmbeddings) {
                        try {
                            queryEmbedding = await Promise.race([
                                embedQuery(query, { baseUrl: config.ollamaUrl, model: config.embeddingModel }),
                                new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), 4000)),
                            ]);
                        } catch { queryEmbedding = undefined; }
                    }
                    return orchestrator
                        .rankBrainForEval(query, brain, {
                            limit: Math.max(...EVAL_KS) + 5,
                            queryEmbedding,
                            embeddingModel: config.embeddingModel || undefined,
                            embeddingBlendAlpha: config.embeddingBlendAlpha,
                            chunkLevelRetrieval: config.chunkLevelRetrieval === true,
                            chunkTargetChars: config.chunkTargetChars,
                        })
                        .map((r) => r.relativePath);
                };

                const report = await runRetrievalEval({ entries, ks: EVAL_KS, ranker });

                // 5) 리포트 저장 + 열기.
                const now = new Date();
                const stamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
                const mode = config.chunkLevelRetrieval === true
                    ? `섹션 청크 (target=${config.chunkTargetChars}자)`
                    : '파일 단위 (baseline)';
                const noteParts = [`검색 모드: ${mode}`];
                if (parseErrors > 0) noteParts.push(`골든셋 파싱 실패 ${parseErrors}줄 (무시됨)`);
                const md = formatReportMarkdown(report, {
                    brainName: brain.name,
                    dateStr: now.toLocaleString(),
                    embeddingModel: useEmbeddings ? config.embeddingModel : '',
                    alpha: config.embeddingBlendAlpha ?? 0,
                    notes: noteParts.join(' · '),
                });
                const reportPath = path.join(brain.localBrainPath, '.astra', 'eval', `report-${stamp}.md`);
                fs.mkdirSync(path.dirname(reportPath), { recursive: true });
                fs.writeFileSync(reportPath, md, 'utf8');
                logInfo('Retrieval eval complete.', {
                    queries: report.total,
                    recallAt5: report.recallAtK[5],
                    mrr: report.mrr,
                    reportPath,
                });

                const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(reportPath));
                await vscode.window.showTextDocument(doc, { preview: false });
                vscode.window.showInformationMessage(
                    `검색 평가 완료 · recall@5 ${(report.recallAtK[5] * 100).toFixed(0)}% · MRR ${report.mrr.toFixed(2)} (질의 ${report.total}개)`,
                );
            },
        );
    } catch (err: any) {
        logError('Retrieval eval command failed.', { error: err?.message || String(err) });
        vscode.window.showErrorMessage(`검색 평가 실패: ${err?.message ?? err}`);
    }
}

/**
 * 두뇌 전체 임베딩 색인 채우기. 평소엔 턴마다 *검색된 파일* 만 lazy backfill 되므로 dense
 * 검색이 충분히 효과를 내려면 오래 걸린다 — 이 명령으로 한 번에 채운다. 청크 모드면 청크
 * 단위 벡터를, 아니면 파일 단위 벡터를 채운다. 엔진 호출 크기를 제한하려 파일 배치로 처리.
 */
async function backfillEmbeddingsCommand(): Promise<void> {
    try {
        const brain = getActiveBrainProfile();
        if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) {
            vscode.window.showErrorMessage('활성 두뇌 폴더를 찾을 수 없습니다.');
            return;
        }
        const config = getConfig();
        if (!config.embeddingModel) {
            vscode.window.showWarningMessage(
                '임베딩 모델이 설정되지 않았습니다. 엔진(Ollama/LM Studio)에 임베딩 모델을 로드한 뒤 ' +
                'g1nation.embeddingModel 에 그 모델명을 입력하세요. (없어도 TF-IDF 검색은 동작합니다.)',
            );
            return;
        }

        const chunkMode = config.chunkLevelRetrieval === true;
        await vscode.window.withProgress(
            { location: vscode.ProgressLocation.Notification, title: `Astra 임베딩 색인 (${config.embeddingModel})`, cancellable: true },
            async (progress, token) => {
                const allFiles = findBrainFiles(brain.localBrainPath);
                getBrainTokenIndex(brain.localBrainPath, allFiles);
                const embed = (texts: string[]) => embedTexts(texts, { baseUrl: config.ollamaUrl, model: config.embeddingModel });

                const BATCH = 40;
                let embedded = 0;
                for (let i = 0; i < allFiles.length; i += BATCH) {
                    if (token.isCancellationRequested) break;
                    const slice = allFiles.slice(i, i + BATCH);
                    progress.report({
                        message: `${Math.min(i + BATCH, allFiles.length)}/${allFiles.length} 파일 · 임베딩 ${embedded}개`,
                        increment: (BATCH / allFiles.length) * 100,
                    });
                    try {
                        embedded += chunkMode
                            ? await backfillBrainChunkEmbeddings(brain.localBrainPath, slice, config.embeddingModel, embed, config.chunkTargetChars)
                            : await backfillBrainEmbeddings(brain.localBrainPath, slice, config.embeddingModel, embed);
                    } catch (e: any) {
                        logInfo('Embedding batch failed — continuing.', { batchStart: i, error: e?.message ?? String(e) });
                    }
                }
                logInfo('Full-brain embedding backfill done.', { mode: chunkMode ? 'chunk' : 'file', files: allFiles.length, embedded });
                vscode.window.showInformationMessage(
                    `임베딩 색인 완료 · ${chunkMode ? '청크' : '파일'} 단위 · 신규 ${embedded}개 (${allFiles.length} 파일 스캔). ` +
                    `이제 '검색 평가 실행'으로 dense 효과를 측정해 보세요.`,
                );
            },
        );
    } catch (err: any) {
        logError('Embedding backfill command failed.', { error: err?.message || String(err) });
        vscode.window.showErrorMessage(`임베딩 색인 실패: ${err?.message ?? err}`);
    }
}

/** 골든셋 파일이 없을 때 템플릿을 만든다. 이미 (깨진/빈) 파일이 있으면 덮어쓰지 않는다. */
async function scaffoldGoldenSet(goldenPath: string, existingSource: string | null, parseErrors: number): Promise<boolean> {
    if (existingSource && fs.existsSync(existingSource)) {
        // 파일은 있는데 유효 항목이 0개 — 사용자가 작성 중이거나 오타. 덮어쓰지 않고 안내만.
        vscode.window.showWarningMessage(
            `골든셋(${path.basename(existingSource)})에 유효한 항목이 없습니다${parseErrors ? ` (파싱 실패 ${parseErrors}줄)` : ''}. ` +
            '각 줄을 {"query": "...", "expected": ["파일명.md"]} 형식으로 작성하세요.',
        );
        const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(existingSource));
        await vscode.window.showTextDocument(doc);
        return false;
    }
    try {
        fs.mkdirSync(path.dirname(goldenPath), { recursive: true });
        fs.writeFileSync(goldenPath, GOLDEN_TEMPLATE, 'utf8');
        return true;
    } catch (e: any) {
        vscode.window.showErrorMessage(`골든셋 템플릿 생성 실패: ${e?.message ?? e}`);
        return false;
    }
}
