import * as vscode from 'vscode';
import {
    buildApiUrl,
    logError,
    logInfo,
    resolveEngine,
    summarizeText,
} from '../../utils';
import { buildEngineMessageVariants } from '../../lib/contextBuilders/engineMessages';
import { buildModelCandidates } from '../../lib/contextBuilders/modelCandidates';
import { samplingToRestBody } from '../../lmstudio/streamer';
import { lmStudioSamplingFromConfig } from '../../lib/contextBuilders/lmStudioSampling';
import type { ChatMessage } from '../../agent';

export interface CreateStreamingRequestDeps {
    context: vscode.ExtensionContext;
    getAbortSignal: () => AbortSignal | undefined;
}

export async function createStreamingRequest(deps: CreateStreamingRequestDeps, params: {
    baseUrl: string;
    modelName: string;
    reqMessages: ChatMessage[];
    temperature: number;
    /** Dynamic output-token cap computed from the remaining context budget. */
    maxTokens?: number;
    /** Model context window in tokens (used for Ollama's num_ctx). */
    contextLength?: number;
}): Promise<{ response: Response; engine: 'lmstudio' | 'ollama'; apiUrl: string }> {
    const { baseUrl, modelName, reqMessages, temperature } = params;
    const maxTokens = Math.max(256, params.maxTokens ?? 4096);

    // Cloud provider 라우팅 — model id 가 'openrouter:' / 'anthropic:' / 'gemini:' 로 시작하면
    // 해당 adapter 호출. body 는 OpenAI 호환 SSE 로 transform 되어 반환되므로
    // 아래 로컬 엔진 경로의 consumer 가 동일하게 처리.
    try {
        const { parseModelPrefix, streamCloudCompletion } =
            require('../../features/providers') as typeof import('../../features/providers');
        const hit = parseModelPrefix(modelName);
        if (hit) {
            logInfo('AI streaming request (cloud).', { provider: hit.provider, model: hit.model });
            const response = await streamCloudCompletion(deps.context, hit, {
                messages: reqMessages.map((m) => ({ role: m.role as any, content: m.content })),
                temperature,
                maxTokens,
                signal: deps.getAbortSignal(),
            });
            if (!response.ok) {
                const errText = await response.text();
                throw new Error(`Cloud (${hit.provider}) ${response.status}: ${summarizeText(errText, 300)}`);
            }
            return { response, engine: 'lmstudio', apiUrl: `cloud://${hit.provider}/${hit.model}` };
        }
    } catch (e) {
        // 모듈 로드 실패 / 매칭 안 됨 — 로컬 경로로 fall through.
        // (단, 명시적으로 cloud routing 했는데 실패한 경우는 throw 되어 위에서 catch 됨.)
        const msg = (e as Error)?.message ?? '';
        if (msg.startsWith('Cloud (')) throw e;
    }

    const numCtx = Math.max(2048, params.contextLength ?? 32768);
    const engine = resolveEngine(baseUrl);  // 사용자가 설정한 엔진만 사용
    const apiUrl = buildApiUrl(baseUrl, engine, 'chat');
    const messageVariants = buildEngineMessageVariants(reqMessages, engine);
    const modelCandidates = buildModelCandidates(modelName, engine);
    let lastError: Error | null = null;

    // 같은 엔진 내에서만 model candidate / message variant retry
    for (const candidateModel of modelCandidates) {
        for (const variant of messageVariants) {
            const sampling = samplingToRestBody(lmStudioSamplingFromConfig());
            const streamBody = {
                model: candidateModel,
                messages: variant.messages,
                stream: true,
                ...(engine === 'lmstudio'
                    // LM Studio's OpenAI-compatible REST extends the schema with top_k/min_p/
                    // repeat_penalty (same names as Ollama). Spread the shared sampling block so
                    // the REST fallback matches the SDK path — without it a fallback after a
                    // dead handle quietly loses the glitch-suppression preset.
                    ? { max_tokens: maxTokens, temperature, ...sampling }
                    : { options: { num_ctx: numCtx, num_predict: maxTokens, temperature, ...sampling } }),
            };

            // 일시적 네트워크 오류용 retry (최대 2회, 지수 backoff)
            const MAX_RETRIES = 2;
            let serviceDown = false;
            for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
                try {
                    if (attempt > 0) {
                        const delay = 500 * Math.pow(2, attempt - 1);  // 500ms, 1000ms
                        await new Promise(r => setTimeout(r, delay));
                        logInfo('AI streaming request retry.', { engine, attempt, model: candidateModel });
                    }
                    logInfo('AI streaming request started.', {
                        engine, apiUrl, model: candidateModel,
                        variant: variant.name, messageCount: variant.messages.length,
                        attempt
                    });
                    const response = await fetch(apiUrl, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Accept': 'text/event-stream',
                            'Cache-Control': 'no-cache',
                            'Connection': 'keep-alive'
                        },
                        body: JSON.stringify(streamBody),
                        signal: deps.getAbortSignal(),
                        keepalive: true
                    });

                    if (!response.ok) {
                        const errText = await response.text();
                        lastError = new Error(`AI Engine error (${engine}/${variant.name}): ${response.status} - ${summarizeText(errText, 300)}`);
                        logError('AI streaming request returned non-OK status.', {
                            engine, variant: variant.name, apiUrl,
                            status: response.status, body: summarizeText(errText, 500)
                        });
                        // 4xx는 재시도해도 의미없음. 5xx만 재시도.
                        if (response.status >= 400 && response.status < 500) break;
                        continue;
                    }

                    logInfo('AI streaming request connected.', { engine, variant: variant.name, apiUrl });
                    return { response, engine, apiUrl };
                } catch (error: any) {
                    lastError = error instanceof Error ? error : new Error(String(error));
                    // AbortError는 사용자가 취소한 것이므로 retry 금지
                    if (lastError.name === 'AbortError') {
                        throw lastError;
                    }
                    // ECONNREFUSED / DNS-level failures mean the engine process isn't even
                    // listening — no amount of retries or message-variant juggling will help.
                    // Abandon the candidate/variant loops now and surface the "is X running?"
                    // error fast instead of burning 12 fetch attempts before giving up.
                    const errCode = (error?.cause?.code ?? error?.code ?? '').toString();
                    const errMsg = lastError.message;
                    if (
                        errCode === 'ECONNREFUSED' || errCode === 'ENOTFOUND' || errCode === 'EAI_AGAIN'
                        || /ECONNREFUSED|ENOTFOUND|getaddrinfo|fetch failed/i.test(errMsg)
                    ) {
                        serviceDown = true;
                        logError('AI streaming request: engine appears to be down.', {
                            engine, apiUrl, code: errCode, error: errMsg,
                        });
                        break; // exit retry loop
                    }
                    logError('AI streaming request failed.', {
                        engine, variant: variant.name, apiUrl, model: candidateModel,
                        attempt, error: lastError.message
                    });
                }
            }
            if (serviceDown) break; // skip remaining variants
        }
        // serviceDown also short-circuits the model-candidate loop — there is no
        // candidate / variant the engine can answer if it isn't listening at all.
        if (lastError && /ECONNREFUSED|ENOTFOUND|fetch failed/i.test(lastError.message)) break;
    }

    // 명확한 에러 메시지: 어느 엔진이 실패했는지 사용자에게 알림
    const engineLabel = engine === 'lmstudio' ? 'LM Studio' : 'Ollama';
    throw new Error(
        `${engineLabel} 엔진에 연결할 수 없습니다. ` +
        `${engineLabel}가 실행 중이고 모델 '${modelName}'이 로드되어 있는지 확인하세요. ` +
        `(원인: ${lastError?.message || 'unknown'})`
    );
}
