"""FikoYT Görsel Üretim Sunucusu (REST API)
================================================
- Uvicorn FastAPI port 8002
- Asenkron iş kuyruğu (tek GPU üzerinden sıralı üretim)
- Endpointler: /health, /models, /generate, /task/{id}, /image, /upload
- Şablon: ACE-Step API ile uyumlu (status int, task_id string).
"""
from __future__ import annotations

# Windows console UTF-8 — Unicode arrow (→) ve Türkçe karakter encoding fix
import sys as _sys
try:
    _sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    _sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass

import argparse
import asyncio
import io
import os
import sys
import time
import uuid
import shutil
import threading
import traceback
from pathlib import Path
from typing import Optional, List, Dict, Any
from concurrent.futures import ThreadPoolExecutor

import json
import math
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Body


def _json_temizle(obj):
    """NaN/Inf değerleri None ile değiştirir (JSON uyumluluğu)."""
    if isinstance(obj, float):
        if math.isnan(obj) or math.isinf(obj):
            return None
        return obj
    if isinstance(obj, dict):
        return {k: _json_temizle(v) for k, v in obj.items()}
    if isinstance(obj, list):
        return [_json_temizle(v) for v in obj]
    if isinstance(obj, tuple):
        return tuple(_json_temizle(v) for v in obj)
    return obj
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field

SUNUCU_KOK = Path(__file__).parent.resolve()
sys.path.insert(0, str(SUNUCU_KOK))

from boru import (
    GorselBorusu,
    UretimIstegi,
    UretimSonucu,
    SDXL_MODELLERI,
    VARSAYILAN_MODEL,
)
from lora_egit import lora_egit

# prompt_yardim opsiyonel — yüklenmezse endpoint'ler stub döner
try:
    from prompt_yardim import (
        ollama_var_mi, turkce_to_ingilizce, prompt_gelistir,
        sihirbaz_prompt_olustur,
    )
    _PROMPT_YARDIM_AKTIF = True
except Exception as _e:
    print(f"[Sunucu] prompt_yardim yüklenemedi: {_e}")
    _PROMPT_YARDIM_AKTIF = False

    def ollama_var_mi() -> bool:  # type: ignore
        return False

    def turkce_to_ingilizce(metin: str) -> str:  # type: ignore
        return ""

    def prompt_gelistir(metin: str, kategori: str = "genel") -> str:  # type: ignore
        return ""

    def sihirbaz_prompt_olustur(girdi: dict, kategori: str) -> str:  # type: ignore
        return ""


# ===================== Görev Modeli =====================
class Gorev:
    def __init__(self, gid: str, istek_dict: dict):
        self.id = gid
        self.istek = istek_dict
        self.durum = 0  # 0=calisiyor, 1=tamam, 2=hata
        self.ilerleme = 0  # 0-100
        self.ilerleme_metni = ""
        self.adim = 0
        self.toplam_adim = 0
        self.sonuc: Optional[Dict[str, Any]] = None
        self.hata: Optional[str] = None
        self.baslama = time.time()
        self.bitis: Optional[float] = None


# ===================== Servis =====================
class GorselServisi:
    def __init__(self, cikti_kok: Path):
        self.boru = GorselBorusu()
        self.gorevler: Dict[str, Gorev] = {}
        self._kuyruk: "asyncio.Queue[str]" = asyncio.Queue()
        self._thread_pool = ThreadPoolExecutor(max_workers=1)
        self._kilit = threading.Lock()
        # GPU kilidi: eğitim ve üretim aynı VRAM'i kullanır → seri çalıştır
        self._gpu_kilit = threading.Lock()
        self.cikti_kok = cikti_kok
        self.cikti_kok.mkdir(parents=True, exist_ok=True)
        (self.cikti_kok / "yuklemeler").mkdir(exist_ok=True)

    async def baslat(self):
        asyncio.create_task(self._isci())

    async def _isci(self):
        while True:
            gid = await self._kuyruk.get()
            gorev = self.gorevler.get(gid)
            if not gorev:
                continue
            try:
                istek = UretimIstegi(**gorev.istek)
                # cikti_klasoru — istek üzerinden gelmediyse server köküne
                if not istek.cikti_klasoru or istek.cikti_klasoru == "outputs":
                    istek.cikti_klasoru = str(self.cikti_kok / "outputs")
                gorev.toplam_adim = max(1, istek.steps)

                def cb(adim: int, toplam: int, metin: str):
                    with self._kilit:
                        gorev.adim = adim
                        gorev.toplam_adim = toplam
                        gorev.ilerleme_metni = metin
                        gorev.ilerleme = int(min(99, (adim / max(1, toplam)) * 100))

                loop = asyncio.get_event_loop()

                # GPU kilidi: eğitim çalışıyorsa bekle
                def _kilitli_uret():
                    with self._gpu_kilit:
                        return self.boru.uret(istek, cb)

                sonuc: UretimSonucu = await loop.run_in_executor(
                    self._thread_pool, _kilitli_uret
                )
                gorev.sonuc = {
                    "yollar": sonuc.yollar,
                    "seed": sonuc.seed,
                    "sure_sn": sonuc.sure_sn,
                }
                gorev.ilerleme = 100
                gorev.durum = 1
            except Exception as e:
                gorev.hata = f"{type(e).__name__}: {e}"
                gorev.durum = 2
                traceback.print_exc()
            finally:
                gorev.bitis = time.time()

    async def kuyruga_ekle(self, istek_dict: dict) -> str:
        gid = uuid.uuid4().hex[:16]
        g = Gorev(gid, istek_dict)
        self.gorevler[gid] = g
        await self._kuyruk.put(gid)
        return gid

    def gorev_sil(self, gid: str) -> bool:
        return self.gorevler.pop(gid, None) is not None

    def dosya_kaydet(self, dosya: UploadFile, alt_klasor: str = "yuklemeler") -> Path:
        hedef_kok = self.cikti_kok / alt_klasor
        hedef_kok.mkdir(parents=True, exist_ok=True)
        ext = Path(dosya.filename or "image").suffix.lower() or ".png"
        if ext not in (".png", ".jpg", ".jpeg", ".webp", ".bmp"):
            ext = ".png"
        hedef = hedef_kok / f"{int(time.time()*1000)}_{uuid.uuid4().hex[:6]}{ext}"
        with hedef.open("wb") as f:
            shutil.copyfileobj(dosya.file, f)
        return hedef


# ===================== Pydantic Şemaları =====================
class PromptCevirGirdi(BaseModel):
    metin: str = ""


class PromptGelistirGirdi(BaseModel):
    metin: str = ""
    kategori: str = "genel"


class PromptSihirbazGirdi(BaseModel):
    girdi: Dict[str, Any] = Field(default_factory=dict)
    kategori: str = "karakter"


class LoraEgitGirdi(BaseModel):
    karakter_id: str
    lora_isim: str
    veri_klasoru: str
    base_model_repo: str
    tetikleyici: Optional[str] = None
    cins: str = "person"
    adim: int = 1500
    rank: int = 16
    lr: float = 1e-4
    boyut: int = 1024
    # Devam et modu: mevcut LoRA dosya yolu — yoksa sıfırdan eğit
    mevcut_lora_yolu: Optional[str] = None
    # Devam ederken aynı dosyaya yaz (üzerine), yoksa yeni job_id'li dosya
    ayni_dosyaya_yaz: bool = False
    # Pro: DoRA (subject fidelity ↑, %30 yavaş)
    use_dora: bool = False
    # Pro: Min-SNR-gamma loss (5.0 standart, 0=kapalı)
    min_snr_gamma: float = 0.0
    # Pro: WD14 ile per-image smart captioning
    smart_caption: bool = False
    # Pro: eğitim sonrası kalite testi (ArcFace + 8 prompt, ~3 dk)
    auto_test: bool = False


class UretimGirdi(BaseModel):
    prompt: str
    negative_prompt: str = ""
    model_ismi: str = VARSAYILAN_MODEL
    genislik: int = 1024
    yukseklik: int = 1024
    steps: int = 30
    cfg: float = 7.0
    seed: int = -1
    batch: int = 1
    scheduler: str = "auto"
    referans_yuz_yolu: Optional[str] = None
    yuz_referans_olcek: float = 0.7
    yuz_modu: str = "faceid"  # "faceid" | "plus"
    referans_urun_yolu: Optional[str] = None
    urun_referans_olcek: float = 0.6
    referans_sac_yolu: Optional[str] = None
    sac_referans_olcek: float = 0.6
    referans_elbise_yolu: Optional[str] = None
    elbise_referans_olcek: float = 0.7
    referans_arkaplan_yolu: Optional[str] = None
    arkaplan_referans_olcek: float = 0.4
    face_swap_kaynak_yuz: Optional[str] = None
    face_swap_etkin: bool = False
    kaynak_resim_yolu: Optional[str] = None  # img2img için
    degisiklik_gucu: float = 0.7
    gorev_tipi: str = "text2img"
    coklu_yuz_kaynaklari: Optional[List[str]] = None
    adimlar: Optional[List[Dict[str, Any]]] = None
    genislet_yon: str = "all"
    genislet_oran: float = 1.5
    aktif_loralar: Optional[List[Dict[str, Any]]] = None
    cikti_klasoru: Optional[str] = None  # boş → server default
    cikti_format: str = "png"
    # Pro: ADetailer (yüz/el restorasyon, ~10sn ek)
    adetailer_etkin: bool = False
    adetailer_denoise: float = 0.4  # 0.3-0.5 önerilir
    # Pro: Upscaler (Real-ESRGAN → LANCZOS+refine fallback)
    upscale_faktor: int = 1  # 1=kapalı, 2=2x, 4=4x
    upscale_model: str = "auto"  # "auto" | "realesrgan" | "lanczos"


# ===================== FastAPI =====================
def app_olustur(cikti_kok: Path) -> FastAPI:
    servis = GorselServisi(cikti_kok)
    app = FastAPI(title="FikoYT Görsel Üretim", version="0.1.0")
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["*"],
        allow_headers=["*"],
    )

    @app.on_event("startup")
    async def _basla():
        await servis.baslat()

    @app.get("/health")
    async def health():
        return {
            "ok": True,
            "cuda": __import__("torch").cuda.is_available(),
            "aktif_model": servis.boru._aktif_model,
            "kuyruk": servis._kuyruk.qsize(),
            "gorev_sayi": len(servis.gorevler),
        }

    @app.get("/models")
    async def models():
        return {
            "modeller": [
                {
                    "isim": k,
                    "tip": v["tip"],
                    "varsayilan_steps": v["varsayilan_steps"],
                    "varsayilan_cfg": v["varsayilan_cfg"],
                    "repo": v["repo"],
                }
                for k, v in SDXL_MODELLERI.items()
            ],
            "varsayilan": VARSAYILAN_MODEL,
        }

    @app.post("/generate")
    async def generate(istek: UretimGirdi):
        if istek.model_ismi not in SDXL_MODELLERI:
            raise HTTPException(400, f"Bilinmeyen model: {istek.model_ismi}")
        gid = await servis.kuyruga_ekle(istek.model_dump())
        return {"task_id": gid}

    @app.get("/task/{gid}")
    async def task(gid: str):
        g = servis.gorevler.get(gid)
        if not g:
            raise HTTPException(404, "Görev bulunamadı")
        return {
            "task_id": g.id,
            "status": g.durum,
            "progress": g.ilerleme,
            "progress_text": g.ilerleme_metni,
            "step": g.adim,
            "total_steps": g.toplam_adim,
            "result": g.sonuc,
            "error": g.hata,
            "elapsed": (g.bitis or time.time()) - g.baslama,
        }

    @app.delete("/task/{gid}")
    async def task_sil(gid: str):
        return {"silindi": servis.gorev_sil(gid)}

    @app.post("/upload")
    async def upload(dosya: UploadFile = File(...)):
        hedef = servis.dosya_kaydet(dosya, "yuklemeler")
        return {"yol": str(hedef)}

    @app.get("/image")
    async def image(path: str):
        # Güvenlik: path mutlaka cikti_kok altında olmalı (path traversal koruma)
        try:
            p = Path(path).resolve()
            kok_resolved = servis.cikti_kok.resolve()
            # cikti_kok altında mı?
            try:
                p.relative_to(kok_resolved)
            except ValueError:
                raise HTTPException(403, "Erişim reddedildi (geçersiz yol)")
        except HTTPException:
            raise
        except Exception:
            raise HTTPException(400, "Geçersiz yol")
        if not p.exists() or not p.is_file():
            raise HTTPException(404, "Dosya yok")
        if p.suffix.lower() not in (".png", ".jpg", ".jpeg", ".webp", ".bmp"):
            raise HTTPException(400, "Desteklenmeyen format")
        return FileResponse(str(p))

    # ========= LoRA EĞİTİM ENDPOINT'LERİ =========
    isler_db = Path(servis.cikti_kok) / "loralar" / "_isler.json"
    isler_db.parent.mkdir(parents=True, exist_ok=True)

    def _isleri_yukle() -> Dict[str, Dict[str, Any]]:
        if not isler_db.exists():
            return {}
        try:
            data = json.loads(isler_db.read_text(encoding="utf-8"))
            # Restart sonrası — çalışıyor görünenler kesintiye uğramış kabul edilir
            for k, v in data.items():
                if v.get("durum") == 0:
                    v["durum"] = 2
                    v["hata"] = "Sunucu yeniden başlatıldı (eğitim kesintiye uğradı)"
            return data
        except Exception:
            return {}

    def _isleri_kaydet():
        try:
            isler_db.write_text(
                json.dumps(_json_temizle(lora_isleri), ensure_ascii=False,
                           indent=2),
                encoding="utf-8",
            )
        except Exception as e:
            print(f"[Sunucu] İş listesi kaydedilemedi: {e}")

    lora_isleri: Dict[str, Dict[str, Any]] = _isleri_yukle()

    def _lora_egit_thread(job_id: str, girdi: dict):
        """Arka plan thread'inde eğit. GPU kilidi tutulur — üretim bekler."""
        is_obj = lora_isleri[job_id]
        # GPU kilidi al — bu sırada generate task'ları bekler
        servis._gpu_kilit.acquire()
        try:
            cikti_klasor = Path(servis.cikti_kok) / "loralar"
            cikti_klasor.mkdir(parents=True, exist_ok=True)
            # Aynı dosyaya yazma (devam et + eski dosyayı güncelle)
            mevcut_yol = girdi.get("mevcut_lora_yolu")
            ayni = girdi.get("ayni_dosyaya_yaz") and mevcut_yol
            if ayni and Path(mevcut_yol).exists():
                cikti_yolu = Path(mevcut_yol)
            else:
                cikti_yolu = cikti_klasor / f"{job_id}.safetensors"

            # ÖNEMLİ: Eğitim öncesi inference pipeline'dan tüm LoRA'ları
            # unload et — Windows mmap kilidi olmadan dosyayı yazabilelim
            try:
                pipe_obj = getattr(servis.boru, "_pipe", None)
                if pipe_obj is not None:
                    print("[Sunucu] Eğitim için inference LoRA'ları "
                          "unload ediliyor (mmap fix)")
                    try:
                        pipe_obj.unload_lora_weights()
                    except Exception as e:
                        print(f"[Sunucu] unload_lora_weights uyarı: {e}")
                    # Aktif LoRA seçim listesini de temizle
                    try:
                        servis.boru._aktif_loralari_uygula(None)
                    except Exception:
                        pass
                    # gc + cuda cache temizle (mmap referansları bırakılır)
                    import gc as _gc
                    _gc.collect()
                    try:
                        import torch as _torch
                        if _torch.cuda.is_available():
                            _torch.cuda.empty_cache()
                    except Exception:
                        pass
            except Exception as e:
                print(f"[Sunucu] Pipeline unload hatası: {e}")

            tetikleyici = girdi.get("tetikleyici") or f"zwx_{job_id[:6]}"

            def cb(step: int, toplam: int, faz: str):
                is_obj["adim"] = step
                is_obj["toplam_adim"] = toplam
                is_obj["faz"] = faz
                is_obj["ilerleme"] = int(min(99, (step / max(1, toplam)) * 100))

            sonuc = lora_egit(
                veri_klasoru=girdi["veri_klasoru"],
                cikti_yolu=str(cikti_yolu),
                base_model=girdi["base_model_repo"],
                tetikleyici=tetikleyici,
                cins=girdi.get("cins", "person"),
                adim=girdi.get("adim", 1500),
                rank=girdi.get("rank", 16),
                lr=girdi.get("lr", 1e-4),
                boyut=girdi.get("boyut", 1024),
                karakter_id=girdi.get("karakter_id"),
                lora_isim=girdi.get("lora_isim"),
                mevcut_lora_yolu=mevcut_yol,
                use_dora=girdi.get("use_dora", False),
                min_snr_gamma=girdi.get("min_snr_gamma", 0.0),
                smart_caption=girdi.get("smart_caption", False),
                auto_test=girdi.get("auto_test", False),
                ilerleme_cb=cb,
                iptal_kontrol=lambda: is_obj.get("iptal", False),
            )
            if sonuc.get("basarili"):
                is_obj["durum"] = 1
                is_obj["sonuc"] = _json_temizle(sonuc)
                is_obj["ilerleme"] = 100
            else:
                is_obj["durum"] = 2
                is_obj["hata"] = sonuc.get("hata", "Eğitim başarısız")
        except Exception as e:
            import traceback
            traceback.print_exc()
            is_obj["durum"] = 2
            is_obj["hata"] = f"{type(e).__name__}: {e}"
        finally:
            try:
                servis._gpu_kilit.release()
            except Exception:
                pass
            _isleri_kaydet()

    @app.post("/lora/egit")
    async def lora_egit_baslat(istek: LoraEgitGirdi):
        import threading
        job_id = uuid.uuid4().hex[:16]
        lora_isleri[job_id] = {
            "id": job_id,
            "karakter_id": istek.karakter_id,
            "lora_isim": istek.lora_isim,
            "base_model": istek.base_model_repo,
            "durum": 0,  # 0=calisiyor 1=tamam 2=hata
            "ilerleme": 0,
            "adim": 0,
            "toplam_adim": istek.adim,
            "faz": "Hazırlanıyor",
            "iptal": False,
            "baslama": time.time(),
        }
        _isleri_kaydet()
        threading.Thread(
            target=_lora_egit_thread,
            args=(job_id, istek.model_dump()),
            daemon=True,
        ).start()
        return {"job_id": job_id}

    @app.get("/lora/job/{job_id}")
    async def lora_job(job_id: str):
        if job_id not in lora_isleri:
            raise HTTPException(404, "İş bulunamadı")
        return _json_temizle(lora_isleri[job_id])

    @app.delete("/lora/job/{job_id}")
    async def lora_job_iptal(job_id: str):
        if job_id in lora_isleri:
            lora_isleri[job_id]["iptal"] = True
            return {"iptal": True}
        return {"iptal": False}

    @app.get("/lora/list")
    async def lora_listesi():
        """Mevcut .safetensors LoRA'ları listele (meta json ile birlikte)."""
        kok = Path(servis.cikti_kok) / "loralar"
        liste = []
        if kok.exists():
            for f in sorted(kok.glob("*.safetensors")):
                meta_yolu = f.with_suffix(".json")
                meta = {}
                if meta_yolu.exists():
                    try:
                        meta = json.loads(
                            meta_yolu.read_text(encoding="utf-8"),
                        )
                    except Exception:
                        pass
                liste.append({
                    "id": f.stem,
                    "yol": str(f),
                    "boyut_mb": round(f.stat().st_size / 1e6, 1),
                    "meta": meta,
                })
        return _json_temizle({"loralar": liste})

    @app.patch("/lora/{lora_id}/meta")
    async def lora_meta_guncelle(lora_id: str, guncellemeler: Dict[str, Any]):
        """LoRA meta JSON'ını günceller (örn. karakter_id, isim, tetikleyici).
        Eski LoRA'lara karakter atamak için."""
        kok = Path(servis.cikti_kok) / "loralar"
        meta_yolu = kok / f"{lora_id}.json"
        if not meta_yolu.exists():
            raise HTTPException(404, "Meta dosyası yok")
        try:
            mevcut = json.loads(meta_yolu.read_text(encoding="utf-8"))
        except Exception:
            mevcut = {}
        mevcut.update(guncellemeler)
        meta_yolu.write_text(
            json.dumps(_json_temizle(mevcut), ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
        return {"basarili": True, "meta": _json_temizle(mevcut)}

    @app.delete("/lora/{lora_id}")
    async def lora_sil(lora_id: str):
        kok = Path(servis.cikti_kok) / "loralar"
        f = kok / f"{lora_id}.safetensors"
        meta = kok / f"{lora_id}.json"
        silindi = False
        for x in (f, meta):
            if x.exists():
                try:
                    x.unlink()
                    silindi = True
                except Exception:
                    pass
        return {"silindi": silindi}

    # ========= PROMPT YARDIM ENDPOINT'LERİ =========
    @app.get("/prompt/durum")
    async def prompt_durum():
        try:
            return {
                "ollama_aktif": ollama_var_mi(),
                "modul_aktif": _PROMPT_YARDIM_AKTIF,
            }
        except Exception as e:
            return {"ollama_aktif": False, "modul_aktif": False,
                    "hata": str(e)}

    @app.post("/prompt/cevir")
    async def prompt_cevir(istek: PromptCevirGirdi):
        try:
            sonuc = turkce_to_ingilizce(istek.metin)
            return {"basarili": bool(sonuc), "prompt": sonuc}
        except Exception as e:
            raise HTTPException(500, f"Çeviri hatası: {type(e).__name__}")

    @app.post("/prompt/gelistir")
    async def prompt_gelistir_ep(istek: PromptGelistirGirdi):
        try:
            sonuc = prompt_gelistir(istek.metin, istek.kategori)
            return {"basarili": bool(sonuc), "prompt": sonuc}
        except Exception as e:
            raise HTTPException(500, f"Geliştirme hatası: {type(e).__name__}")

    @app.post("/prompt/sihirbaz")
    async def prompt_sihirbaz(istek: PromptSihirbazGirdi):
        try:
            sonuc = sihirbaz_prompt_olustur(istek.girdi, istek.kategori)
            return {"basarili": bool(sonuc), "prompt": sonuc}
        except Exception as e:
            raise HTTPException(500, f"Sihirbaz hatası: {type(e).__name__}")

    # ========= MODEL/LORA IMPORT ENDPOINT'LERI =========
    # Klasor yapisi:
    #   cikti_kok/loralar/   → SDXL LoRA dosyalari (.safetensors + meta .json)
    #   cikti_kok/modeller/  → SDXL base model dosyalari (.safetensors + meta .json)
    (servis.cikti_kok / "modeller").mkdir(parents=True, exist_ok=True)

    # Buyuk dosya yuklemeleri icin: 100 MB+ destek (chunked stream)
    _UPLOAD_CHUNK = 4 * 1024 * 1024  # 4 MB

    async def _async_dosya_yaz(dosya: UploadFile, hedef: Path) -> int:
        """UploadFile -> hedef yola async yazar, toplam byte dondurur."""
        toplam = 0
        with hedef.open("wb") as f:
            while True:
                chunk = await dosya.read(_UPLOAD_CHUNK)
                if not chunk:
                    break
                f.write(chunk)
                toplam += len(chunk)
        return toplam

    def _guvenli_isim(ad: str) -> str:
        """Path traversal koruma — sadece dosya adi kismi."""
        return Path(ad or "model").name.replace("..", "_")

    @app.post("/modeller/lora_import")
    async def lora_import(
        dosya: UploadFile = File(...),
        isim: Optional[str] = Form(None),
        karakter_id: Optional[str] = Form(None),
        tetikleyici: Optional[str] = Form(None),
        cins: Optional[str] = Form("person"),
    ):
        """Internetten indirilen .safetensors LoRA dosyasini sisteme import eder."""
        try:
            ad = _guvenli_isim(dosya.filename or "lora.safetensors")
            if not ad.lower().endswith(".safetensors"):
                return {"basarili": False,
                        "hata": "Sadece .safetensors uzantisi desteklenir"}
            kok = servis.cikti_kok / "loralar"
            kok.mkdir(parents=True, exist_ok=True)
            lora_id = uuid.uuid4().hex[:16]
            hedef = kok / f"{lora_id}.safetensors"
            boyut = await _async_dosya_yaz(dosya, hedef)
            meta = {
                "id": lora_id,
                "isim": isim or Path(ad).stem,
                "karakter_id": karakter_id,
                "tetikleyici": tetikleyici or f"zwx_{lora_id[:6]}",
                "cins": cins or "person",
                "kaynak": "import",
                "orijinal_dosya": ad,
                "boyut_byte": boyut,
                "import_tarih": time.time(),
            }
            (kok / f"{lora_id}.json").write_text(
                json.dumps(_json_temizle(meta), ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
            return {"basarili": True, "lora_id": lora_id,
                    "yol": str(hedef), "meta": meta}
        except Exception as e:
            traceback.print_exc()
            return {"basarili": False, "hata": f"{type(e).__name__}: {e}"}

    @app.post("/modeller/base_import")
    async def base_import(
        dosya: UploadFile = File(...),
        isim: str = Form(...),
        tip: str = Form("sdxl"),
    ):
        """SDXL base model .safetensors dosyasini sisteme import eder."""
        try:
            ad = _guvenli_isim(dosya.filename or "model.safetensors")
            if not ad.lower().endswith(".safetensors"):
                return {"basarili": False,
                        "hata": "Sadece .safetensors uzantisi desteklenir"}
            isim_guv = _guvenli_isim(isim).replace(".safetensors", "")
            if not isim_guv:
                return {"basarili": False, "hata": "Gecersiz isim"}
            kok = servis.cikti_kok / "modeller"
            kok.mkdir(parents=True, exist_ok=True)
            hedef = kok / f"{isim_guv}.safetensors"
            if hedef.exists():
                return {"basarili": False,
                        "hata": f"Bu isimde model var: {isim_guv}"}
            boyut = await _async_dosya_yaz(dosya, hedef)
            meta = {
                "isim": isim_guv,
                "tip": tip,
                "kaynak": "import",
                "orijinal_dosya": ad,
                "boyut_byte": boyut,
                "import_tarih": time.time(),
            }
            (kok / f"{isim_guv}.json").write_text(
                json.dumps(_json_temizle(meta), ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
            return {"basarili": True, "isim": isim_guv,
                    "yol": str(hedef), "meta": meta}
        except Exception as e:
            traceback.print_exc()
            return {"basarili": False, "hata": f"{type(e).__name__}: {e}"}

    @app.post("/modeller/lora_import_url")
    async def lora_import_url(
        url: str = Form(...),
        isim: str = Form(...),
        karakter_id: Optional[str] = Form(None),
        tetikleyici: Optional[str] = Form(None),
        cins: Optional[str] = Form("person"),
    ):
        """HuggingFace veya benzeri direct file URL'den LoRA indirir.
        urllib ile streaming download — 100 MB+ destegi."""
        try:
            if not (url.startswith("http://") or url.startswith("https://")):
                return {"basarili": False, "hata": "Gecersiz URL"}
            kok = servis.cikti_kok / "loralar"
            kok.mkdir(parents=True, exist_ok=True)
            lora_id = uuid.uuid4().hex[:16]
            hedef = kok / f"{lora_id}.safetensors"

            # Bloklamayan indirme (thread executor)
            from urllib.request import urlopen, Request
            req = Request(url, headers={"User-Agent": "FikoYT-Importer/1.0"})

            def _indir():
                with urlopen(req, timeout=600) as r, hedef.open("wb") as f:
                    toplam = 0
                    while True:
                        chunk = r.read(_UPLOAD_CHUNK)
                        if not chunk:
                            break
                        f.write(chunk)
                        toplam += len(chunk)
                    return toplam

            loop = asyncio.get_event_loop()
            boyut = await loop.run_in_executor(None, _indir)

            meta = {
                "id": lora_id,
                "isim": isim,
                "karakter_id": karakter_id,
                "tetikleyici": tetikleyici or f"zwx_{lora_id[:6]}",
                "cins": cins or "person",
                "kaynak": "url",
                "url": url,
                "boyut_byte": boyut,
                "import_tarih": time.time(),
            }
            (kok / f"{lora_id}.json").write_text(
                json.dumps(_json_temizle(meta), ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
            return {"basarili": True, "lora_id": lora_id,
                    "yol": str(hedef), "meta": meta}
        except Exception as e:
            traceback.print_exc()
            return {"basarili": False, "hata": f"{type(e).__name__}: {e}"}

    @app.get("/modeller/list")
    async def modeller_list():
        """Sistemdeki tum LoRA + base modelleri listele."""
        try:
            lora_kok = servis.cikti_kok / "loralar"
            base_kok = servis.cikti_kok / "modeller"
            loralar: List[Dict[str, Any]] = []
            base_modeller: List[Dict[str, Any]] = []
            if lora_kok.exists():
                for f in sorted(lora_kok.glob("*.safetensors")):
                    meta_y = f.with_suffix(".json")
                    meta = {}
                    if meta_y.exists():
                        try:
                            meta = json.loads(
                                meta_y.read_text(encoding="utf-8"),
                            )
                        except Exception:
                            pass
                    loralar.append({
                        "id": f.stem,
                        "yol": str(f),
                        "boyut_mb": round(f.stat().st_size / 1e6, 1),
                        "meta": meta,
                    })
            if base_kok.exists():
                for f in sorted(base_kok.glob("*.safetensors")):
                    meta_y = f.with_suffix(".json")
                    meta = {}
                    if meta_y.exists():
                        try:
                            meta = json.loads(
                                meta_y.read_text(encoding="utf-8"),
                            )
                        except Exception:
                            pass
                    base_modeller.append({
                        "isim": f.stem,
                        "yol": str(f),
                        "boyut_mb": round(f.stat().st_size / 1e6, 1),
                        "meta": meta,
                    })
            return _json_temizle({
                "loralar": loralar,
                "base_modeller": base_modeller,
            })
        except Exception as e:
            return {"basarili": False, "hata": f"{type(e).__name__}: {e}"}

    @app.delete("/modeller/{tip}/{isim}")
    async def model_sil(tip: str, isim: str):
        """tip: 'lora' veya 'base'. isim path traversal'a karsi sanitize edilir."""
        try:
            if tip not in ("lora", "base"):
                return {"basarili": False,
                        "hata": "tip 'lora' veya 'base' olmali"}
            isim_guv = _guvenli_isim(isim).replace(".safetensors", "")
            if not isim_guv:
                return {"basarili": False, "hata": "Gecersiz isim"}
            kok = (servis.cikti_kok / "loralar") if tip == "lora" \
                else (servis.cikti_kok / "modeller")
            silindi = False
            for x in (kok / f"{isim_guv}.safetensors",
                      kok / f"{isim_guv}.json"):
                if x.exists():
                    try:
                        x.unlink()
                        silindi = True
                    except Exception as e:
                        print(f"[Sunucu] Silme hatasi: {e}")
            return {"basarili": silindi, "tip": tip, "isim": isim_guv}
        except Exception as e:
            return {"basarili": False, "hata": f"{type(e).__name__}: {e}"}

    # ========= YENI ENDPOINT —/json import =========
    import json as _json

    @app.get("/")
    async def kok():
        return PlainTextResponse(
            "FikoYT Görsel Üretim API\n"
            "GET /docs · /health · /models\n"
            "POST /generate · /upload\n"
            "GET /task/{id} · /image?path=...\n"
        )

    return app


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--host", default="127.0.0.1")
    parser.add_argument("--port", type=int, default=8002)
    parser.add_argument("--cikti", default=str(SUNUCU_KOK))
    args = parser.parse_args()

    import uvicorn
    app = app_olustur(Path(args.cikti))
    uvicorn.run(app, host=args.host, port=args.port, log_level="info")


if __name__ == "__main__":
    main()
