/**
 * Alignment Self-Research — 사용자에게 묻기 전에 두뇌를 먼저 검색.
 *
 * Intent Alignment 분석기가 만든 openQuestions 를 사용자에게 노출하기 전에,
 * 활성 두뇌(지식 폴더)를 TF-IDF 로 검색해 *스스로 답할 수 있는 질문* 을 걸러낸다.
 * 답을 찾은 질문은 answeredQuestions 로 옮겨지고(자가 조사 marker 부착),
 * 정말 모르는 질문만 사용자에게 도달한다.
 *
 * Phase 3(학습 루프): 사용자가 직접 답해준 Q/A 는 두뇌의 전용 폴더에 일반
 * 노트로 저장된다. 다음 turn 에서 같은 질문이 나오면 이 모듈의 자가 조사가
 * 그 노트를 발견해 스스로 해결 — 같은 것을 두 번 묻지 않는 구조.
 *
 * 설계 원칙 (intentAlignment 와 동일):
 *   - 절대 throw 하지 않는다. 검색/LLM/IO 실패는 "자가 조사 안 한 것"과
 *     동일하게 동작해야 한다 — alignment 흐름 차단 금지.
 *   - Lesson(Experience Memory) 시스템과 분리 — 그쪽은 "실수 회피" 카드라
 *     주입 위치/의미가 다르다. 여기 저장물은 평범한 지식 노트.
 */
import * as fs from 'fs';
import * as path from 'path';
import { IAIService } from '../../core/services';
import { findBrainFiles, logError, logInfo } from '../../utils';
import { getBrainTokenIndex } from '../../retrieval/brainIndex';
import { tokenize, expandQuery, scoreTfIdfPreTokenized, extractBestExcerpt } from '../../retrieval/scoring';

/**
 * 자가 조사로 채워진 답변의 식별 prefix. formatContractForPrompt 를 거쳐
 * dispatcher 의 LLM 에게 그대로 전달되므로, "사용자가 직접 말한 사실"과
 * 구분되도록 의미를 텍스트 자체에 내장한다. webview 카드 렌더에서도 이
 * prefix 로 자가 조사 항목을 골라낸다.
 */
export const SELF_RESEARCH_PREFIX = '(자가 조사로 두뇌에서 확인) ';

/** 두뇌 저장 폴더 이름 — Phase 3 의 지식 노트가 쌓이는 곳. */
export const ALIGNMENT_KNOWLEDGE_DIR = 'Alignment Knowledge';

/** 질문 1개에 대해 두뇌에서 모은 근거 발췌. */
export interface QuestionEvidence {
    question: string;
    excerpts: Array<{ title: string; relativePath: string; excerpt: string }>;
}

/** 질문별 검색 상한 — 질문당 top 2 파일, 발췌 600자, 전체 합계 4,000자. */
const FILES_PER_QUESTION = 2;
const EXCERPT_MAX_CHARS = 600;
const TOTAL_EVIDENCE_CAP = 4000;

/**
 * openQuestions 각각을 두뇌에 TF-IDF 검색해 근거 발췌를 모은다.
 * 빈 두뇌 / 잘못된 경로 / IO 에러 → 해당 질문의 excerpts 가 빈 배열 (throw 금지).
 * 인덱스는 brainIndex 의 mtime 캐시를 그대로 활용하므로 재호출 비용이 낮다.
 */
export function gatherEvidenceForQuestions(
    brainPath: string,
    questions: string[],
): QuestionEvidence[] {
    const empty = questions.map((q) => ({ question: q, excerpts: [] as QuestionEvidence['excerpts'] }));
    if (!brainPath || questions.length === 0) return empty;
    try {
        const files = findBrainFiles(brainPath);
        if (files.length === 0) return empty;
        const index = getBrainTokenIndex(brainPath, files);
        if (index.length === 0) return empty;

        let totalChars = 0;
        return questions.map((question) => {
            const queryTokens = expandQuery(tokenize(question));
            if (queryTokens.length === 0) return { question, excerpts: [] };
            const scored = scoreTfIdfPreTokenized(queryTokens, index)
                .filter((s) => s.score > 0)
                .slice(0, FILES_PER_QUESTION);
            const excerpts: QuestionEvidence['excerpts'] = [];
            for (const s of scored) {
                if (totalChars >= TOTAL_EVIDENCE_CAP) break;
                const doc = index[s.index];
                let content = '';
                try {
                    content = fs.readFileSync(doc.filePath, 'utf8');
                } catch {
                    continue; // 인덱스에 있는데 디스크에서 사라진 파일 — skip
                }
                const excerpt = extractBestExcerpt(content, tokenize(question), EXCERPT_MAX_CHARS);
                if (!excerpt.trim()) continue;
                totalChars += excerpt.length;
                excerpts.push({ title: doc.title, relativePath: doc.relativePath, excerpt });
            }
            return { question, excerpts };
        });
    } catch (e: any) {
        logError('alignmentResearch: evidence gathering failed; skipping self-research.', {
            error: e?.message ?? String(e),
        });
        return empty;
    }
}

const SELF_ANSWER_SYSTEM_PROMPT = `당신은 "1인 기업 모드"의 *자가 조사 판정가*입니다. 사용자에게 질문을 던지기 전에, 두뇌(저장된 지식 노트)에서 검색된 근거만으로 각 질문에 답할 수 있는지 판정합니다.

규칙:
- 근거 발췌에 *명시적으로 적혀 있는* 내용으로만 답하세요. 일반 상식·추측으로 채우는 것은 금지입니다.
- 근거가 부분적이거나 모호하면 그 질문은 "unanswered" 입니다. 확신이 없으면 unanswered 가 정답입니다.
- answered 인 질문의 answer 는 근거에서 추출한 사실을 2~3문장으로 요약하고, 출처 노트 제목을 괄호로 덧붙이세요.

⚠️ 반드시 아래 JSON 한 번만 출력. 다른 텍스트(설명·코드펜스·머리말) 일체 금지.

{
  "answers": [
    { "question": "<원문 그대로>", "status": "answered" | "unanswered", "answer": "<answered 일 때만, 아니면 빈 문자열>" }
  ]
}`;

/** 자가 조사 판정 결과 한 건. */
export interface SelfAnswer {
    question: string;
    answered: boolean;
    answer: string;
}

/**
 * LLM 1회 호출로 "근거만으로 답 가능한 질문"을 판정. 호출/파싱 실패 시
 * 모든 질문을 unanswered 로 반환 — 원래의 질문 카드 흐름이 그대로 동작.
 */
export async function selfAnswerQuestions(
    ai: IAIService,
    input: {
        userPrompt: string;
        evidence: QuestionEvidence[];
        model?: string;
        timeoutMs?: number;
    },
): Promise<SelfAnswer[]> {
    const withEvidence = input.evidence.filter((e) => e.excerpts.length > 0);
    const fallback = input.evidence.map((e) => ({ question: e.question, answered: false, answer: '' }));
    if (withEvidence.length === 0) return fallback;

    const lines: string[] = [];
    lines.push('[사용자 원본 요청]');
    lines.push(input.userPrompt);
    lines.push('');
    lines.push('[판정할 질문과 두뇌 검색 근거]');
    for (const e of withEvidence) {
        lines.push('');
        lines.push(`질문: ${e.question}`);
        for (const x of e.excerpts) {
            lines.push(`- 근거 (노트: "${x.title}", 경로: ${x.relativePath}):`);
            lines.push(`  ${x.excerpt.replace(/\n/g, '\n  ')}`);
        }
    }
    lines.push('');
    lines.push('판정 JSON만 출력:');

    let raw = '';
    try {
        const result = await ai.chat({
            system: SELF_ANSWER_SYSTEM_PROMPT,
            user: lines.join('\n'),
            model: input.model,
            timeoutMs: input.timeoutMs,
        });
        raw = result.content || '';
    } catch (e: any) {
        logError('alignmentResearch: self-answer call failed; all questions pass through.', {
            error: e?.message ?? String(e),
        });
        return fallback;
    }
    const parsed = _parseSelfAnswerJson(raw);
    if (!parsed) {
        logInfo('alignmentResearch: self-answer parse failed; all questions pass through.', {
            rawHead: raw.slice(0, 100),
        });
        return fallback;
    }
    // LLM 출력을 원래 질문 목록에 매핑 — 누락된 질문은 unanswered 로 채움.
    const byQuestion = new Map(parsed.map((a) => [a.question.trim(), a]));
    return input.evidence.map((e) => {
        const hit = byQuestion.get(e.question.trim());
        if (hit && hit.status === 'answered' && hit.answer.trim()) {
            return { question: e.question, answered: true, answer: hit.answer.trim() };
        }
        return { question: e.question, answered: false, answer: '' };
    });
}

/** 4-stage 관용 파서 — intentAlignment 와 동일 패턴 (작은 모델의 펜스/머리말 대응). */
export function _parseSelfAnswerJson(raw: string): Array<{
    question: string;
    status: 'answered' | 'unanswered';
    answer: string;
}> | null {
    if (!raw || !raw.trim()) return null;
    const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
    const stage1 = (fenced ? fenced[1] : raw).trim();
    for (const candidate of [stage1, _extractFirstBalancedObject(stage1)]) {
        if (!candidate) continue;
        try {
            const obj = JSON.parse(candidate);
            const coerced = _coerceSelfAnswers(obj);
            if (coerced) return coerced;
        } catch { /* 다음 stage */ }
    }
    return null;
}

function _coerceSelfAnswers(obj: unknown): ReturnType<typeof _parseSelfAnswerJson> {
    if (!obj || typeof obj !== 'object') return null;
    const answers = (obj as Record<string, unknown>).answers;
    if (!Array.isArray(answers)) return null;
    const out: Array<{ question: string; status: 'answered' | 'unanswered'; answer: string }> = [];
    for (const a of answers) {
        if (!a || typeof a !== 'object') continue;
        const r = a as Record<string, unknown>;
        const question = typeof r.question === 'string' ? r.question.trim() : '';
        if (!question) continue;
        const status = r.status === 'answered' ? 'answered' : 'unanswered';
        const answer = typeof r.answer === 'string' ? r.answer.trim() : '';
        out.push({ question, status, answer });
    }
    return out.length > 0 ? out : null;
}

function _extractFirstBalancedObject(s: string): string | null {
    const start = s.indexOf('{');
    if (start === -1) return null;
    let depth = 0;
    let inString = false;
    let escape = false;
    for (let i = start; i < s.length; i++) {
        const ch = s[i];
        if (inString) {
            if (escape) escape = false;
            else if (ch === '\\') escape = true;
            else if (ch === '"') inString = false;
            continue;
        }
        if (ch === '"') { inString = true; continue; }
        if (ch === '{') depth++;
        else if (ch === '}') {
            depth--;
            if (depth === 0) return s.slice(start, i + 1);
        }
    }
    return null;
}

// ─── Phase 3: 사용자 답변의 두뇌 저장 (학습 루프) ────────────────────────────

/** 저장 대상 필터 — 사용자가 *직접* 답한 실질적 정보만. */
const MIN_ANSWER_CHARS = 20;

/**
 * 사용자가 alignment 라운드에서 직접 답해준 Q/A 를 두뇌의 전용 폴더에 일반
 * 노트로 저장한다. 자가 조사 항목(SELF_RESEARCH_PREFIX)과 20자 미만의 짧은
 * 답은 제외 — 두뇌 오염 방지. 같은 날 같은 요청의 노트가 이미 있으면 skip.
 *
 * @returns 저장된 파일 경로, 저장할 것이 없거나 실패하면 null (throw 금지).
 */
export function saveAlignmentKnowledge(
    brainPath: string,
    input: { userPrompt: string; qaList: Array<{ q: string; a: string }> },
): string | null {
    try {
        if (!brainPath || !fs.existsSync(brainPath)) return null;
        const userAnswered = input.qaList.filter(
            (qa) => !qa.a.startsWith(SELF_RESEARCH_PREFIX) && qa.a.trim().length >= MIN_ANSWER_CHARS,
        );
        if (userAnswered.length === 0) return null;

        const date = new Date();
        const ymd = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
        const slug = _slugify(input.userPrompt, 30);
        const dir = path.join(brainPath, ALIGNMENT_KNOWLEDGE_DIR);
        const filePath = path.join(dir, `${ymd} ${slug}.md`);
        if (fs.existsSync(filePath)) return null; // 같은 turn 재진입/재실행 — 중복 저장 방지

        if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
        const body: string[] = [];
        body.push(`# ${input.userPrompt.replace(/\s+/g, ' ').trim().slice(0, 50)}`);
        body.push('');
        body.push(`> 1인 기업 모드 Intent Alignment 에서 사용자가 직접 제공한 정보. (${ymd})`);
        body.push('');
        body.push('## 원본 요청');
        body.push(input.userPrompt.trim());
        body.push('');
        body.push('## 확인된 정보');
        for (const qa of userAnswered) {
            body.push(`### Q. ${qa.q.replace(/\n/g, ' ').trim()}`);
            body.push(qa.a.trim());
            body.push('');
        }
        fs.writeFileSync(filePath, body.join('\n').trimEnd() + '\n', 'utf8');
        logInfo('alignmentResearch: saved user-provided knowledge to brain.', { filePath });
        return filePath;
    } catch (e: any) {
        logError('alignmentResearch: knowledge save failed (non-fatal).', {
            error: e?.message ?? String(e),
        });
        return null;
    }
}

/** 파일명 안전 slug — 한글 보존, 경로 위험 문자만 제거. */
export function _slugify(text: string, maxChars: number): string {
    const cleaned = text
        .replace(/\s+/g, ' ')
        .trim()
        .replace(/[\\/:*?"<>|#%{}$!'@`+=[\]]/g, '')
        .slice(0, maxChars)
        .trim();
    return cleaned || 'alignment';
}
