/**
 * 주간 성장 사이클 워처 — Self-Evolving OS 의 폐루프 자동화.
 *
 * 기존엔 평가·학습큐·노후점검·학습실행이 전부 수동 명령이었다. 이 워처가 매주
 * (기본 일요일 20:00 KST) 자동으로:
 *   1. 검색 평가 — 골든셋 recall@k/MRR (주간 추이 데이터, TF-IDF 경로)
 *   2. 학습 큐 갱신 — Reflection → Need Engine → proposed 항목 병합
 *   3. 지식 노후 점검 — 분야별 반감기 감쇠 평가
 *   4. 성장 리포트 — Reflection 추이 + 스킬 점수 + 성공 패턴
 *   5. 승인된 학습 실행 — approved 큐 항목을 Research Agent 로 조사 패키지화
 *      (사이클당 최대 3건 — LLM 시간 상한. 승인 자체는 여전히 사람: Permission
 *       Based Learning 유지 — 자동화되는 건 '승인된 것의 실행'뿐)
 *   6. 요약 통지 — VS Code 알림 + (연결 시) 텔레그램
 *
 * 측정 → 부족 식별 → (사람 승인) → 학습 실행 → 재측정이 사람 손 없이 돈다.
 * 모든 단계는 독립 try/catch — 한 단계 실패가 사이클을 멈추지 않는다.
 * 결과물은 기존 수동 명령과 동일 위치(.astra/eval/, .astra/growth/)에 저장 —
 * 수동 명령과 완전 호환.
 */
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { getConfig } from '../../config';
import { findBrainFiles, getActiveBrainProfile, logError, logInfo } from '../../utils';
import { RetrievalOrchestrator } from '../../retrieval';
import { getBrainTokenIndex } from '../../retrieval/brainIndex';
import { loadGoldenSet, runRetrievalEval, formatReportMarkdown } from '../../retrieval/evalHarness';
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';
import { simpleChatCompletion } from '../../intelligence/llmCall';
import {
    loadCorrectionCases, computeWeaknessProfile, saveWeaknessProfile,
    runRegressionCase, formatRegressionReport,
} from '../../intelligence/correctionLoop';
import { TelegramHttpClient } from '../../integrations/telegram/telegramClient';
import { TELEGRAM_TOKEN_SECRET_KEY } from '../../extension/telegramCommands';

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

let _timer: NodeJS.Timeout | undefined;
let _disposed = false;
let _lastFiredYmd = '';

function nowInKst(): { hour: number; minute: number; ymd: string; weekday: number } {
    const parts = new Intl.DateTimeFormat('en-US', {
        timeZone: 'Asia/Seoul',
        year: 'numeric', month: '2-digit', day: '2-digit',
        hour: '2-digit', minute: '2-digit', hour12: false,
    }).formatToParts(new Date());
    const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
    const ymd = `${get('year')}-${get('month')}-${get('day')}`;
    return { hour: Number(get('hour')), minute: Number(get('minute')), ymd, weekday: new Date(`${ymd}T00:00:00Z`).getUTCDay() };
}

function cycleSchedule(): { day: number; hour: number; minute: number } {
    const cfg = vscode.workspace.getConfiguration('g1nation');
    const day = Math.min(6, Math.max(0, cfg.get<number>('growthCycle.day', 0)));
    const raw = (cfg.get<string>('growthCycle.time', '20:00') || '20:00').trim();
    const m = raw.match(/^(\d{1,2}):(\d{2})$/);
    return { day, hour: m ? Math.min(23, Number(m[1])) : 20, minute: m ? Math.min(59, Number(m[2])) : 0 };
}

function msUntilNextFire(): number {
    const { hour, minute, ymd, weekday } = nowInKst();
    const sch = cycleSchedule();
    const nowMin = hour * 60 + minute;
    const targetMin = sch.hour * 60 + sch.minute;
    let daysAhead = (sch.day - weekday + 7) % 7;
    if (daysAhead === 0 && (targetMin <= nowMin || _lastFiredYmd === ymd)) daysAhead = 7;
    return (daysAhead * 24 * 60 + targetMin - nowMin) * 60_000;
}

/** 사이클 1회 실행 — 단계별 독립 실패 허용, 마지막에 요약 통지. */
export async function runGrowthCycleOnce(context: vscode.ExtensionContext): Promise<string> {
    const brain = getActiveBrainProfile();
    if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) {
        return '두뇌 폴더 없음 — 사이클 건너뜀';
    }
    const config = getConfig();
    const growthDir = path.join(brain.localBrainPath, '.astra', 'growth');
    fs.mkdirSync(growthDir, { recursive: true });
    const summary: string[] = [];
    const now = new Date();

    // (1) 검색 평가 — 골든셋이 있을 때만. TF-IDF 경로(임베딩 backfill 은 무겁고 선택적).
    try {
        const { entries } = loadGoldenSet(brain.localBrainPath);
        if (entries.length > 0) {
            const allFiles = findBrainFiles(brain.localBrainPath);
            getBrainTokenIndex(brain.localBrainPath, allFiles);
            const orchestrator = new RetrievalOrchestrator();
            const report = await runRetrievalEval({
                entries, ks: EVAL_KS,
                ranker: async (q) => orchestrator
                    .rankBrainForEval(q, brain, {
                        limit: Math.max(...EVAL_KS) + 5,
                        chunkLevelRetrieval: config.chunkLevelRetrieval === true,
                        chunkTargetChars: config.chunkTargetChars,
                    })
                    .map(r => r.relativePath),
            });
            const md = formatReportMarkdown(report, {
                brainName: brain.name, dateStr: now.toLocaleString(),
                embeddingModel: '', alpha: 0, notes: '주간 자동 사이클 (TF-IDF 경로)',
            });
            const stamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
            fs.writeFileSync(path.join(brain.localBrainPath, '.astra', 'eval', `report-${stamp}-auto.md`), md, 'utf8');
            summary.push(`검색 recall@1 ${(report.recallAtK[1] * 100).toFixed(0)}% · MRR ${report.mrr.toFixed(2)}`);

            // [회귀 경보] 직전 사이클 지표와 비교 — 성장(지식 추가)이 검색 품질을
            // 퇴보시키지 않았는지 감시. 하락 시 그 기간에 추가된 문서를 용의자로 제시.
            // 자동 롤백은 하지 않는다 (지식은 git 에 있고 판단은 사람 — 승인 원칙).
            try {
                const histFile = path.join(brain.localBrainPath, '.astra', 'eval', 'metrics-history.jsonl');
                let prev: { ts: string; recall1: number; mrr: number } | null = null;
                if (fs.existsSync(histFile)) {
                    const rows = fs.readFileSync(histFile, 'utf8').trim().split('\n').filter(Boolean);
                    if (rows.length > 0) { try { prev = JSON.parse(rows[rows.length - 1]); } catch { /* skip */ } }
                }
                const cur = { ts: now.toISOString(), recall1: report.recallAtK[1], recall3: report.recallAtK[3], mrr: report.mrr, queries: entries.length };
                fs.appendFileSync(histFile, JSON.stringify(cur) + '\n', 'utf8');
                const R1_DROP = 0.10, MRR_DROP = 0.08;
                if (prev && (prev.recall1 - cur.recall1 >= R1_DROP || prev.mrr - cur.mrr >= MRR_DROP)) {
                    const prevTs = Date.parse(prev.ts) || 0;
                    const suspects = allFiles
                        .map(f => { try { return { f, m: fs.statSync(f).mtimeMs }; } catch { return null; } })
                        .filter((x): x is { f: string; m: number } => !!x && x.m > prevTs)
                        .sort((a, b) => b.m - a.m).slice(0, 10)
                        .map(x => path.relative(brain.localBrainPath, x.f));
                    const alert = [
                        `# ⚠️ 검색 회귀 감지 — ${now.toLocaleString()}`, '',
                        `recall@1 ${(prev.recall1 * 100).toFixed(0)}% → ${(cur.recall1 * 100).toFixed(0)}% · MRR ${prev.mrr.toFixed(2)} → ${cur.mrr.toFixed(2)}`,
                        '', `## 직전 사이클 이후 추가·변경된 문서 (용의자 ${suspects.length}건)`,
                        ...suspects.map(s => `- ${s}`),
                        '', '조치: 용의자 문서의 중복/제목 충돌 여부 확인. 복구는 git 으로 (자동 롤백 안 함 — 판단은 사람).',
                    ].join('\n');
                    fs.writeFileSync(path.join(growthDir, 'regression-alert.md'), alert, 'utf8');
                    summary.push(`⚠️ 검색 회귀 (recall@1 ${(prev.recall1 * 100).toFixed(0)}%→${(cur.recall1 * 100).toFixed(0)}%) — regression-alert.md 확인`);
                }
            } catch (e: any) { logError('성장 사이클: 회귀 비교 실패.', { error: e?.message ?? String(e) }); }
        }
    } catch (e: any) { logError('성장 사이클: 검색 평가 실패.', { error: e?.message ?? String(e) }); }

    // (1.5) Correction Loop — 정정 회귀 테스트 + 약점 프로필 갱신.
    //  a. 약점 프로필: 최근 60일 정정 태그 통계 → weakness-profile.json
    //     (memoryContext 가 다음 턴부터 자기검토 블록으로 주입 — 통계가 행동을 바꾼다)
    //  b. 회귀: 최근 정정받은 질문을 두뇌 검색 컨텍스트와 함께 다시 풀어
    //     "같은 실수 반복?" 을 LLM-judge 로 판정 — 정정이 곧 Ground Truth.
    try {
        const cases = loadCorrectionCases(brain.localBrainPath);
        if (cases.length > 0) {
            saveWeaknessProfile(brain.localBrainPath, computeWeaknessProfile(cases, now.getTime()));
            if (config.defaultModel && config.ollamaUrl) {
                const MAX_REGRESSION_PER_CYCLE = 8;
                const recent = cases.slice(-MAX_REGRESSION_PER_CYCLE);
                const orchestrator = new RetrievalOrchestrator();
                const allFiles = findBrainFiles(brain.localBrainPath);
                getBrainTokenIndex(brain.localBrainPath, allFiles);
                const llm = { baseUrl: config.ollamaUrl, model: config.defaultModel };
                const answerFn = async (question: string): Promise<string> => {
                    // 실제 채팅과 동일하게 두뇌 근거를 주고 답하게 한다 (검색 없는 맨몸 답변은
                    // 회귀 판정이 아니라 모델 암기 테스트가 되어버림).
                    const refs = orchestrator.rankBrainForEval(question, brain, {
                        limit: 5, chunkLevelRetrieval: config.chunkLevelRetrieval === true, chunkTargetChars: config.chunkTargetChars,
                    }).slice(0, 5).map(r => {
                        try { return `[${r.relativePath}]\n${fs.readFileSync(path.join(brain.localBrainPath, r.relativePath), 'utf8').slice(0, 1500)}`; }
                        catch { return ''; }
                    }).filter(Boolean).join('\n\n');
                    return simpleChatCompletion(
                        '두뇌 발췌를 근거로 간결히 답하라. 근거 없는 내용은 추정임을 명시하라.',
                        `[두뇌 발췌]\n${refs || '(없음)'}\n\n[질문] ${question}`,
                        { ...llm, temperature: 0.2, maxTokens: 700, timeoutMs: 120000 },
                    );
                };
                const results = [];
                for (const c of recent) results.push(await runRegressionCase(c, answerFn, llm));
                fs.writeFileSync(
                    path.join(growthDir, 'regression-report.md'),
                    formatRegressionReport(results, { dateStr: now.toLocaleString() }), 'utf8',
                );
                const repeated = results.filter(r => r.repeated === true).length;
                summary.push(`정정 회귀 ${results.length}건 중 재발 ${repeated}건${repeated > 0 ? ' ⚠️' : ''}`);
            }
        }
    } catch (e: any) { logError('성장 사이클: 정정 회귀/약점 프로필 실패.', { error: e?.message ?? String(e) }); }

    // (2) 학습 큐 갱신 (Need Engine)
    let proposedCount = 0;
    try {
        const records = loadReflections(brain.localBrainPath);
        const needs = computeNeeds(records);
        const queue = mergeNeedsIntoQueue(loadQueue(brain.localBrainPath), needs, now.toISOString());
        saveQueue(brain.localBrainPath, queue);
        const md = [formatNeedsMarkdown(needs, knowledgeInventory(records), computeKnowledgeDebt(records)), formatQueueMarkdown(queue)].join('\n---\n\n');
        fs.writeFileSync(path.join(growthDir, 'learning-needs.md'), md, 'utf8');
        proposedCount = queue.filter(q => q.status === 'proposed').length;
        summary.push(`학습 제안 ${proposedCount}건 대기`);
    } catch (e: any) { logError('성장 사이클: 학습 큐 갱신 실패.', { error: e?.message ?? String(e) }); }

    // (3) 지식 노후 점검
    try {
        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);
                entries.push({ relPath: path.relative(brain.localBrainPath, abs) || f, lastUpdated: fs.statSync(abs).mtimeMs });
            } catch { /* skip */ }
        }
        const items = auditKnowledgeDecay(entries);
        fs.writeFileSync(path.join(growthDir, 'decay-report.md'), formatDecayReport(items, { brainName: brain.name, dateStr: now.toLocaleString() }), 'utf8');
        const stale = items.filter(i => i.status === 'stale').length;
        if (stale > 0) summary.push(`노후 지식 ${stale}건`);
    } catch (e: any) { logError('성장 사이클: 노후 점검 실패.', { error: e?.message ?? String(e) }); }

    // (4) 성장 리포트
    try {
        const records = loadReflections(brain.localBrainPath);
        const md = [
            formatGrowthReport(records),
            formatSkillScoresMarkdown(computeSkillScores(records)),
            formatSuccessPatternsMarkdown(loadSuccessPatterns(brain.localBrainPath)),
        ].join('\n\n');
        fs.writeFileSync(path.join(growthDir, 'growth-report.md'), md, 'utf8');
    } catch (e: any) { logError('성장 사이클: 성장 리포트 실패.', { error: e?.message ?? String(e) }); }

    // (5) 승인된 학습 자동 실행 — 사람이 approved 로 바꾼 항목만 (Permission Based Learning 유지)
    try {
        const autoRun = vscode.workspace.getConfiguration('g1nation').get<boolean>('growthCycle.autoRunApproved', true);
        if (autoRun && config.defaultModel && config.ollamaUrl) {
            const queue = loadQueue(brain.localBrainPath);
            const approved = queue.filter(q => q.status === 'approved').slice(0, MAX_RESEARCH_PER_CYCLE);
            if (approved.length > 0) {
                const orchestrator = new RetrievalOrchestrator();
                const allFiles = findBrainFiles(brain.localBrainPath);
                getBrainTokenIndex(brain.localBrainPath, allFiles);
                const fetchInternalRefs = async (topic: string): Promise<ExistingKnowledgeRef[]> => {
                    const refs: ExistingKnowledgeRef[] = [];
                    for (const r of orchestrator.rankBrainForEval(topic, brain, { limit: 5 }).slice(0, 5)) {
                        try {
                            const abs = path.join(brain.localBrainPath, r.relativePath);
                            refs.push({ title: path.basename(r.relativePath), content: fs.readFileSync(abs, 'utf8').slice(0, 2000), lastUpdated: fs.statSync(abs).mtimeMs, filePath: r.relativePath });
                        } catch { /* skip */ }
                    }
                    return refs;
                };
                const proposalsDir = path.join(growthDir, 'proposals');
                fs.mkdirSync(proposalsDir, { recursive: true });
                let ran = 0;
                for (const item of approved) {
                    const pkg = await runResearch({
                        item, fetchInternalRefs,
                        callLlm: (system, user, maxTokens) => simpleChatCompletion(system, user, {
                            baseUrl: config.ollamaUrl, model: config.defaultModel, temperature: 0.3, maxTokens, timeoutMs: 180000,
                        }),
                        nowIso: new Date().toISOString(),
                    });
                    fs.writeFileSync(path.join(proposalsDir, `${item.id}.md`), formatProposalMarkdown(pkg, { dateStr: new Date().toLocaleString(), modelName: config.defaultModel }), 'utf8');
                    item.status = 'in-progress';
                    item.updatedAt = new Date().toISOString();
                    ran++;
                }
                saveQueue(brain.localBrainPath, queue);
                summary.push(`승인 학습 ${ran}건 실행 → proposals/`);
            }
        }
    } catch (e: any) { logError('성장 사이클: 학습 실행 실패.', { error: e?.message ?? String(e) }); }

    const text = summary.length ? summary.join(' · ') : '변화 없음 (Reflection/골든셋 데이터 대기)';
    logInfo('주간 성장 사이클 완료.', { summary: text });

    // (6) 통지 — VS Code + (연결 시) 텔레그램
    const note = `🌱 Astra 주간 성장 사이클 — ${text}`
        + (proposedCount > 0 ? `\n승인하려면 ${LEARNING_QUEUE_REL_PATH} 에서 status 를 approved 로 바꾸세요.` : '');
    void vscode.window.showInformationMessage(note.replace(/\n/g, ' '));
    try {
        const allowed = vscode.workspace.getConfiguration('g1nation').get<number[]>('telegram.allowedChatIds', []) || [];
        const token = (await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '';
        if (allowed.length && token.trim()) {
            const client = new TelegramHttpClient({ getToken: () => token });
            await client.sendMessage({ chatId: allowed[0], text: note });
        }
    } catch (e: any) { logInfo('성장 사이클 텔레그램 통지 실패 (무시).', { error: e?.message ?? String(e) }); }

    return text;
}

function scheduleNext(context: vscode.ExtensionContext): void {
    if (_disposed) return;
    const ms = msUntilNextFire();
    logInfo('주간 성장 사이클 예약.', { inHours: (ms / 3_600_000).toFixed(1) });
    _timer = setTimeout(async () => {
        const enabled = vscode.workspace.getConfiguration('g1nation').get<boolean>('growthCycle.enabled', true);
        if (enabled) {
            _lastFiredYmd = nowInKst().ymd;
            try { await runGrowthCycleOnce(context); } catch (e: any) {
                logError('주간 성장 사이클 실패.', { error: e?.message ?? String(e) });
            }
        }
        scheduleNext(context);
    }, ms);
}

/** VS Code 시작 시 호출 — disposable 반환. */
export function startGrowthCycleWatcher(context: vscode.ExtensionContext): vscode.Disposable {
    _disposed = false;
    scheduleNext(context);
    return new vscode.Disposable(() => {
        _disposed = true;
        if (_timer) { clearTimeout(_timer); _timer = undefined; }
    });
}
