import { logInfo, logError, summarizeText } from '../../utils';
import type { SidebarLmStudioDeps } from '../../sidebarProvider';

/**
 * 모델 dropdown 갱신용 cache + discovery layer. sidebarProvider 의 `_sendModels`
 * 흐름에서 *순수 가져오기 + cache 책임* 만 분리. defaultModel 재배치 / loadedModels
 * probe / cloud model 합치기 / webview 송신 / readyStatus 갱신은 여전히 provider 가
 * 담당 (webview 송신 deps 가 깊어 함께 옮기면 코드만 늘어남).
 *
 * 옛 코드: provider 의 3개 state slot (`_modelsCache`, `_modelDiscoveryInFlight`,
 * static `MODELS_CACHE_TTL_MS`) + `_sendModels` 내부 50줄짜리 discovery 블록. 이걸
 * 하나의 class 로 묶어 (a) cache TTL 정책 한 곳, (b) inflight guard 단일화,
 * (c) SDK→REST fallback 시퀀스가 단위 테스트 가능.
 *
 * Cache 정책:
 *   - url 이 바뀌면 즉시 stale (다른 엔진 hop 후 옛 결과 못 받게)
 *   - fetchedAt + ttlMs 지나면 stale
 *   - `force=true` 면 stale 취급
 */
export class ModelDiscovery {
    private cache: { url: string; models: string[]; online: boolean; fetchedAt: number } | null = null;
    private inflight = false;

    constructor(private readonly ttlMs: number) {}

    isInflight(): boolean {
        return this.inflight;
    }

    /** Inflight 진입 시도. 이미 진행 중이면 false (호출자 skip 해야 함). */
    tryEnter(): boolean {
        if (this.inflight) return false;
        this.inflight = true;
        return true;
    }

    /** Inflight 해제 — finally 절에서 무조건 호출. */
    release(): void {
        this.inflight = false;
    }

    /**
     * Cache 가 유효하면 그 결과 반환, 아니면 null. force 면 unconditionally null.
     */
    getCached(url: string, force: boolean): { models: string[]; online: boolean } | null {
        if (force) return null;
        const c = this.cache;
        if (!c) return null;
        if (c.url !== url) return null;
        if (Date.now() - c.fetchedAt >= this.ttlMs) return null;
        return { models: c.models.slice(), online: c.online };
    }

    /**
     * Ready status 가 빠르게 읽는 online flag. cache 없으면 null (모름).
     * URL mismatch 도 null — 이전 엔진의 online 을 새 엔진 패널에 표시하지 않게.
     */
    cachedOnline(url: string): boolean | null {
        const c = this.cache;
        if (!c) return null;
        if (c.url !== url) return null;
        return c.online;
    }

    setCached(url: string, models: string[], online: boolean): void {
        this.cache = { url, models: models.slice(), online, fetchedAt: Date.now() };
    }

    /**
     * 실제 엔진에서 모델 목록을 가져옴. 시도 순서:
     *   1) LM Studio + SDK 있으면 `system.listDownloadedModels('llm')` — REST 와 달리
     *      JIT 설정에 관계없이 다운로드된 모든 LLM 반환. macOS 에서 dropdown 비는
     *      회귀의 근본 해결책.
     *   2) (1) 실패/0개면 REST `/v1/models` (lmstudio) 또는 `/api/tags` (ollama).
     *
     * online = `models.length > 0`. 두 경로 모두 실패하면 빈 배열 + online=false.
     */
    async discoverFromEngine(opts: {
        url: string;
        engine: 'lmstudio' | 'ollama';
        modelsUrl: string;
        lmStudio?: SidebarLmStudioDeps;
        force: boolean;
    }): Promise<{ models: string[]; online: boolean }> {
        let models: string[] = [];

        if (opts.engine === 'lmstudio' && opts.lmStudio) {
            try {
                logInfo('Model discovery started (SDK).', { engine: opts.engine, force: opts.force });
                const sdkModels = await opts.lmStudio.downloadedModels();
                if (sdkModels.length > 0) {
                    models = sdkModels;
                    logInfo('Model discovery succeeded (SDK).', {
                        engine: opts.engine,
                        count: models.length,
                        modelsPreview: models.slice(0, 5),
                    });
                } else {
                    logInfo('LM Studio SDK returned no downloaded models — falling back to REST /v1/models.', { engine: opts.engine });
                }
            } catch (e: any) {
                logError('LM Studio SDK model discovery failed — falling back to REST.', {
                    engine: opts.engine,
                    error: e?.message || String(e),
                });
            }
        }

        if (models.length === 0) {
            try {
                logInfo('Model discovery started (REST).', { engine: opts.engine, modelsUrl: opts.modelsUrl, force: opts.force });
                const res = await fetch(opts.modelsUrl, { signal: AbortSignal.timeout(5000) });
                const rawText = await res.text();
                if (!res.ok) {
                    logError('Model discovery returned non-OK status.', {
                        engine: opts.engine,
                        modelsUrl: opts.modelsUrl,
                        status: res.status,
                        body: summarizeText(rawText, 300),
                    });
                } else {
                    const data = rawText ? JSON.parse(rawText) as any : {};
                    models = opts.engine === 'lmstudio'
                        ? (data.data || []).map((m: any) => m.id)
                        : (data.models || []).map((m: any) => m.name);

                    if (models.length > 0) {
                        logInfo('Model discovery succeeded (REST).', {
                            engine: opts.engine,
                            count: models.length,
                            modelsPreview: models.slice(0, 5),
                        });
                    }
                }
            } catch (e: any) {
                logError('Model discovery failed (REST).', {
                    engine: opts.engine,
                    modelsUrl: opts.modelsUrl,
                    error: e?.message || String(e),
                });
            }
        }

        return { models, online: models.length > 0 };
    }
}
