import * as fs from 'fs';
import * as path from 'path';
import { getConfig } from '../config';
import { buildApiUrl, logError, logInfo, resolveEngine, summarizeText, _getBrainDir } from '../utils';

/**
 * IAIService: AI 모델 호출에 대한 인터페이스.
 *
 * `call(prompt)` 는 plain user 메시지 1개만 보내는 legacy shortcut이고,
 * `chat({ system, user })` 는 role-aware 호출이다. Telegram 핸들러처럼
 * 모델을 grounding 해야 하는 경로에서는 system을 반드시 채워야 한다 —
 * gemma 같은 작은 모델은 system이 없으면 짧은/모호한 입력에 대해
 * "시는 못 써드려요" 같은 환각 거절을 하는 경향이 있다.
 */
export interface IAIService {
    call(prompt: string): Promise<string>;
    chat(req: AIChatRequest): Promise<AIChatResult>;
}

export interface AIChatRequest {
    /** Optional system prompt. Strongly recommended for short / ambiguous user inputs. */
    system?: string;
    /** Required. The user message. */
    user: string;
    /** Optional override (default = config.defaultModel). */
    model?: string;
    /** Optional override (default = config.timeout). */
    timeoutMs?: number;
    /**
     * 외부 abort signal. fetch 가 받는 signal 과 OR 로 결합되어, 사용자가 회사 모드
     * 도중 Stop 을 누르면 진행 중인 generation 이 즉시 중단된다. 없으면 timeout 만
     * 적용. dispatcher 같은 긴 multi-turn 경로에서 반드시 전달할 것.
     */
    signal?: AbortSignal;
}

export interface AIChatResult {
    content: string;
    /** Engine that actually returned the content. */
    engine: 'lmstudio' | 'ollama';
    model: string;
    /** True iff content came back empty after all retries. Caller decides UX. */
    empty: boolean;
}

/**
 * IBrainService: 지식 베이스(Brain) 조작에 대한 인터페이스
 */
export interface IBrainService {
    inject(title: string, markdown: string): Promise<string>;
}

/**
 * AIService: Ollama 및 LM Studio 폴백 로직을 포함한 AI 호출 구현체.
 *
 * Behavior:
 *   1. Try the user-configured engine first; on transport / 5xx / empty response,
 *      fall through to the other engine.
 *   2. Empty responses are treated as a soft failure: we log + retry the other
 *      engine before giving up. Pure exceptions (network blip) trigger the same
 *      fallback path.
 *   3. The legacy `call(prompt)` is preserved as a thin wrapper around `chat()`
 *      for callers that don't have a system prompt — but new code should pass
 *      a system prompt explicitly.
 */
export class AIService implements IAIService {
    public async call(prompt: string): Promise<string> {
        const result = await this.chat({ user: prompt });
        return result.content;
    }

    public async chat(req: AIChatRequest): Promise<AIChatResult> {
        const config = getConfig();
        const model = (req.model || config.defaultModel || '').trim() || 'gemma4:e2b';
        const timeoutMs = req.timeoutMs ?? config.timeout;
        const primaryEngine = resolveEngine(config.ollamaUrl);
        const engines = primaryEngine === 'lmstudio'
            ? ['lmstudio', 'ollama'] as const
            : ['ollama', 'lmstudio'] as const;

        const messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [];
        if (req.system && req.system.trim()) {
            messages.push({ role: 'system', content: req.system });
        }
        messages.push({ role: 'user', content: req.user });

        let lastError: Error | null = null;
        let lastEmptyEngine: typeof engines[number] | null = null;

        for (const engine of engines) {
            const apiUrl = buildApiUrl(config.ollamaUrl, engine, 'chat');
            const payload = {
                model,
                messages,
                stream: false,
                ...(engine === 'ollama' ? { options: { temperature: 0.7 } } : { temperature: 0.7 }),
            };

            try {
                logInfo('[AIService] Request started.', {
                    engine, apiUrl, model,
                    hasSystem: !!req.system, userChars: req.user.length,
                });
                // timeout signal + 외부 abort signal 결합. 외부 signal 이 fire 되면
                // 진행 중인 fetch 가 즉시 중단되어 사용자 Stop 이 LLM generation 중에도 효과.
                const timeoutSignal = AbortSignal.timeout(timeoutMs);
                const combinedSignal = req.signal
                    ? AbortSignal.any([req.signal, timeoutSignal])
                    : timeoutSignal;
                const res = await fetch(apiUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(payload),
                    signal: combinedSignal,
                });

                const rawText = await res.text();
                if (!res.ok) {
                    lastError = new Error(`AI call failed: ${res.status} ${summarizeText(rawText, 250)}`);
                    logError(`[AIService] ${engine} HTTP ${res.status}`, { body: summarizeText(rawText, 250) });
                    continue;
                }

                const data = rawText ? JSON.parse(rawText) as any : {};
                const content = engine === 'lmstudio'
                    ? (data.choices?.[0]?.message?.content || '')
                    : (data.message?.content || data.response || '');

                if (!content || !content.trim()) {
                    // Treat empty as soft failure so the other engine gets a chance.
                    lastEmptyEngine = engine;
                    lastError = new Error(`AI engine '${engine}' returned an empty response.`);
                    logError(`[AIService] ${engine} empty response — falling through.`, { model });
                    continue;
                }

                return { content, engine, model, empty: false };
            } catch (error: any) {
                lastError = error instanceof Error ? error : new Error(String(error));
                logError(`[AIService] ${engine} failed:`, lastError.message);
            }
        }

        // Both engines exhausted. Surface a result with empty=true so the
        // caller (e.g. Telegram handler) can produce a user-visible reply
        // instead of swallowing the failure.
        if (lastEmptyEngine) {
            return { content: '', engine: lastEmptyEngine, model, empty: true };
        }
        throw lastError || new Error('All AI engines failed.');
    }
}

/**
 * BrainService: 지식 베이스 파일 시스템 저장 및 관리 구현체
 */
export class BrainService implements IBrainService {
    public async inject(title: string, markdown: string): Promise<string> {
        const brainDir = _getBrainDir();
        if (!fs.existsSync(brainDir)) {
            fs.mkdirSync(brainDir, { recursive: true });
        }

        const today = new Date().toISOString().split('T')[0];
        const datePath = path.join(brainDir, '00_Raw', today);
        if (!fs.existsSync(datePath)) {
            fs.mkdirSync(datePath, { recursive: true });
        }

        const safeTitle = title.replace(/[^a-zA-Z0-9가-힣]/gi, '_');
        const filePath = path.join(datePath, `${safeTitle}.md`);
        fs.writeFileSync(filePath, markdown, 'utf-8');

        return filePath;
    }
}
