/**
 * Datacollect LLM 호출 인프라 — OpenAI 호환 chat completion 단발 호출.
 *
 * v2.2.201 에서 slashRouter.ts 에서 분리. 옛 위치는 slashRouter.callLmSynthesis
 * 였으나 datacollect handlers + teamops/communication 양쪽이 import 하므로
 * 별도 인프라 모듈로.
 *
 * v2.2.261: LLM 을 **확장(PC)에서 직접** 호출한다(과거엔 bridge `/api/lm` 프록시 경유).
 * 확장은 PC 의 Node 프로세스라 `g1nation.ollamaUrl`(보통 127.0.0.1) 이 PC 의 LM Studio/
 * Ollama 를 정확히 가리키고, Node fetch 는 CORS 가 없어 프록시가 불필요하다. Bridge 를
 * NAS 로 옮긴 뒤 프록시 경유 시 PC→NAS→PC 로 LLM 을 찾으려다(=NAS 의 127.0.0.1) 실패하던
 * 문제를 제거 — 스크래핑/저장은 NAS, LLM 추론은 PC 로 깔끔히 분리된다.
 *
 * `repairKoreanGlitches` 는 callLmSynthesis 출력 위생용 — 모델이 한·영 혼합
 * 토큰 깨짐 (예: "핵ess") 을 뱉으면 LLM 1회 추가 호출로 교정.
 */

import * as vscode from 'vscode';

/**
 * LM 서버(OpenAI 호환 `/v1/chat/completions`)를 확장에서 직접 호출. LM Studio/Ollama 는
 * 인증이 없으므로 토큰 불필요. 타임아웃 가드 포함, 비정상 응답이면 throw.
 */
async function lmChat(lmUrl: string, payload: unknown, timeoutMs = 120_000): Promise<any> {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const res = await fetch(`${lmUrl}/v1/chat/completions`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload),
            signal: controller.signal,
        });
        if (!res.ok) {
            const text = await res.text().catch(() => '');
            throw new Error(`LM 서버 오류 ${res.status} (${lmUrl}): ${text.slice(0, 200)}`);
        }
        return await res.json();
    } finally {
        clearTimeout(timer);
    }
}

/**
 * 모델 출력 붕괴(degeneration) 감지 — 작은/약한 모델이 긴 한국어 입력에서
 * 반복 루프나 토큰 깨짐(예: "톤을 톤을 톤을…", "서비스 기프트 서비스 기프트…",
 * 깨진 유니코드)에 빠지는 경우를 잡는다. 이런 출력은 사용 불가이므로 재시도 트리거.
 */
export function looksDegenerate(text: string): boolean {
    if (!text) return false;
    // 1) 대체문자(���) — 인코딩/토큰 깨짐의 확실한 신호
    if (/�/.test(text)) return true;
    const compact = text.replace(/\s+/g, ' ');
    // 2) 같은 구절(3~20자)이 5회 이상 반복(사이 공백 허용) — 단, 표 구분선·기호 반복은 제외
    const rep = compact.match(/(.{3,20}?)(?:\s*\1){4,}/);
    if (rep && !/^[\s\-|=_.·•*#]+$/.test(rep[1])) return true;
    // 3) 한글+영문 깨짐 합성 표기가 과다 (정상 교정 한도를 크게 초과)
    if ((text.match(/[가-힣][a-z]{2,}/gi) || []).length > 12) return true;
    return false;
}

interface LmOpts {
    /** 실패·빈응답·degeneration 시 재시도 횟수 (기본 2 = 최대 3회 시도). */
    retries?: number;
    /** 기본 샘플링 온도 override. */
    temperature?: number;
    /** 출력 토큰 상한(max_tokens). 미지정이면 payload 에 넣지 않아 서버 기본값을 따른다.
     *  회의록처럼 긴 출력은 크게 지정해 서버 기본 상한에 잘리는 것을 막는다. */
    maxTokens?: number;
    /** 출력이 length(토큰 상한)로 잘렸을 때 끊긴 지점부터 이어쓰기 최대 횟수(기본 3). 0이면 이어쓰기 안 함.
     *  회의록 "누락" 의 실제 원인(출력 잘림)을 보완 — 잘린 부분을 이어붙여 완결성을 확보. */
    maxContinuations?: number;
}

/** 단발 호출 결과 — 이어쓰기 판단을 위해 finish_reason 을 함께 반환. */
interface LmResult { text: string; finish: string; }

const SELF_REFLECT_RE = /\n*(?:```[\w]*\s*)?\[Self-Reflector Check\][\s\S]*$/i;

/** LM 단발 호출 — 샘플링/토큰상한을 받고 (정제 텍스트 + finish_reason)을 돌려주는 내부 코어. */
async function callLmOnce(
    lmUrl: string, model: string, sys: string, prompt: string,
    sampling: { temperature: number; repeat_penalty: number; top_k: number },
    maxTokens?: number,
): Promise<LmResult> {
    const payload: Record<string, unknown> = {
        model,
        messages: [
            { role: 'system', content: sys },
            { role: 'user', content: prompt },
        ],
        temperature: sampling.temperature,
        top_p: 0.85,
        top_k: sampling.top_k,
        repeat_penalty: sampling.repeat_penalty,
    };
    // max_tokens 는 지정됐을 때만 넣는다 — 미지정 caller(벤치마크 등)의 기존 동작(서버 기본값)을 바꾸지 않기 위해.
    if (maxTokens && maxTokens > 0) payload.max_tokens = maxTokens;
    const res = await lmChat(lmUrl, payload, 120_000);
    const choice = res?.choices?.[0];
    const content = choice?.message?.content ?? choice?.text ?? res?.answer ?? res?.response ?? '';
    const finish = String(choice?.finish_reason ?? choice?.native_finish_reason ?? '').toLowerCase();
    const text = String(content).replace(SELF_REFLECT_RE, '').trim();
    return { text, finish };
}

/** 이어쓰기 프롬프트 — 이미 쓴 뒷부분을 주고, 끊긴 지점부터 중복 없이 이어붙일 텍스트만 요청. */
function buildContinuePrompt(soFarTail: string): string {
    return '아래는 지금 작성 중이던 문서(회의록/보고서)의 **마지막 부분**이다. 토큰 한도로 중간에 끊겼다.\n'
        + '끊긴 바로 그 지점부터 **이어서** 계속 작성하라. 규칙:\n'
        + '- 이미 쓴 내용을 반복하거나 요약하지 말 것. 새 서두·인사·머리말 금지.\n'
        + '- 끊긴 문장/표 행/섹션을 자연스럽게 이어붙일 **뒷부분만** 출력한다(중복 헤더 재출력 금지).\n'
        + '- 원문서와 동일한 형식·문체(개조식·명사형 종결)를 유지한다.\n\n'
        + '[지금까지 출력의 마지막 부분]\n' + soFarTail;
}

/**
 * Bridge `/api/lm` 프록시로 OpenAI 호환 chat completion 호출.
 * LLM 서버/모델은 Astra 설정(g1nation.ollamaUrl / defaultModel)을 사용한다.
 *
 * v2.2.252: 약한 로컬 모델의 출력 붕괴(반복·깨짐)와 LM 서버 일시 오류에 대비해
 * 재시도를 내장한다. 재시도할수록 repeat_penalty 를 올리고 top_k 를 좁혀 반복
 * 루프를 깬다. 모든 시도가 실패하면 마지막 에러를 그대로 throw (호출부가 처리).
 */
export async function callLmSynthesis(prompt: string, systemPrompt?: string, opts?: LmOpts): Promise<string> {
    const cfg = vscode.workspace.getConfiguration('g1nation');
    const lmUrl = (cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434').replace(/\/$/, '');
    const model = (cfg.get<string>('defaultModel', '') || 'gemma4:e2b').trim();
    const baseTemp = opts?.temperature ?? Math.max(0, Math.min(2, cfg.get<number>('datacollectSynthesisTemperature', 0.1) ?? 0.1));
    const baseSys = systemPrompt
        || '당신은 시니어 UX/UI 분석가이자 프론트엔드 아키텍트다. 모든 보고서는 한국어로, 제공된 JSON 스캔 데이터의 구체적 수치를 인용해 작성한다. 원본 레퍼런스 사이트를 처음부터 다시 만들기 위한 명세를 작성하는 것이 미션이며, 다른 서비스로 재해석·확장하지 않는다.';
    const sys = baseSys + '\n\n[출력 위생 규칙 — 반드시 준수]\n'
        + '- 자연스러운 한국어로 작성하고, 한 단어 안에 한글과 영문 알파벳을 섞지 마시오 ("결ently", "인orp" 같은 깨진 합성 표기 절대 금지).\n'
        + '- 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓰시오.\n'
        + '- 같은 단어·구절을 반복하지 말고, 한 항목을 다 쓰면 다음 항목으로 진행하시오 (반복 루프 금지).\n'
        + '- [Self-Reflector Check], Consistency/Completeness/Accuracy 같은 내부 검증·체크 로그 블록을 출력에 절대 포함하지 마시오. 최종 사용자 결과물만 출력하시오.';

    const maxRetries = Math.max(0, opts?.retries ?? 2);
    const maxCont = Math.max(0, opts?.maxContinuations ?? 3);
    let lastErr: unknown = null;
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        // 재시도할수록 반복을 더 강하게 억제: repeat_penalty ↑, top_k ↓.
        const sampling = {
            temperature: baseTemp,
            repeat_penalty: 1.1 + attempt * 0.15,   // 1.1 → 1.25 → 1.4
            top_k: Math.max(10, 20 - attempt * 5),  // 20 → 15 → 10
        };
        try {
            const first = await callLmOnce(lmUrl, model, sys, prompt, sampling, opts?.maxTokens);
            let out = first.text;
            if (!out) { lastErr = new Error('LLM 응답이 비어 있습니다.'); continue; }
            if (looksDegenerate(out)) {
                lastErr = new Error('모델 출력이 붕괴(반복/깨짐)했습니다.');
                continue;  // 다음 시도에서 반복 억제 강화
            }
            // ── 이어쓰기: 출력이 토큰 상한(finish=length)으로 잘렸으면 끊긴 지점부터 이어붙인다.
            //    회의록 후반부·긴 표가 통째로 사라지던 "누락"을 완결성으로 보완한다.
            let finish = first.finish;
            for (let c = 0; c < maxCont && finish === 'length'; c++) {
                let next: LmResult;
                try {
                    next = await callLmOnce(lmUrl, model, sys, buildContinuePrompt(out.slice(-1600)), sampling, opts?.maxTokens);
                } catch { break; }                       // 이어쓰기 호출 실패 시 지금까지분으로 마감
                if (!next.text || looksDegenerate(next.text)) break;  // 이어쓰기가 붕괴하면 중단
                out = `${out}\n${next.text}`.trim();
                finish = next.finish;
            }
            if (/[가-힣][a-z]{2,}/.test(out)) {
                out = await repairKoreanGlitches(out, lmUrl, model);
            }
            return out;
        } catch (e) {
            lastErr = e;
            // bridge/LM 서버 오류 — 다음 시도로 (degeneration 으로 인한 서버측 parse 실패 포함)
        }
    }
    throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
}

/**
 * 한글+영문이 한 단어로 깨진 표기(LLM 토큰 꼬임)를 LLM 1회 호출로 교정.
 */
async function repairKoreanGlitches(text: string, lmUrl: string, model: string): Promise<string> {
    try {
        const res = await lmChat(lmUrl, {
            model,
            messages: [
                { role: 'system', content: '당신은 한국어 교정기입니다. 입력 텍스트에서 한글과 영문 알파벳이 한 단어로 잘못 합쳐진 깨진 표기(예: "핵ess"→"핵심", "결ently"→"결국")만 자연스러운 한국어로 교정합니다. 그 외 내용·문장·마크다운 구조·표·정상적인 영문 용어(API, JSON 등)는 한 글자도 바꾸지 않으며, 교정된 전체 텍스트만 그대로 출력합니다(설명·주석 금지).' },
                { role: 'user', content: text },
            ],
            temperature: 0,
            top_p: 0.7,
            top_k: 20,
        }, 120_000);
        const fixed = String(
            res?.choices?.[0]?.message?.content
            ?? res?.choices?.[0]?.text
            ?? res?.answer ?? res?.response ?? '',
        ).replace(/\n*(?:```[\w]*\s*)?\[Self-Reflector Check\][\s\S]*$/i, '').trim();
        if (!fixed || fixed.length < text.length * 0.7) return text;
        return fixed;
    } catch {
        return text;
    }
}

/**
 * Datacollect bridge 가 자주 뱉는 환경 의존성 에러(Python 패키지 미설치, Python
 * 자체 부재 등) 를 패턴 매칭해서 사용자에게 *해결 명령까지* 알려주는 가이드 텍스트.
 * 없으면 빈 문자열 반환. slashRouter 의 catch 블록에서 일반 에러 메시지 뒤에 append.
 */
export function bridgeErrorRemedy(rawMsg: string): string {
    const msg = String(rawMsg || '');
    const pkgMatch = msg.match(/필수 패키지가 없습니다?[:\s]+([\w\-,\s.]+)/i)
        || msg.match(/missing (?:python )?packages?[:\s]+([\w\-,\s.]+)/i);
    if (pkgMatch) {
        const pkgs = pkgMatch[1].split(/[,\s]+/).map((s) => s.trim()).filter(Boolean).join(' ');
        return `\n\n💡 **해결**: Datacollect bridge 가 도는 환경에서 아래 명령으로 누락된 Python 패키지를 설치하세요.\n\n`
            + '```bash\n'
            + `# macOS (homebrew Python — PEP 668 보호 우회):\n`
            + `python3 -m pip install --user --break-system-packages ${pkgs}\n\n`
            + `# 또는 가상환경(venv) 사용 시 그 venv 활성화 후:\n`
            + `pip install ${pkgs}\n`
            + '```\n\n'
            + `설치 후 **bridge 재시작은 보통 불필요** — bridge 는 Python 을 child process 로 spawn 하므로 다음 호출이 바로 새 패키지를 인식합니다. 그래도 안 되면 \`npm run bridge\` 재시작.\n`;
    }
    if (/Python 3이 설치돼 있지 않거나 PATH/i.test(msg) || /command not found.*python/i.test(msg)) {
        return `\n\n💡 **해결**: Python 3 이 설치돼 있어야 합니다. https://www.python.org 에서 설치 후 터미널에서 \`python3 --version\` 으로 확인하세요. 이미 설치돼 있으면 PATH 설정 확인 필요.`;
    }
    if (/ECONNREFUSED|fetch failed/i.test(msg) || /연결할 수 없습니다/i.test(msg)) {
        return `\n\n💡 **해결**: Datacollect bridge 가 떠 있지 않습니다. \`Datacollector_MAC\` 프로젝트에서 \`npm run bridge\` 실행 후 다시 시도하세요.`;
    }
    return '';
}
