/**
 * Correction Loop — 사용자 정정 1회가 시스템을 세 군데서 성장시키는 단일 파이프라인.
 *
 *   사용자 정정 ("아니야, 그거 6월이야")
 *     ① 감지(looksLikeCorrection) + LLM 오류 분류(classifyCorrection)
 *     ├→ 태깅된 레슨 저장 (lessons/ — error-tag frontmatter)
 *     └→ 회귀 케이스 적립 (.astra/eval/corrections.jsonl — {질문, 틀린답, 정정})
 *     ② 주간 성장 사이클이 회귀 재검사("같은 실수 반복?") + 태그 통계 → 약점 프로필
 *     ③ 약점 프로필(.astra/growth/weakness-profile.json)이 시스템 프롬프트에 자동 주입
 *
 * 설계 원칙:
 *  - 정답지를 사람이 만들지 않는다 — 정정 자체가 Ground Truth.
 *  - 통찰→행동 경로가 기계적이다 — 태그 통계가 리포트에 머물지 않고 다음 턴 프롬프트를 바꾼다.
 *  - 투명성 — 프로필·케이스 모두 사람이 열어 수정/삭제 가능한 파일 (Permission Based Learning).
 *  - 캡처는 fire-and-forget — 정정 턴의 응답 속도에 영향 없음.
 */
import * as fs from 'fs';
import * as path from 'path';
import { simpleChatCompletion } from './llmCall';
import { loadQueue, saveQueue } from './learningQueue';
import { linkRelatedLessons } from './lessonNetwork';

// ── 타입/상수 ────────────────────────────────────────────────────────────────

export const ERROR_TAGS = ['사실오류', '근거누락', '맥락누락', '추론오류', '지시불이행', '형식오류'] as const;
export type ErrorTag = typeof ERROR_TAGS[number] | '기타';

export interface CorrectionCase {
    ts: string;             // ISO
    errorTag: ErrorTag;
    /** 틀린 답을 만들었던 사용자 질문. */
    question: string;
    /** 틀린 답 발췌 (저장 비용 제한). */
    wrongAnswer: string;
    /** 사용자의 정정 발화 — Ground Truth. */
    correction: string;
    /** 분류 LLM 이 뽑은 한 줄 요지 (레슨 제목 겸용). */
    title: string;
}

export const CORRECTIONS_REL_PATH = path.join('.astra', 'eval', 'corrections.jsonl');
export const WEAKNESS_PROFILE_REL_PATH = path.join('.astra', 'growth', 'weakness-profile.json');

const MAX_FIELD_CHARS = 600;

// ── ① 정정 감지 (보수적 — 오탐은 레슨 노이즈가 되므로 명확한 신호만) ─────────

const CORRECTION_HEAD_RE = /^\s*(아니야|아니지|아닌데|틀렸|그게\s*아니|잘못\s*(알|됐|했)|땡|노노)/;
const CORRECTION_ANY_RE = /(틀렸(어|네|잖|다)|사실(이|과)\s*(아니|다르|달라|다름)|정정(해|할게|하자)|잘못\s*된\s*정보|가\s*아니라\s*|이\s*아니라\s*|착각(했|하)|헛소리|지어내)/;

/**
 * 사용자 발화가 직전 답변에 대한 *정정*인지. 짧은 명령("아니 그거 말고 이거 해줘")과
 * 구분하기 위해 직전 assistant 답변 존재는 호출부가 보장한다.
 */
export function looksLikeCorrection(prompt: string): boolean {
    const t = (prompt || '').trim();
    if (t.length < 4 || t.length > 1500) return false;
    return CORRECTION_HEAD_RE.test(t) || CORRECTION_ANY_RE.test(t);
}

// ── ① 오류 분류 (LLM, 실패 시 휴리스틱 fallback) ────────────────────────────

const CLASSIFY_SYSTEM = [
    '너는 AI 답변 오류 분류기다. 사용자가 AI 답변을 정정했다. 오류 유형을 하나만 고르고 한 줄 요지를 써라.',
    '유형: 사실오류(틀린 사실/수치/날짜), 근거누락(출처 없이 단정), 맥락누락(대화/문서 맥락 놓침), 추론오류(논리 비약), 지시불이행(요구사항 무시), 형식오류(형식/언어 문제)',
    '반드시 JSON 한 줄만 출력: {"tag":"<유형>","title":"<요지 40자 이내>"}',
].join('\n');

export async function classifyCorrection(
    question: string,
    wrongAnswer: string,
    correction: string,
    llm: { baseUrl: string; model: string },
): Promise<{ tag: ErrorTag; title: string }> {
    const fallback = (): { tag: ErrorTag; title: string } => ({
        tag: /(출처|근거|소스)/.test(correction) ? '근거누락'
            : /(아까|위에|전에|문서|말했)/.test(correction) ? '맥락누락'
            : '사실오류',
        title: correction.slice(0, 40).replace(/\n/g, ' '),
    });
    try {
        const user = [
            `[질문] ${question.slice(0, MAX_FIELD_CHARS)}`,
            `[AI 답변 발췌] ${wrongAnswer.slice(0, MAX_FIELD_CHARS)}`,
            `[사용자 정정] ${correction.slice(0, MAX_FIELD_CHARS)}`,
        ].join('\n');
        const raw = await simpleChatCompletion(CLASSIFY_SYSTEM, user, {
            baseUrl: llm.baseUrl, model: llm.model, temperature: 0.1, maxTokens: 120, timeoutMs: 30000,
        });
        const m = raw.match(/\{[\s\S]*?\}/);
        if (!m) return fallback();
        const parsed = JSON.parse(m[0]);
        const tag = (ERROR_TAGS as readonly string[]).includes(parsed?.tag) ? parsed.tag as ErrorTag : '기타';
        const title = String(parsed?.title || '').trim().slice(0, 60) || fallback().title;
        return { tag, title };
    } catch {
        return fallback();
    }
}

// ── ① 영속화: 회귀 케이스 + 태깅 레슨 ────────────────────────────────────────

export function appendCorrectionCase(brainPath: string, c: CorrectionCase): boolean {
    try {
        const file = path.join(brainPath, CORRECTIONS_REL_PATH);
        fs.mkdirSync(path.dirname(file), { recursive: true });
        const row = {
            ...c,
            question: c.question.slice(0, MAX_FIELD_CHARS),
            wrongAnswer: c.wrongAnswer.slice(0, MAX_FIELD_CHARS),
            correction: c.correction.slice(0, MAX_FIELD_CHARS),
        };
        fs.appendFileSync(file, JSON.stringify(row) + '\n', 'utf8');
        return true;
    } catch {
        return false;
    }
}

export function loadCorrectionCases(brainPath: string, limit = 200): CorrectionCase[] {
    try {
        const file = path.join(brainPath, CORRECTIONS_REL_PATH);
        if (!fs.existsSync(file)) return [];
        const out: CorrectionCase[] = [];
        for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
            const t = line.trim();
            if (!t || t.startsWith('//')) continue;
            try {
                const o = JSON.parse(t);
                if (o && typeof o.question === 'string' && typeof o.correction === 'string') out.push(o);
            } catch { /* skip bad line */ }
        }
        return out.slice(-limit);
    } catch {
        return [];
    }
}

/** 정정 레슨 카드 — 기존 lessons/ 템플릿과 같은 구조 + error-tag frontmatter. */
export function correctionLessonMarkdown(c: CorrectionCase, today: string): string {
    const safeTitle = c.title.replace(/\n/g, ' ').trim() || '사용자 정정';
    return [
        '---',
        'type: lesson',
        `title: ${safeTitle}`,
        `error-tag: ${c.errorTag}`,
        'applies-to: []',
        'severity: medium',
        'source: user-correction',
        'occurrences: 1',
        `last-seen: ${today}`,
        '---',
        '',
        `# Lesson: ${safeTitle}`,
        '',
        '## Situation',
        `사용자 질문: ${c.question}`,
        '',
        '## Mistake / Risk',
        `[${c.errorTag}] AI 답변: ${c.wrongAnswer}`,
        '',
        '## Fix',
        `사용자 정정 (Ground Truth): ${c.correction}`,
        '',
        '## Prevention Checklist',
        `- 같은 질문 유형에서 [${c.errorTag}] 재발 여부 확인 — 주간 회귀 테스트 대상`,
        '',
    ].join('\n');
}

/**
 * 정정 1건 캡처 — 분류 → 레슨 저장 + 회귀 케이스 적립. fire-and-forget 용
 * (실패는 로그 대상이지 사용자 턴을 막지 않는다). 저장된 레슨 경로 또는 null 반환.
 */
export async function captureCorrection(opts: {
    brainPath: string;
    question: string;
    wrongAnswer: string;
    correction: string;
    llm: { baseUrl: string; model: string };
}): Promise<string | null> {
    const { tag, title } = await classifyCorrection(opts.question, opts.wrongAnswer, opts.correction, opts.llm);
    const now = new Date();
    const c: CorrectionCase = {
        ts: now.toISOString(),
        errorTag: tag,
        question: opts.question,
        wrongAnswer: opts.wrongAnswer,
        correction: opts.correction,
        title,
    };
    appendCorrectionCase(opts.brainPath, c);
    try {
        const dir = path.join(opts.brainPath, 'lessons');
        fs.mkdirSync(dir, { recursive: true });
        const ymd = now.toISOString().slice(0, 10);
        const slug = title.toLowerCase().replace(/[^a-z0-9가-힣]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 50) || 'correction';
        const file = path.join(dir, `${ymd}-correction-${slug}.md`);
        fs.writeFileSync(file, correctionLessonMarkdown(c, ymd), 'utf8');
        // A-MEM 레슨 네트워크 — 비슷한 과거 교훈과 상호 링크 (LLM 호출 없음, 실패 무해).
        try { linkRelatedLessons(opts.brainPath, file); } catch { /* 링크 실패가 캡처를 막지 않음 */ }
        return file;
    } catch {
        return null;
    }
}

// ── ②③ 약점 프로필 — 태그 통계를 다음 턴의 행동으로 변환 ────────────────────

export interface WeaknessProfile {
    updatedAt: string;
    totalCases: number;
    /** 최근 윈도우의 태그별 건수 (내림차순). */
    tagCounts: Array<{ tag: string; count: number; example: string }>;
}

/** 최근 windowDays 의 정정 케이스에서 약점 프로필 산출 (성장 사이클이 주간 호출). */
export function computeWeaknessProfile(cases: CorrectionCase[], nowMs: number, windowDays = 60): WeaknessProfile {
    const cutoff = nowMs - windowDays * 86_400_000;
    const recent = cases.filter(c => {
        const t = Date.parse(c.ts);
        return Number.isFinite(t) && t >= cutoff;
    });
    const byTag = new Map<string, { count: number; example: string }>();
    for (const c of recent) {
        const cur = byTag.get(c.errorTag) || { count: 0, example: '' };
        cur.count++;
        cur.example = c.title; // 최신 케이스 제목을 예시로
        byTag.set(c.errorTag, cur);
    }
    return {
        updatedAt: new Date(nowMs).toISOString(),
        totalCases: recent.length,
        tagCounts: Array.from(byTag.entries())
            .map(([tag, v]) => ({ tag, count: v.count, example: v.example }))
            .sort((a, b) => b.count - a.count),
    };
}

export function saveWeaknessProfile(brainPath: string, profile: WeaknessProfile): boolean {
    try {
        const file = path.join(brainPath, WEAKNESS_PROFILE_REL_PATH);
        fs.mkdirSync(path.dirname(file), { recursive: true });
        fs.writeFileSync(file, JSON.stringify(profile, null, 2) + '\n', 'utf8');
        return true;
    } catch {
        return false;
    }
}

export function loadWeaknessProfile(brainPath: string): WeaknessProfile | null {
    try {
        const file = path.join(brainPath, WEAKNESS_PROFILE_REL_PATH);
        if (!fs.existsSync(file)) return null;
        const o = JSON.parse(fs.readFileSync(file, 'utf8'));
        return o && Array.isArray(o.tagCounts) ? o as WeaknessProfile : null;
    } catch {
        return null;
    }
}

/**
 * 약점 프로필 → 시스템 프롬프트 자기검토 블록. 같은 태그 2회 이상일 때만 주입
 * (1회성 실수로 프롬프트를 어지럽히지 않게). 프로필 없으면 ''.
 */
export function buildSelfReviewBlock(profile: WeaknessProfile | null): string {
    if (!profile) return '';
    const significant = profile.tagCounts.filter(t => t.count >= 2).slice(0, 2);
    if (significant.length === 0) return '';
    const lines = ['[자기검토 — 최근 정정 통계 기반]'];
    for (const t of significant) {
        lines.push(`- 너는 최근 "${t.tag}" 정정을 ${t.count}회 받았다 (예: ${t.example}). ${SELF_CHECK_BY_TAG[t.tag] || '같은 유형의 실수가 없는지 답하기 전 재확인하라.'}`);
    }
    return lines.join('\n');
}

const SELF_CHECK_BY_TAG: Record<string, string> = {
    '사실오류': '수치·날짜·고유명사는 두뇌 근거가 없으면 단정하지 말고 "확인 필요"로 표시하라.',
    '근거누락': '주장마다 근거 문서를 인용하고, 인용할 수 없으면 추정임을 명시하라.',
    '맥락누락': '답하기 전 직전 대화와 제공된 문서에서 관련 맥락을 다시 확인하라.',
    '추론오류': '결론 전에 추론 단계를 명시적으로 나열하고 비약이 없는지 점검하라.',
    '지시불이행': '답하기 전 사용자 요구사항 목록을 만들고 각각 충족했는지 확인하라.',
    '형식오류': '요구된 출력 형식(언어·구조·길이)을 답변 전에 재확인하라.',
};

// ── ③ 지식 공백 → 학습 큐 자동 proposed 등록 (Need Engine 연결) ──────────────

/**
 * GROUNDING 약함으로 판정된 질문을 학습 큐에 proposed 로 등록한다. 같은 질문은
 * 1회만 (해시 id 중복 차단 — done/rejected 포함 어떤 상태든 재등록 안 함).
 * 승인은 사람 (Permission Based Learning). 새로 등록됐으면 true.
 */
export function registerKnowledgeGap(brainPath: string, question: string, topScore: number): boolean {
    const q = (question || '').trim();
    if (q.length < 10) return false;
    try {
        const norm = q.toLowerCase().replace(/\s+/g, ' ').slice(0, 200);
        let h = 5381;
        for (let i = 0; i < norm.length; i++) h = ((h << 5) + h + norm.charCodeAt(i)) | 0;
        const id = `gap-${(h >>> 0).toString(36)}`;
        const queue = loadQueue(brainPath);
        if (queue.some(item => item.id === id)) return false;
        // 폭주 방지 — gap 제안이 20건 쌓여 있으면 사람이 정리할 때까지 추가 등록 중단.
        if (queue.filter(item => item.id.startsWith('gap-') && item.status === 'proposed').length >= 20) return false;
        const nowIso = new Date().toISOString();
        queue.push({
            id,
            topic: `지식 공백: ${q.slice(0, 80)}`,
            priority: 40,
            reason: `대화 중 GROUNDING 약함 자동 감지 (두뇌 최고 점수 ${topScore.toFixed(2)})`,
            status: 'proposed',
            createdAt: nowIso,
            updatedAt: nowIso,
        });
        saveQueue(brainPath, queue);
        return true;
    } catch {
        return false;
    }
}

// ── ② 주간 회귀 테스트 — "정정받은 질문에서 같은 실수를 반복하는가" ──────────

export interface RegressionResult {
    question: string;
    errorTag: ErrorTag;
    repeated: boolean | null; // null = 판정 실패
    note: string;
}

const REGRESSION_JUDGE_SYSTEM = [
    '너는 회귀 판정기다. 과거에 사용자가 AI 답변의 오류를 정정했다.',
    '같은 질문에 대한 AI 의 *새 답변*이 같은 오류를 반복하는지 판정하라.',
    '반드시 JSON 한 줄만 출력: {"repeated":true|false,"note":"<근거 30자>"}',
].join('\n');

/**
 * 회귀 케이스 1건 재검사. answerFn 은 호출부가 주입 (성장 사이클: 두뇌 검색 컨텍스트
 * 포함 LLM 호출). LLM-judge 가 정정 내용 대비 재발 여부를 판정.
 */
export async function runRegressionCase(
    c: CorrectionCase,
    answerFn: (question: string) => Promise<string>,
    llm: { baseUrl: string; model: string },
): Promise<RegressionResult> {
    try {
        const newAnswer = (await answerFn(c.question)).slice(0, 1500);
        const user = [
            `[질문] ${c.question}`,
            `[과거 오류 (${c.errorTag})] ${c.wrongAnswer}`,
            `[사용자 정정 (Ground Truth)] ${c.correction}`,
            `[새 답변] ${newAnswer}`,
        ].join('\n');
        const raw = await simpleChatCompletion(REGRESSION_JUDGE_SYSTEM, user, {
            baseUrl: llm.baseUrl, model: llm.model, temperature: 0.1, maxTokens: 100, timeoutMs: 60000,
        });
        const m = raw.match(/\{[\s\S]*?\}/);
        if (!m) return { question: c.question, errorTag: c.errorTag, repeated: null, note: '판정 파싱 실패' };
        const parsed = JSON.parse(m[0]);
        return {
            question: c.question,
            errorTag: c.errorTag,
            repeated: parsed?.repeated === true,
            note: String(parsed?.note || '').slice(0, 60),
        };
    } catch (e: any) {
        return { question: c.question, errorTag: c.errorTag, repeated: null, note: `실패: ${(e?.message ?? e)}`.slice(0, 60) };
    }
}

export function formatRegressionReport(results: RegressionResult[], meta: { dateStr: string }): string {
    const lines = [`# 정정 회귀 리포트 — ${meta.dateStr}`, ''];
    lines.push('과거 사용자 정정(Ground Truth)을 같은 질문으로 재검사한 결과.');
    lines.push('');
    lines.push('| 결과 | 유형 | 질문 | 비고 |');
    lines.push('|---|---|---|---|');
    for (const r of results) {
        const mark = r.repeated === true ? '❌ 재발' : r.repeated === false ? '✅ 통과' : '⚠️ 판정불가';
        lines.push(`| ${mark} | ${r.errorTag} | ${r.question.slice(0, 60).replace(/\|/g, '/')} | ${r.note.replace(/\|/g, '/')} |`);
    }
    return lines.join('\n');
}
