/**
 * Intent classifier for 1인 기업 모드 chat input.
 *
 * The company mode used to route *every* chat message through the full
 * dispatcher (CEO planner → specialists → CEO synthesis). That meant
 * casual messages like "고마워", "방금 그거 다시 보여줘", "이 파일 뭐 하는
 * 거야?" all kicked off a multi-agent round — wasteful at best, confusing
 * at worst because the user expects ordinary chat to behave like ordinary
 * chat regardless of which mode the chip is in.
 *
 * This module runs *one* small-model LLM call per message and decides:
 *   - `chat`     — greeting / thanks / generic question → answer briefly
 *   - `followup` — refers to the previous round ("그거 다시", "어떻게 됐어?")
 *   - `new_task` — a fresh work request → run the pipeline
 *
 * The caller (`chatHandlers`) uses the verdict to route. If the LLM call
 * fails for any reason we fall back to `new_task` so we never *silently*
 * eat a real work request — the worst-case is "the classifier misfires
 * and we run a pipeline we didn't need", same as the old behaviour.
 *
 * Returns `intent`, plus a one-line `reason` from the LLM and the raw
 * response for debug. The reason is shown in the chat label so the user
 * can tell *why* their message was treated as chat vs. a task.
 */
import { IAIService } from '../../core/services';
import { logError, logInfo } from '../../utils';

export type ChatIntent = 'chat' | 'followup' | 'new_task';

export interface IntentResult {
    intent: ChatIntent;
    /** One-line Korean explanation from the classifier (or a fallback note). */
    reason: string;
    /** Raw LLM body — kept for the debug log. */
    raw: string;
    /** True iff the JSON parse succeeded. False means we fell back to default. */
    parsed: boolean;
    /**
     * 분류기가 새 task에 적합하다고 본 파이프라인 id. `new_task` 결과에서만
     * 의미 있고, classifier가 추천 안 했거나 컨텍스트에 후보가 없으면
     * undefined. 호출자(chatHandlers)는 사용자 설정
     * (`companyAutoSelectPipeline`)이 켜져 있을 때만 이 값을 활용해 dispatch
     * 시점에 활성 파이프라인을 일시 override; 평소엔 사용자가 명시적으로
     * 활성화해 둔 파이프라인을 그대로 존중.
     */
    suggestedPipelineId?: string;
}

/** 분류기가 컨텍스트로 받는 파이프라인 후보 한 줄. */
export interface PipelineHint {
    id: string;
    name: string;
    /** Short description shown to the classifier so it can pick the right one. */
    description?: string;
    /** Number of stages — helps the classifier judge "is this overkill?". */
    stageCount: number;
}

/**
 * Context passed in from the caller. All fields optional — empty context is
 * the cold-start case (no prior turn yet).
 */
export interface IntentContext {
    /** Brief from the previous turn, if any. */
    previousBrief?: string;
    /** Tail of the previous CEO report (truncated by caller). */
    previousReportTail?: string;
    /** ISO timestamp of when the previous turn ended, for staleness hints. */
    previousTurnAt?: number;
    /** Whether a pipeline is currently configured + active. Tweaks the prompt. */
    activePipelineName?: string;
    /**
     * 분류기가 골라 추천할 후보 파이프라인 리스트. autoSelectPipeline이
     * 켜져 있을 때만 의미가 있다. 호출자가 비워 보내면 classifier도 추천
     * 시도조차 안 함.
     */
    availablePipelines?: PipelineHint[];
}

const SYSTEM_PROMPT_BASE = `당신은 "1인 기업 모드"의 메시지 분류기입니다. 사용자가 방금 보낸 한 줄이 다음 중 무엇인지 한 번에 정확히 판정하세요.

  - "chat"     : 인사·감사·잡담·짧은 질문·간단한 정보 요청. 새 프로젝트가 아님.
  - "followup" : 직전 라운드의 산출물·과정을 가리키는 발화. "그거 다시", "어디까지 했어", "그 결과 보여줘", "방금 그 파일 열어줘" 등.
  - "new_task" : 새로 시작할 *업무*. 기획·개발·디자인·리서치·QA 등 여러 단계가 필요한 작업 요청. 한 단어라도 "만들어줘"·"기획해줘"·"개발해줘"·"분석해줘" 같이 명확한 새 업무이면 new_task.

판단 기준:
- 직전 라운드 컨텍스트가 있고 사용자 발화가 그것을 가리키면 followup.
- 직전 라운드가 없거나(첫 메시지) 직전 내용과 무관하고 새 결과물을 *만들어 달라* 요구이면 new_task.
- 위 두 가지가 아니거나 모호하면 chat.
- 사용자가 명시적으로 "파이프라인 돌려"·"풀 사이클"·"기획부터" 같은 키워드 쓰면 무조건 new_task.
- "고마워"·"잘했어"·"오케이" 같은 짧은 응답은 무조건 chat.`;

const SYSTEM_PROMPT_NO_PIPELINE_PICK = `${SYSTEM_PROMPT_BASE}

⚠️ 반드시 아래 JSON 형식 정확히 한 번. 다른 텍스트(설명, 코드펜스, 머리말) 일체 금지.

{"intent":"chat"|"followup"|"new_task","reason":"한 줄(20자 이내)"}`;

const SYSTEM_PROMPT_WITH_PIPELINE_PICK = `${SYSTEM_PROMPT_BASE}

new_task인 경우, *사용자 컨텍스트에 제공된 파이프라인 후보 중 가장 적합한 것* 하나도 같이 골라주세요.

🛑 **사용자 명시 신호 우선 (절대 위반 금지)**:
- 사용자 발화에 "기획만"·"기획서까지"·"기획서 작성"·"plan only"·"plan-only"·"기획만 해줘" 등이 있으면 → 후보 중 *기획·plan* 관련 단어가 들어간 가장 짧은 파이프라인을 고르세요. 풀 사이클 절대 금지.
- "디자인까지"·"디자인 단계까지" → 디자인 포함, 개발 제외 파이프라인.
- "개발까지"·"풀 사이클"·"끝까지"·"배포까지" → 풀 파이프라인.
- 명시 신호가 없으면 요청 규모를 보고 짧고 충분한 것 우선. 애매하면 풀 사이클보다 짧은 쪽.
- 적당한 후보가 없으면 suggestedPipelineId 필드를 빈 문자열로 두세요.

⚠️ 사용자가 "기획만 해줘"라고 했는데 개발 stage가 포함된 파이프라인을 골라주면 사용자 의도를 정면으로 무시하는 것입니다. 반드시 짧은 파이프라인 우선.

⚠️ 반드시 아래 JSON 형식 정확히 한 번. 다른 텍스트(설명, 코드펜스, 머리말) 일체 금지.

{"intent":"chat"|"followup"|"new_task","reason":"한 줄(20자 이내)","suggestedPipelineId":"<후보 id 또는 빈 문자열>"}`;

/**
 * 4-stage tolerant JSON parser — same shape as ceoPlanner's. Small models
 * routinely break the "no extra text" rule with fences / leading prose, so
 * a strict JSON.parse only catches the well-behaved minority.
 */
function _parseIntentJson(raw: string): { intent: string; reason: string; suggestedPipelineId?: 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();

    try {
        const obj = JSON.parse(stage1);
        const c = _coerce(obj);
        if (c) return c;
    } catch { /* fall through */ }

    const balanced = _extractFirstBalancedObject(stage1);
    if (balanced) {
        try {
            const obj = JSON.parse(balanced);
            const c = _coerce(obj);
            if (c) return c;
        } catch { /* fall through */ }
    }

    // Last resort: regex pluck — 작은 모델이 JSON 깨뜨려도 핵심 필드만 건짐.
    const intentMatch = stage1.match(/"intent"\s*:\s*"(chat|followup|new_task)"/i);
    const reasonMatch = stage1.match(/"reason"\s*:\s*"([^"]*)"/);
    const pipeMatch = stage1.match(/"suggestedPipelineId"\s*:\s*"([^"]*)"/);
    if (intentMatch) {
        return {
            intent: intentMatch[1],
            reason: reasonMatch?.[1] ?? '',
            suggestedPipelineId: pipeMatch?.[1] || undefined,
        };
    }
    return null;
}

function _coerce(obj: unknown): { intent: string; reason: string; suggestedPipelineId?: string } | null {
    if (!obj || typeof obj !== 'object') return null;
    const o = obj as Record<string, unknown>;
    const intent = typeof o.intent === 'string' ? o.intent.trim() : '';
    const reason = typeof o.reason === 'string' ? o.reason.trim() : '';
    if (intent !== 'chat' && intent !== 'followup' && intent !== 'new_task') return null;
    const suggestedPipelineId = typeof o.suggestedPipelineId === 'string' && o.suggestedPipelineId.trim()
        ? o.suggestedPipelineId.trim()
        : undefined;
    return { intent, reason, suggestedPipelineId };
}

/**
 * 사용자 발화에서 명시적 범위 신호(예: "기획만", "디자인까지", "풀 사이클")를
 * 잡아내 일치하는 후보 파이프라인 id로 강제 매핑. LLM 추천이 무시 못 하게
 * 백엔드 측 안전망. 매칭 못 하면 undefined 반환 → LLM 추천 그대로 사용.
 *
 * 매칭 룰:
 *   - "기획만" / "기획서까지" / "plan only" → 이름·설명에 "기획" 또는 "plan"이
 *     들어가고 개발/배포 단어가 *없는* 파이프라인 중 stageCount 가장 작은 것.
 *   - "디자인까지" / "디자인만" → "design" / "디자인" 단어, 개발 단어 없음.
 *   - "풀 사이클" / "끝까지" / "배포까지" → stageCount 가장 큰 것.
 */
function _keywordPickPipeline(
    userPrompt: string,
    candidates: PipelineHint[],
): string | undefined {
    if (!candidates.length) return undefined;
    const text = userPrompt.toLowerCase();

    const wantsPlanOnly = /(기획만|기획서까지|기획만\s*해|기획서\s*작성|기획부터\s*기획|plan\s*[-_]?only|plan\s*only)/i.test(text);
    const wantsDesignStop = /(디자인까지|디자인만|디자인\s*단계까지)/i.test(text);
    const wantsFull = /(풀\s*사이클|끝까지|배포까지|풀\s*프로덕트|개발까지|production\s*full|full\s*pipeline)/i.test(text);

    const hasDev = (s: string) => /(개발|코드|배포|구현|deploy|develop|dev|implement)/i.test(s);
    const hasDesign = (s: string) => /(디자인|design|ui)/i.test(s);

    if (wantsPlanOnly) {
        // 개발/디자인 stage 없는 짧은 기획 파이프라인 우선. 이름/설명 둘 다 본다.
        const planOnly = candidates
            .filter((p) => !hasDev(p.name + ' ' + (p.description ?? ''))
                       && !hasDesign(p.name + ' ' + (p.description ?? '')))
            .sort((a, b) => a.stageCount - b.stageCount);
        if (planOnly.length > 0) return planOnly[0].id;
        // 그것도 없으면 stageCount 가장 작은 후보 (그래도 풀 사이클은 피함).
        const shortest = [...candidates].sort((a, b) => a.stageCount - b.stageCount)[0];
        return shortest?.id;
    }
    if (wantsDesignStop) {
        const designStop = candidates
            .filter((p) => !hasDev(p.name + ' ' + (p.description ?? '')))
            .sort((a, b) => a.stageCount - b.stageCount);
        if (designStop.length > 0) return designStop[0].id;
    }
    if (wantsFull) {
        const fullest = [...candidates].sort((a, b) => b.stageCount - a.stageCount)[0];
        return fullest?.id;
    }
    return undefined;
}

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;
}

function _buildUserMessage(userPrompt: string, ctx: IntentContext): string {
    const lines: string[] = [];
    if (ctx.activePipelineName) {
        lines.push(`(참고) 현재 활성 파이프라인: "${ctx.activePipelineName}". 사용자가 이 파이프라인을 다시 돌리길 원하면 new_task, 결과 확인이면 followup.`);
    }
    if (ctx.previousBrief || ctx.previousReportTail) {
        lines.push('');
        lines.push('[직전 라운드 컨텍스트]');
        if (ctx.previousTurnAt) {
            const ageMin = Math.round((Date.now() - ctx.previousTurnAt) / 60000);
            if (ageMin >= 60) lines.push(`(${Math.round(ageMin / 60)}시간 전 — 오래되었음)`);
            else lines.push(`(${ageMin}분 전)`);
        }
        if (ctx.previousBrief) lines.push(`brief: ${ctx.previousBrief.slice(0, 300)}`);
        if (ctx.previousReportTail) lines.push(`보고서 끝부분: ${ctx.previousReportTail.slice(0, 300)}`);
    } else {
        lines.push('(직전 라운드 컨텍스트 없음 — 첫 메시지이거나 새 세션)');
    }
    if (ctx.availablePipelines && ctx.availablePipelines.length > 0) {
        lines.push('');
        lines.push('[선택 가능한 파이프라인 후보]');
        for (const p of ctx.availablePipelines) {
            const desc = p.description ? ` — ${p.description}` : '';
            lines.push(`- id: ${p.id} | "${p.name}" (${p.stageCount}단계)${desc}`);
        }
    }
    lines.push('');
    lines.push('[방금 사용자 메시지]');
    lines.push(userPrompt);
    lines.push('');
    lines.push('판정 JSON만 출력:');
    return lines.join('\n');
}

/**
 * End-to-end classification. Never throws — returns a sensible default
 * (`new_task`) on any failure so the user never silently loses a real
 * work request.
 */
export async function classifyChatIntent(
    ai: IAIService,
    userPrompt: string,
    ctx: IntentContext,
    options: { model?: string; timeoutMs?: number } = {},
): Promise<IntentResult> {
    const trimmed = userPrompt.trim();
    if (!trimmed) {
        return { intent: 'chat', reason: '빈 메시지', raw: '', parsed: false };
    }

    // ── Heuristic short-circuits ────────────────────────────────────────────
    // 매우 명확한 chat 신호는 LLM 호출 없이 즉시 결정 — 작은 모델이 흔들리는
    // 경계를 좁히기 위해 비용 0의 안전망을 둔다. 신호가 약하면 통과시켜
    // LLM이 판정하게 함.
    if (trimmed.length <= 8 && /^(고마워|감사|땡큐|ㅇㅇ|ㅇㅋ|네|예|아니|좋아|굿|ok|okay|thanks?|good|great)/i.test(trimmed)) {
        return { intent: 'chat', reason: '짧은 인사·동의', raw: '', parsed: false };
    }

    // 후보 파이프라인이 제공됐을 때만 분류기에 "골라 봐" 요청 — 후보 없이 그
    // 필드를 비워달라고 강제하면 모델이 불필요한 빈값을 채우려다 응답 형식
    // 깨뜨릴 수 있다.
    const wantPipelinePick = !!(ctx.availablePipelines && ctx.availablePipelines.length > 0);
    const system = wantPipelinePick ? SYSTEM_PROMPT_WITH_PIPELINE_PICK : SYSTEM_PROMPT_NO_PIPELINE_PICK;

    let raw = '';
    try {
        const result = await ai.chat({
            system,
            user: _buildUserMessage(trimmed, ctx),
            model: options.model,
            timeoutMs: options.timeoutMs,
        });
        raw = result.content || '';
    } catch (e: any) {
        logError('intentClassifier: AI call failed; defaulting to new_task.', { error: e?.message ?? String(e) });
        return { intent: 'new_task', reason: '분류 실패 — 안전하게 업무로 처리', raw: '', parsed: false };
    }

    const parsed = _parseIntentJson(raw);
    if (!parsed) {
        logInfo('intentClassifier: parse failed; defaulting to new_task.', { rawHead: raw.slice(0, 100) });
        return { intent: 'new_task', reason: '판정 형식 불일치 — 업무로 처리', raw, parsed: false };
    }
    // suggestedPipelineId는 *제공된 후보 목록 안에 존재할 때만* 신뢰. 분류기가
    // 환각한 id를 그대로 dispatch에 넘기면 dispatcher에서 silent fallback 발생.
    let suggestedPipelineId: string | undefined;
    if (parsed.intent === 'new_task' && parsed.suggestedPipelineId && wantPipelinePick) {
        const knownIds = new Set((ctx.availablePipelines ?? []).map((p) => p.id));
        if (knownIds.has(parsed.suggestedPipelineId)) {
            suggestedPipelineId = parsed.suggestedPipelineId;
        }
    }
    // 백엔드 키워드 fallback — LLM 추천을 *덮어쓴다*. 사용자가 "기획만"·"디자인만"
    // 같은 명시 신호를 줬는데 LLM이 풀 사이클을 골라버리는 사고를 막기 위함.
    // 후보 파이프라인 이름/설명에 매칭되는 키워드가 prompt에 있으면 그 쪽을 강제.
    if (parsed.intent === 'new_task' && wantPipelinePick && ctx.availablePipelines) {
        const keywordPick = _keywordPickPipeline(trimmed, ctx.availablePipelines);
        if (keywordPick) suggestedPipelineId = keywordPick;
    }
    logInfo('intentClassifier: parsed.', { intent: parsed.intent, reason: parsed.reason, suggestedPipelineId });
    return {
        intent: parsed.intent as ChatIntent,
        reason: parsed.reason || '(이유 없음)',
        raw,
        parsed: true,
        suggestedPipelineId,
    };
}
