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';
import {
    loadTaskGoldenSet,
    runTaskEval,
    formatTaskEvalReport,
    TASK_GOLDEN_DIR,
} from '../intelligence/taskEvalHarness';
import { buildRequirementGraphBlock } from '../intelligence/requirementGraph';
import { buildEpistemicGuardBlock } from '../intelligence/epistemicGuardBlock';
import { simpleChatCompletion } from '../intelligence/llmCall';
import { loadReflections, formatGrowthReport } from '../intelligence/reflectionStore';
import { computeNeeds, knowledgeInventory, computeKnowledgeDebt, formatNeedsMarkdown } from '../intelligence/needEngine';
import { auditKnowledgeDecay, formatDecayReport } from '../intelligence/knowledgeDecay';
import { computeSkillScores, formatSkillScoresMarkdown, loadSuccessPatterns, formatSuccessPatternsMarkdown } from '../intelligence/skillScore';
import { runResearch, formatProposalMarkdown } from '../intelligence/researchAgent';
import type { ExistingKnowledgeRef } from '../intelligence/knowledgeValidation';
import { loadQueue, saveQueue, mergeNeedsIntoQueue, formatQueueMarkdown, LEARNING_QUEUE_REL_PATH } from '../intelligence/learningQueue';

/**
 * 검색 평가 명령 묶음 (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),
        vscode.commands.registerCommand('g1nation.eval.tasks', runTaskEvalCommand),
        vscode.commands.registerCommand('g1nation.growth.report', growthReportCommand),
        vscode.commands.registerCommand('g1nation.growth.learningQueue', learningQueueCommand),
        vscode.commands.registerCommand('g1nation.knowledge.decayAudit', decayAuditCommand),
        vscode.commands.registerCommand('g1nation.research.runQueue', researchRunQueueCommand),
    ];
}

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}`);
    }
}

/**
 * 업무 평가 (Self Evaluation v1, Phase 3 / Track 3-4) — 회의록 골든셋의 각 원자료를
 * LLM 에게 회의록으로 작성시키고 필수 요소 커버리지를 결정론적으로 채점. 같은 골든셋을
 * 버전마다 돌려 점수 추이로 성장세를 증명한다 (검색 평가와 동일 방법론).
 */
async function runTaskEvalCommand(): Promise<void> {
    try {
        const brain = getActiveBrainProfile();
        if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) {
            vscode.window.showErrorMessage('활성 두뇌 폴더를 찾을 수 없습니다. 먼저 두뇌를 추가/선택하세요.');
            return;
        }
        const { records, parseErrors, sourcePath } = loadTaskGoldenSet(brain.localBrainPath, 'meeting-minutes');
        if (records.length === 0) {
            vscode.window.showWarningMessage(
                `업무 골든셋이 없습니다: ${path.join(TASK_GOLDEN_DIR, 'meeting-minutes.golden.jsonl')}` +
                (parseErrors ? ` (파싱 실패 ${parseErrors}줄)` : ''),
            );
            return;
        }
        const config = getConfig();
        const model = config.defaultModel;
        if (!model || !config.ollamaUrl) {
            vscode.window.showErrorMessage('모델/엔진 설정이 없습니다 (defaultModel, ollamaUrl).');
            return;
        }

        await vscode.window.withProgress(
            { location: vscode.ProgressLocation.Notification, title: 'Astra 업무 평가 (회의록)', cancellable: true },
            async (progress, token) => {
                const result = await runTaskEval({
                    records,
                    readSource: (sourceFile) => fs.readFileSync(sourceFile, 'utf8'),
                    generate: async (record, sourceContent) => {
                        if (token.isCancellationRequested) throw new Error('취소됨');
                        // 프로덕션과 같은 지시 체계 — Requirement Graph + Epistemic Guard 블록 주입.
                        const system = [
                            '너는 업무 비서다. 제공된 회의 전사를 회의록으로 정리한다.',
                            buildRequirementGraphBlock(record.query),
                            buildEpistemicGuardBlock({ chunkCount: 1, taskDetected: true }),
                        ].filter(Boolean).join('\n\n');
                        const user = `${record.query}\n\n[회의 전사]\n${sourceContent}`;
                        return simpleChatCompletion(system, user, {
                            baseUrl: config.ollamaUrl,
                            model,
                            temperature: 0.2,
                            maxTokens: 1600,
                            timeoutMs: 180000,
                        });
                    },
                    onProgress: (done, total) => progress.report({ message: `${done}/${total} 레코드 평가 중…` }),
                });

                const now = new Date();
                const stamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
                const md = formatTaskEvalReport(result, {
                    taskLabel: '회의록',
                    brainName: brain.name,
                    dateStr: now.toLocaleString(),
                    modelName: model,
                    notes: parseErrors ? `골든셋 파싱 실패 ${parseErrors}줄 (무시됨)` : undefined,
                });
                const reportPath = path.join(brain.localBrainPath, TASK_GOLDEN_DIR, `report-${stamp}.md`);
                fs.mkdirSync(path.dirname(reportPath), { recursive: true });
                fs.writeFileSync(reportPath, md, 'utf8');
                logInfo('Task eval complete.', { records: result.scores.length, avgCoverage: result.avgCoverage, reportPath });

                const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(reportPath));
                await vscode.window.showTextDocument(doc, { preview: false });
                vscode.window.showInformationMessage(
                    `업무 평가 완료 · 평균 커버리지 ${(result.avgCoverage * 100).toFixed(1)}% · 전 요소 충족 ${result.perfectCount}/${result.scores.length}건 (골든셋: ${path.basename(sourcePath)})`,
                );
            },
        );
    } catch (err: any) {
        logError('Task eval command failed.', { error: err?.message || String(err) });
        vscode.window.showErrorMessage(`업무 평가 실패: ${err?.message ?? err}`);
    }
}

/** 성장 리포트 — Reflection 기록(.astra/growth/reflections.jsonl)의 주별 추이 + 반복 실수 Top. */
async function growthReportCommand(): Promise<void> {
    try {
        const brain = getActiveBrainProfile();
        if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) {
            vscode.window.showErrorMessage('활성 두뇌 폴더를 찾을 수 없습니다.');
            return;
        }
        const records = loadReflections(brain.localBrainPath);
        const md = [
            formatGrowthReport(records),
            formatSkillScoresMarkdown(computeSkillScores(records)),
            formatSuccessPatternsMarkdown(loadSuccessPatterns(brain.localBrainPath)),
        ].join('\n\n');
        const reportPath = path.join(brain.localBrainPath, '.astra', 'growth', 'growth-report.md');
        fs.mkdirSync(path.dirname(reportPath), { recursive: true });
        fs.writeFileSync(reportPath, md, 'utf8');
        const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(reportPath));
        await vscode.window.showTextDocument(doc, { preview: false });
        if (records.length === 0) {
            vscode.window.showInformationMessage('아직 Reflection 기록이 없습니다 — 업무(회의록/조사/일정) 요청을 처리하면 자동으로 쌓입니다.');
        }
    } catch (err: any) {
        logError('Growth report command failed.', { error: err?.message || String(err) });
        vscode.window.showErrorMessage(`성장 리포트 실패: ${err?.message ?? err}`);
    }
}

/**
 * 학습 큐 갱신 (Phase 3 / Track 3-3 + 3-5) — Reflection 기록을 Need Engine 으로 집계해
 * 학습 우선순위를 산출하고 Learning Queue 에 *proposed* 로 병합한다. 승인(approved)은
 * 사람이 learning-queue.json 에서 직접 — Permission Based Learning (Constitution 8-2).
 */
async function learningQueueCommand(): Promise<void> {
    try {
        const brain = getActiveBrainProfile();
        if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) {
            vscode.window.showErrorMessage('활성 두뇌 폴더를 찾을 수 없습니다.');
            return;
        }
        const records = loadReflections(brain.localBrainPath);
        const needs = computeNeeds(records);
        const inventory = knowledgeInventory(records);
        const debt = computeKnowledgeDebt(records);

        const queue = mergeNeedsIntoQueue(loadQueue(brain.localBrainPath), needs, new Date().toISOString());
        saveQueue(brain.localBrainPath, queue);

        // 사람이 읽는 요약 md — Need 근거 + Inventory + Debt + 큐 현황.
        const md = [formatNeedsMarkdown(needs, inventory, debt), formatQueueMarkdown(queue)].join('\n---\n\n');
        const reportPath = path.join(brain.localBrainPath, '.astra', 'growth', 'learning-needs.md');
        fs.mkdirSync(path.dirname(reportPath), { recursive: true });
        fs.writeFileSync(reportPath, md, 'utf8');

        const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(reportPath));
        await vscode.window.showTextDocument(doc, { preview: false });
        const proposed = queue.filter((q) => q.status === 'proposed').length;
        vscode.window.showInformationMessage(
            records.length === 0
                ? '아직 Reflection 기록이 없습니다 — 업무 turn 이 쌓이면 학습 우선순위가 산출됩니다.'
                : `학습 큐 갱신 완료 · 제안 ${proposed}건 (승인은 ${LEARNING_QUEUE_REL_PATH} 에서 status 를 approved 로).`,
        );
    } catch (err: any) {
        logError('Learning queue command failed.', { error: err?.message || String(err) });
        vscode.window.showErrorMessage(`학습 큐 갱신 실패: ${err?.message ?? err}`);
    }
}

/**
 * 지식 노후 점검 (Phase 4 / Track 4-3) — 두뇌 전체 파일의 mtime 을 분야별 반감기로
 * 감쇠 평가, 노후 지식 보고서를 연다. v1 은 보고만 — 자동 이동/삭제 없음 (Human Override).
 */
async function decayAuditCommand(): Promise<void> {
    try {
        const brain = getActiveBrainProfile();
        if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) {
            vscode.window.showErrorMessage('활성 두뇌 폴더를 찾을 수 없습니다.');
            return;
        }
        const allFiles = findBrainFiles(brain.localBrainPath);
        const entries: Array<{ relPath: string; lastUpdated: number }> = [];
        for (const f of allFiles) {
            try {
                const abs = path.isAbsolute(f) ? f : path.join(brain.localBrainPath, f);
                const st = fs.statSync(abs);
                entries.push({ relPath: path.relative(brain.localBrainPath, abs) || f, lastUpdated: st.mtimeMs });
            } catch { /* 파일 사라짐 등 — skip */ }
        }
        const items = auditKnowledgeDecay(entries);
        const md = formatDecayReport(items, { brainName: brain.name, dateStr: new Date().toLocaleString() });
        const reportPath = path.join(brain.localBrainPath, '.astra', 'growth', 'decay-report.md');
        fs.mkdirSync(path.dirname(reportPath), { recursive: true });
        fs.writeFileSync(reportPath, md, 'utf8');
        const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(reportPath));
        await vscode.window.showTextDocument(doc, { preview: false });
        const stale = items.filter((i) => i.status === 'stale').length;
        vscode.window.showInformationMessage(`지식 노후 점검 완료 · ${entries.length}개 파일 중 노후 ${stale}개.`);
    } catch (err: any) {
        logError('Decay audit command failed.', { error: err?.message || String(err) });
        vscode.window.showErrorMessage(`지식 노후 점검 실패: ${err?.message ?? err}`);
    }
}

/**
 * 학습 실행 (Phase 6 / Track 7-1, Research Agent) — Learning Queue 의 *approved* 항목을
 * 조사 패키지(브리프 + 내부 현황 + 추정 라벨 초안 + Validation 판정)로 만들어
 * proposals/ 에 저장하고 상태를 in-progress 로 바꾼다. 두뇌 본문 자동 저장 없음 —
 * 사람이 외부 근거로 보강·승인해야 지식이 된다 (Permission Based Learning).
 */
async function researchRunQueueCommand(): Promise<void> {
    try {
        const brain = getActiveBrainProfile();
        if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) {
            vscode.window.showErrorMessage('활성 두뇌 폴더를 찾을 수 없습니다.');
            return;
        }
        const config = getConfig();
        const model = config.defaultModel;
        if (!model || !config.ollamaUrl) {
            vscode.window.showErrorMessage('모델/엔진 설정이 없습니다 (defaultModel, ollamaUrl).');
            return;
        }
        const queue = loadQueue(brain.localBrainPath);
        const approved = queue.filter((q) => q.status === 'approved');
        if (approved.length === 0) {
            vscode.window.showInformationMessage(
                `승인된 학습 항목이 없습니다 — ${LEARNING_QUEUE_REL_PATH} 에서 status 를 approved 로 바꾼 뒤 다시 실행하세요.`,
            );
            return;
        }

        await vscode.window.withProgress(
            { location: vscode.ProgressLocation.Notification, title: 'Astra 학습 실행 (Research Agent)', cancellable: true },
            async (progress, token) => {
                const orchestrator = new RetrievalOrchestrator();
                const allFiles = findBrainFiles(brain.localBrainPath);
                getBrainTokenIndex(brain.localBrainPath, allFiles);

                const fetchInternalRefs = async (topic: string): Promise<ExistingKnowledgeRef[]> => {
                    const ranked = orchestrator.rankBrainForEval(topic, brain, { limit: 5 }).slice(0, 5);
                    const refs: ExistingKnowledgeRef[] = [];
                    for (const r of ranked) {
                        try {
                            const abs = path.join(brain.localBrainPath, r.relativePath);
                            const content = fs.readFileSync(abs, 'utf8').slice(0, 2000);
                            const st = fs.statSync(abs);
                            refs.push({ title: path.basename(r.relativePath), content, lastUpdated: st.mtimeMs, filePath: r.relativePath });
                        } catch { /* skip unreadable */ }
                    }
                    return refs;
                };

                let done = 0;
                const proposalsDir = path.join(brain.localBrainPath, '.astra', 'growth', 'proposals');
                fs.mkdirSync(proposalsDir, { recursive: true });
                const proposalPaths: string[] = [];
                for (const item of approved) {
                    if (token.isCancellationRequested) break;
                    progress.report({ message: `${++done}/${approved.length} — ${item.topic}` });
                    const pkg = await runResearch({
                        item,
                        fetchInternalRefs,
                        callLlm: (system, user, maxTokens) => simpleChatCompletion(system, user, {
                            baseUrl: config.ollamaUrl, model, temperature: 0.3, maxTokens, timeoutMs: 180000,
                        }),
                        nowIso: new Date().toISOString(),
                    });
                    const md = formatProposalMarkdown(pkg, { dateStr: new Date().toLocaleString(), modelName: model });
                    const filePath = path.join(proposalsDir, `${item.id}.md`);
                    fs.writeFileSync(filePath, md, 'utf8');
                    proposalPaths.push(filePath);
                    item.status = 'in-progress';
                    item.updatedAt = new Date().toISOString();
                }
                saveQueue(brain.localBrainPath, queue);
                logInfo('Research agent run complete.', { processed: proposalPaths.length });

                if (proposalPaths.length > 0) {
                    const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(proposalPaths[0]));
                    await vscode.window.showTextDocument(doc, { preview: false });
                }
                vscode.window.showInformationMessage(
                    `학습 제안 ${proposalPaths.length}건 생성 (.astra/growth/proposals/). 외부 근거로 보강 후 두뇌에 저장하고 큐 상태를 done 으로 바꾸세요.`,
                );
            },
        );
    } catch (err: any) {
        logError('Research run 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;
    }
}
