import { getAiConfig } from './aiConfig';
import { withAiQueue } from './aiQueue';

export interface ChatMsg { role: string; content: string; }

// Model her istekte bellekten atılıp yeniden yüklenmesin diye (varsayılan Ollama
// davranışı: 5 dakika kullanılmazsa boşalır, sıradaki istek soğuk başlangıç
// gecikmesi yaşar) worker'ı bu süre boyunca bellekte tutmasını istiyoruz.
// -1: Ollama'ya özel değer, "asla otomatik atma" anlamına gelir — sadece Ollama
// sunucusu yeniden başlarsa model tekrar yüklenir. GPU'da bolca boş VRAM var
// (bkz. 2026-08-14 log: 44GB boş / 17.5GB kullanılan), bu yüzden modeli sürekli
// bellekte tutmanın maliyeti yok; 30dk'lık sınır sadece gereksiz soğuk başlangıç
// gecikmelerine (~90sn, bazen backend timeout'undan uzun) sebep oluyordu.
const KEEP_ALIVE = -1;
// 800 pratikte çok düşük çıktı — çok bölümlü hukuki cevaplar (kira feshi, tazminat
// hesabı gibi) bu sınıra çarpıp cümlenin ortasında kesiliyordu. 2048 hem çoğu cevabı
// tam bitirmeye yetiyor hem de modelin sonsuz uzayıp gitmesine karşı hâlâ bir üst sınır.
const DEFAULT_NUM_PREDICT = 2048;
const DEFAULT_TIMEOUT_MS = 90_000;
const DEFAULT_STALL_TIMEOUT_MS = 60_000;

function buildBody(messages: ChatMsg[], model: string, stream: boolean, numPredict: number) {
  return {
    model,
    messages,
    think: false,
    stream,
    keep_alive: KEEP_ALIVE,
    // num_predict: modelin üretebileceği maksimum token sayısı — öngörülemez
    // uzunlukta "düşünme"/üretim sürelerine karşı üst sınır.
    options: { num_predict: numPredict },
  };
}

// Tek seferlik (streaming olmayan) çağrı — başlık üretimi, belge analizi, dilekçe
// taslağı, içtihat sorgu optimizasyonu gibi kullanıcının token token izlemediği,
// arka planda tamamlanması yeterli olan işler için. Ollama'nın native /api/chat
// uç noktasını kullanır — think:false burada güvenilir çalışıyor (OpenAI-uyumluluk
// katmanı /v1/chat/completions'ta güvenilir değil).
export async function callOllama(
  messages: ChatMsg[],
  opts: { numPredict?: number; timeoutMs?: number } = {}
): Promise<string> {
  // withAiQueue'nun İÇİNDE: zaman aşımı sayacı, istek GPU sırasında beklerken
  // değil, gerçekten gönderilmeye başladığında işlemeye başlasın — yoksa kuyrukta
  // uzun bekleyen bir istek, sırası gelir gelmez anında "zaman aşımı" olurdu.
  return withAiQueue(async () => {
    const { runpodUrl, aiModel } = getAiConfig();
    const ctrl = new AbortController();
    const timeoutId = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
    try {
      const response = await fetch(`${runpodUrl}/api/chat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(buildBody(messages, aiModel, false, opts.numPredict ?? DEFAULT_NUM_PREDICT)),
        signal: ctrl.signal,
      });
      if (!response.ok) {
        throw new Error(`AI Model request failed: ${response.statusText}`);
      }
      const data = await response.json();
      return (data.message?.content || '').toString().trim();
    } catch (err: any) {
      if (err?.name === 'AbortError' || (err?.message && String(err.message).toLowerCase().includes('aborted'))) {
        throw new Error('Yapay zeka yanıt verme süresi aşıldı (Zaman Aşımı). Lütfen tekrar deneyin.');
      }
      throw err;
    } finally {
      clearTimeout(timeoutId);
    }
  });
}

// Akışlı (streaming) çağrı — kullanıcının yanıtı token token canlı gördüğü
// etkileşimli sohbet uç noktaları için. Ollama /api/chat stream:true ile
// newline-delimited JSON döner (her satır { message: { content }, done } içerir).
// onToken her parça geldiğinde çağrılır; dönüş değeri tüm yanıtın birleştirilmiş hali.
// Sabit bir toplam süre sınırı YOK (kullanıcı üretilmekte olan metni zaten görüyor) —
// bunun yerine art arda gelen parçalar arasında stallTimeoutMs kadar sessizlik olursa
// (model takıldı / bağlantı koptu) isteği iptal ediyoruz.
export async function streamOllama(
  messages: ChatMsg[],
  onToken: (delta: string) => void,
  opts: { numPredict?: number; stallTimeoutMs?: number } = {}
): Promise<string> {
  return withAiQueue(() => streamOllamaInner(messages, onToken, opts));
}

async function streamOllamaInner(
  messages: ChatMsg[],
  onToken: (delta: string) => void,
  opts: { numPredict?: number; stallTimeoutMs?: number } = {}
): Promise<string> {
  const { runpodUrl, aiModel } = getAiConfig();
  const stallMs = opts.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
  const ctrl = new AbortController();
  let stallTimer: ReturnType<typeof setTimeout>;
  const resetStall = () => {
    clearTimeout(stallTimer);
    stallTimer = setTimeout(() => ctrl.abort(), stallMs);
  };
  resetStall();

  try {
    const response = await fetch(`${runpodUrl}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(buildBody(messages, aiModel, true, opts.numPredict ?? DEFAULT_NUM_PREDICT)),
      signal: ctrl.signal,
    });
    if (!response.ok || !response.body) {
      throw new Error(`AI Model request failed: ${response.statusText}`);
    }

    let full = '';
    let buffer = '';
    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    const consumeLine = (line: string) => {
      const trimmed = line.trim();
      if (!trimmed) return;
      let json: any;
      try {
        json = JSON.parse(trimmed);
      } catch {
        return;
      }
      const delta = json.message?.content || '';
      if (delta) {
        full += delta;
        onToken(delta);
      }
      // Ollama son satırda done_reason:"length" döndürürse yanıt num_predict
      // sınırına çarpıp yarım kesilmiş demektir — sessizce geçmek yerine loglayıp
      // ileride benzer bir "cevaplar yarım kalıyor" şikayetini hızlı teşhis edebilelim.
      if (json.done && json.done_reason === 'length') {
        console.warn('[aiClient] Yanıt num_predict sınırına çarpıp kesildi.');
      }
    };

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      resetStall();
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';
      lines.forEach(consumeLine);
    }
    if (buffer.trim()) consumeLine(buffer);

    return full.trim();
  } finally {
    clearTimeout(stallTimer!);
  }
}

const DEFAULT_OCR_NUM_PREDICT = 1200;
const DEFAULT_OCR_TIMEOUT_MS = 60_000;

// DeepSeek-OCR ("deepseek-ocr-fixed") ile bir görseli metne çevirir. Mizan'dan
// (sohbet/analiz) tamamen ayrı bir model — aynı Ollama sunucusunda, farklı
// model adıyla çağrılıyor, withAiQueue ikisini de aynı GPU sırasında bekletiyor.
//
// NOT — "deepseek-ocr-fixed" modeli Ollama kütüphanesindeki resmi paketten
// FARKLI: resmi paketin şablonu ("template": "{{ .Prompt }}") görsel için hiçbir
// yer tutucu içermiyordu, bu yüzden model gönderilen görseli hiç görmüyordu.
// Sunucuda elle şu şekilde düzeltilmiş bir kopya oluşturuldu:
//
//   curl -X POST $RUNPOD_API_BASE_URL/api/create -d '{
//     "model": "deepseek-ocr-fixed",
//     "from": "deepseek-ocr",
//     "template": "{{ if .Images }}{{ range .Images }}[img-0]{{ end }}\n{{ end }}{{ .Prompt }}",
//     "parameters": { "temperature": 0 }
//   }'
//
// Ollama'nın model verisi silinir/sıfırlanırsa (örn. volume resetlenirse) bu
// komut tekrar çalıştırılmalı — yoksa OCR istekleri sessizce boş metin döner.
export async function callOllamaVisionOcr(
  imageBase64: string,
  opts: { numPredict?: number; timeoutMs?: number; prompt?: string } = {}
): Promise<string> {
  return withAiQueue(async () => {
    const { runpodUrl, ocrModel } = getAiConfig();
    const ctrl = new AbortController();
    const timeoutId = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? DEFAULT_OCR_TIMEOUT_MS);
    try {
      const response = await fetch(`${runpodUrl}/api/chat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          model: ocrModel,
          messages: [
            {
              role: 'user',
              content: opts.prompt || '<image>\nConvert the document to markdown.',
              images: [imageBase64],
            },
          ],
          think: false,
          stream: false,
          keep_alive: KEEP_ALIVE,
          options: { num_predict: opts.numPredict ?? DEFAULT_OCR_NUM_PREDICT },
        }),
        signal: ctrl.signal,
      });
      if (!response.ok) {
        const errText = await response.text().catch(() => '');
        throw new Error(`OCR model request failed: ${response.statusText} ${errText}`.trim());
      }
      const data = await response.json();
      return (data.message?.content || '').toString().trim();
    } finally {
      clearTimeout(timeoutId);
    }
  });
}
