/**
 * Critic Agent + Debate Loop (v1) — 제출된 업무 산출물의 LLM 검수.
 *
 * Self-Evolving OS 마스터 플랜 Phase 1 / Track 2-3. 신뢰 조건 T3 의 LLM 계층:
 * Requirement Coverage(결정론적, 정규식) 가 "요소가 *언급* 됐는가" 만 보면,
 * Critic 은 "내용이 *충실* 한가 + 결정/미결 구분이 맞는가 + 근거 없는 단정이
 * 없는가" 를 본다.
 *
 * Debate Loop 원형은 작성→비판→재작성→재검토지만, 로컬 Gemma 의 latency 비용
 * 때문에 v1 은 *조건부 1-pass 검수* — 결정론적 검사(커버리지 누락 또는 확신도
 * <70)가 문제를 신호할 때만 Critic LLM 1회 호출, 결과를 답변 아래 보완 카드로
 * 표시. 전면 다회전 debate 는 config knob(maxRounds) 만 준비해 두고 후속 증분.
 *
 * 모든 LLM 의존은 주입(critique caller) — 모듈 자체는 순수, 테스트 가능.
 */

import type { TaskRequirement } from './requirementGraph';

export interface CriticIssue {
    severity: 'major' | 'minor';
    description: string;
}

export interface CritiqueResult {
    /** true = 검수 통과 (보완 불필요). */
    pass: boolean;
    issues: CriticIssue[];
    /** 누락 요소를 보완하는 추가 섹션 제안 (Critic 이 생성 가능했을 때만). */
    supplement: string;
    /** 디버그용 원문 (파싱 실패 분석). */
    raw?: string;
}

/** 주입형 LLM caller — agent.ts 의 callNonStreaming 또는 평가 하니스의 단순 호출. */
export type CritiqueLlmCall = (system: string, user: string, maxTokens: number) => Promise<string>;

export interface CriticOptions {
    /** 검수 대상 초안 최대 길이 (chars) — 초과분 잘라서 전달. 기본 12000. */
    maxDraftChars: number;
    /** Critic 응답 max tokens. 기본 700. */
    maxTokens: number;
}

export const DEFAULT_CRITIC_OPTIONS: CriticOptions = {
    maxDraftChars: 12000,
    maxTokens: 700,
};

export function buildCritiquePrompt(
    userPrompt: string,
    draft: string,
    requirement: TaskRequirement | null,
    missingLabels: string[],
    opts: CriticOptions = DEFAULT_CRITIC_OPTIONS,
): { system: string; user: string } {
    const reqSection = requirement
        ? [
            `업무 유형: ${requirement.label}`,
            '필수 요소:',
            ...requirement.elements.map((e) => `- ${e.label}: ${e.hint}`),
        ].join('\n')
        : '업무 유형: (미분류)';
    const missingSection = missingLabels.length > 0
        ? `결정론적 검사가 누락 가능성을 표시한 요소: ${missingLabels.join(', ')}`
        : '결정론적 검사 통과 (참고용 재확인)';

    const system = [
        '너는 업무 산출물 검수자(Critic)다. 동료가 작성한 초안을 비판적으로 검토한다.',
        '검수 기준:',
        '1. 필수 요소가 *내용으로* 충실한가 (단어만 등장 ≠ 충족).',
        '2. 결정사항과 미결(논의만 된 것)이 구분돼 있는가.',
        '3. 근거 없는 단정·지어낸 수치/이름/날짜가 없는가. 원문에 없는 내용 발견 시 major.',
        '4. 정보가 없는 항목은 "(확인 필요)" 로 솔직히 표시했는가.',
        '',
        '반드시 아래 JSON *만* 출력 (다른 텍스트 금지):',
        '{"pass": true|false, "issues": [{"severity": "major"|"minor", "description": "..."}], "supplement": "누락 보완 텍스트 (보완 불가능하면 빈 문자열)"}',
        'supplement 는 초안에 *실제로 추가할 수 있는* 마크다운 섹션만. 원문에 없는 내용을 지어내 보완하는 것은 금지 — 그 경우 "(확인 필요)" 항목으로 작성.',
    ].join('\n');

    const draftCapped = draft.length > opts.maxDraftChars ? draft.slice(0, opts.maxDraftChars) + '\n…(잘림)' : draft;
    const user = [
        `[원래 요청]\n${userPrompt}`,
        `[검수 기준 컨텍스트]\n${reqSection}\n${missingSection}`,
        `[검수 대상 초안]\n${draftCapped}`,
    ].join('\n\n');

    return { system, user };
}

/** Critic LLM 응답에서 JSON 추출 — 코드펜스/잡설 섞여도 첫 균형 {} 블록을 파싱. */
export function parseCritique(raw: string): CritiqueResult | null {
    if (!raw || !raw.trim()) return null;
    const start = raw.indexOf('{');
    if (start === -1) return null;
    // 균형 괄호 스캔 — 중첩 객체(issues 배열 내부) 안전.
    let depth = 0;
    let end = -1;
    let inString = false;
    let escaped = false;
    for (let i = start; i < raw.length; i++) {
        const ch = raw[i];
        if (escaped) { escaped = false; continue; }
        if (ch === '\\') { escaped = true; continue; }
        if (ch === '"') { inString = !inString; continue; }
        if (inString) continue;
        if (ch === '{') depth++;
        else if (ch === '}') {
            depth--;
            if (depth === 0) { end = i; break; }
        }
    }
    if (end === -1) return null;
    try {
        const obj = JSON.parse(raw.slice(start, end + 1));
        const issues: CriticIssue[] = Array.isArray(obj.issues)
            ? obj.issues
                .filter((i: any) => i && typeof i.description === 'string')
                .map((i: any) => ({
                    severity: i.severity === 'major' ? 'major' : 'minor',
                    description: String(i.description).slice(0, 500),
                }))
            : [];
        return {
            pass: obj.pass === true && issues.length === 0,
            issues,
            supplement: typeof obj.supplement === 'string' ? obj.supplement.slice(0, 4000) : '',
            raw: raw.slice(0, 200),
        };
    } catch {
        return null;
    }
}

/**
 * Critic 검수 1회 실행. LLM 실패/파싱 실패 시 null — 호출자(hook)는 silent skip
 * (검수 실패가 main turn 을 막지 않도록).
 */
export async function runCriticReview(params: {
    userPrompt: string;
    draft: string;
    requirement: TaskRequirement | null;
    missingLabels: string[];
    callLlm: CritiqueLlmCall;
    options?: Partial<CriticOptions>;
}): Promise<CritiqueResult | null> {
    const opts: CriticOptions = { ...DEFAULT_CRITIC_OPTIONS, ...(params.options || {}) };
    const { system, user } = buildCritiquePrompt(params.userPrompt, params.draft, params.requirement, params.missingLabels, opts);
    let raw: string;
    try {
        raw = await params.callLlm(system, user, opts.maxTokens);
    } catch {
        return null;
    }
    return parseCritique(raw);
}

/** 검수 결과 footer — pass 면 빈 문자열 (노이즈 방지). */
export function formatCriticFooter(critique: CritiqueResult): string {
    if (critique.pass) return '';
    const lines: string[] = [];
    lines.push('\n\n> 🔁 **검수 (Critic)** — 초안에서 발견된 문제:');
    for (const issue of critique.issues.slice(0, 6)) {
        const tag = issue.severity === 'major' ? '🔴' : '🟡';
        lines.push(`> - ${tag} ${issue.description}`);
    }
    if (critique.supplement && critique.supplement.trim()) {
        lines.push('>');
        lines.push('> **보완 제안:**');
        for (const l of critique.supplement.trim().split('\n')) {
            lines.push(`> ${l}`);
        }
    }
    return lines.join('\n');
}
