import { z } from "zod";
import { getOpenRouter, raporModeli, sohbetModeli } from "./client";

/**
 * AI çağrı katmanı: tüm istekler OpenRouter'ın OpenAI-uyumlu API'sine gider
 * (tek kapı — kullanıcı kararı, 7 Ağu 2026). Rapor ve sohbet kodu SDK/istek
 * ayrıntısını görmesin diye tek arayüz burada toplanıyor.
 */

/**
 * zod şemasını JSON Schema'ya çevirir: z.toJSONSchema'nın eklediği $schema
 * alanı ayıklanır, her object düğümüne additionalProperties: false eklenir —
 * OpenAI-uyumlu strict json_schema doğrulayıcıları bunu şart koşuyor.
 */
function jsonSemasi(sema: z.ZodType): Record<string, unknown> {
  const donustur = (dugum: unknown): unknown => {
    if (Array.isArray(dugum)) return dugum.map(donustur);
    if (dugum && typeof dugum === "object") {
      const cikti: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(dugum as Record<string, unknown>)) {
        if (k === "$schema") continue;
        cikti[k] = donustur(v);
      }
      if (cikti.type === "object" && cikti.additionalProperties === undefined) {
        cikti.additionalProperties = false;
      }
      return cikti;
    }
    return dugum;
  };
  return donustur(z.toJSONSchema(sema)) as Record<string, unknown>;
}

/**
 * OpenRouter yoğunlukta/limitte 429, üst sağlayıcı arızasında 5xx dönebiliyor;
 * bu geçici hatalarda artan bekleme ile yeniden dene. Kalıcı hatalar
 * (400, 401, 403) olduğu gibi fırlar.
 */
const GECICI_KODLAR = new Set([429, 500, 502, 503, 504]);

async function geciciHatadaTekrarla<T>(
  islem: () => Promise<T>,
  denemeSayisi = 3,
): Promise<T> {
  let sonHata: unknown;
  for (let i = 0; i < denemeSayisi; i++) {
    try {
      return await islem();
    } catch (hata) {
      const kod = (hata as { status?: number })?.status;
      if (kod == null || !GECICI_KODLAR.has(kod) || i === denemeSayisi - 1) {
        throw hata;
      }
      sonHata = hata;
      await new Promise((r) => setTimeout(r, 2000 * 2 ** i));
    }
  }
  throw sonHata;
}

/**
 * Tek yapılandırılmış üretim çağrısının üst süre sınırı. Süre, çağıranın
 * (server action) platform limitinden önce dolmalı ki hata iade kodunun
 * çalışabildiği pencerede fırlasın — fonksiyon dışarıdan öldürülürse
 * harcanan kredi iade edilemiyor. Flash sınıfı model tipik ~10-20 sn'de
 * biter; 60 sn üst sağlayıcı yavaşlamalarına pay bırakır.
 */
const URETIM_ZAMAN_ASIMI_MS = 60_000;

/**
 * Şemaya uyan tek seferlik çıktı üretir. Şemaya uymayan yanıtta null döner
 * (çağıran taraf yeniden dener). Zaman aşımında abort hatası fırlar.
 */
export async function yapilandirilmisUret<T extends z.ZodType>(opts: {
  system: string;
  kullanici: string;
  sema: T;
  // Sağlayıcıya giden json_schema bu şemadan üretilir (verilmezse sema).
  // Lokal doğrulama HER ZAMAN sema ile yapılır: tel şeması sıkı uzunluk
  // kısıtları taşıyabilir (grammar zorlaması), sağlayıcı bu kısıtları yok
  // sayarsa çıktı yine gevşek şemayla kurtarılır. Not: strict json_schema'da
  // minItems/maxItems bazı sağlayıcılarda 400 döndürebilir; mevcut sağlayıcı
  // (deepseek) kabul ediyor — sağlayıcı değişiminde geri çekilme yolu
  // wireSema'dan uzunluk kısıtlarını kaldırmaktır.
  wireSema?: z.ZodType;
  maxTokens: number;
}): Promise<z.infer<T> | null> {
  // Tek sinyal tüm iç denemeleri kapsar: süre dolunca bekleyen retry'lar da
  // anında düşer, toplam süre sınırın üstüne çıkamaz.
  const sinyal = AbortSignal.timeout(URETIM_ZAMAN_ASIMI_MS);
  const yanit = await geciciHatadaTekrarla(() =>
    getOpenRouter().chat.completions.create(
      {
        model: raporModeli(),
        max_tokens: opts.maxTokens,
        // Görev küçük (havuzdan seçim + tek cümlelik gerekce) ve şemaya
        // uymayan çıktıyı doğrulama döngüsü zaten yakalıyor; hibrit
        // modellerde düşünme bütçe/süre yemesin.
        reasoning_effort: "none",
        messages: [
          { role: "system", content: opts.system },
          { role: "user", content: opts.kullanici },
        ],
        response_format: {
          type: "json_schema",
          json_schema: {
            name: "yanit",
            strict: true,
            schema: jsonSemasi(opts.wireSema ?? opts.sema),
          },
        },
      },
      { signal: sinyal },
    ),
  );
  // OpenRouter üst sağlayıcı arızasında HTTP 200 gövdesinde {"error": ...}
  // dönebiliyor; choices o durumda yok — TypeError yerine null (retry yolu).
  if (!Array.isArray(yanit.choices) || yanit.choices.length === 0) return null;
  const metin = yanit.choices[0]?.message?.content;
  if (!metin) return null;
  try {
    const parsed = opts.sema.safeParse(JSON.parse(metin));
    return parsed.success ? parsed.data : null;
  } catch {
    return null;
  }
}

export type SohbetMesaji = { role: "user" | "assistant"; content: string };

/**
 * Sohbet yanıtını parça parça üretir; düz metin parçaları yield eder, çağıran
 * taraf hem stream'ler hem biriktirip kaydeder. Akış ortasında zaman aşımı
 * olursa hata fırlar (route'taki kısmi kaydet + iade yolu devreye girer).
 */
export async function* sohbetAkisi(opts: {
  system: string;
  mesajlar: SohbetMesaji[];
  maxTokens: number;
}): AsyncGenerator<string> {
  // Asılı kalan üst sağlayıcıya karşı sigorta
  const sinyal = AbortSignal.timeout(120_000);
  const akis = await geciciHatadaTekrarla(() =>
    getOpenRouter().chat.completions.create(
      {
        model: sohbetModeli(),
        max_tokens: opts.maxTokens,
        // Sohbet 1024 token'a sığmalı; düşünme hem bütçeyi hem adayı bekletiyor
        reasoning_effort: "none",
        stream: true,
        messages: [
          { role: "system", content: opts.system },
          ...opts.mesajlar,
        ],
      },
      { signal: sinyal },
    ),
  );
  for await (const parca of akis) {
    // Yalnızca delta.content akar; varsa delta.reasoning kullanıcıya sızmaz
    const metin = parca.choices?.[0]?.delta?.content;
    if (metin) yield metin;
  }
  // openai SDK akış ortasındaki abort'u yutup iteratörü NORMAL bitiriyor
  // (core/streaming: isAbortError -> return); kesik cevabın tam cevap
  // sanılmaması ve iade yolunun çalışması için zaman aşımını elle fırlat.
  if (sinyal.aborted) throw sinyal.reason;
}
