"""FikoYT ACE-Step LoRA Eğitim Sunucusu (REST API)
=====================================================
- Uvicorn FastAPI port 8003
- Subprocess ile ACE-Step train.py çağırır
- Endpointler: /health, /lora/dataset/upload, /lora/dataset/{id},
  /lora/egit, /lora/job/{id}, /lora/list, /lora/{id}, /lora/{id}/meta
- gorsel-uretim/sunucu.py pattern'i ile uyumlu
"""
from __future__ import annotations

# Windows console UTF-8 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 json
import math
import os
import shutil
import sys
import threading
import time
import traceback
import uuid
from pathlib import Path
from typing import Optional, List, Dict, Any

from fastapi import FastAPI, HTTPException, UploadFile, File, Form
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 ace_lora_egit import ace_lora_egit


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


# ===================== Pydantic Şemaları =====================
class LoraEgitGirdi(BaseModel):
    sanatci_id: str
    lora_isim: str
    dataset_klasoru: str
    base_model_variant: str = "turbo"
    train_mode: str = "fixed"  # fixed | vanilla
    epoch: int = 500
    batch_size: int = 1
    lr: float = 1e-4
    karakter_id: Optional[str] = None
    voice_clone_etkin: bool = False


class LoraMetaGuncelle(BaseModel):
    lora_isim: Optional[str] = None
    karakter_id: Optional[str] = None
    sanatci_id: Optional[str] = None
    notlar: Optional[str] = None


# ===================== FastAPI =====================
def app_olustur(cikti_kok: Path) -> FastAPI:
    cikti_kok.mkdir(parents=True, exist_ok=True)
    (cikti_kok / "loralar").mkdir(exist_ok=True)
    (cikti_kok / "yuklemeler").mkdir(exist_ok=True)
    (cikti_kok / "datasetler").mkdir(exist_ok=True)

    app = FastAPI(title="FikoYT ACE-Step LoRA Eğitim", version="0.1.0")
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # GPU kilidi: aynı anda tek eğitim
    gpu_kilit = threading.Lock()

    # İş veritabanı (dosya tabanlı persist)
    isler_db = 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"))
            for k, v in data.items():
                if v.get("durum") == 0:
                    v["durum"] = 2
                    v["hata"] = "Sunucu yeniden başlatıldı (eğitim kesildi)"
            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()

    # ========= EĞİTİM THREAD =========
    def _egit_thread(job_id: str, girdi: dict):
        is_obj = lora_isleri[job_id]
        gpu_kilit.acquire()
        try:
            cikti_klasor = cikti_kok / "loralar"
            cikti_yolu = cikti_klasor / f"{job_id}.safetensors"

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

            sonuc = ace_lora_egit(
                sanatci_id=girdi["sanatci_id"],
                lora_isim=girdi["lora_isim"],
                dataset_klasoru=girdi["dataset_klasoru"],
                cikti_yolu=str(cikti_yolu),
                base_model_variant=girdi.get("base_model_variant", "turbo"),
                train_mode=girdi.get("train_mode", "fixed"),
                epoch=girdi.get("epoch", 500),
                batch_size=girdi.get("batch_size", 1),
                lr=girdi.get("lr", 1e-4),
                karakter_id=girdi.get("karakter_id"),
                voice_clone_etkin=girdi.get("voice_clone_etkin", 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:
            traceback.print_exc()
            is_obj["durum"] = 2
            is_obj["hata"] = f"{type(e).__name__}: {e}"
        finally:
            try:
                gpu_kilit.release()
            except Exception:
                pass
            is_obj["bitis"] = time.time()
            _isleri_kaydet()

    # ========= ENDPOINTS =========
    @app.get("/")
    async def kok():
        return PlainTextResponse(
            "FikoYT ACE-Step LoRA Eğitim API\n"
            "GET /docs · /health · /lora/list\n"
            "POST /lora/egit · /lora/dataset/upload · /upload\n"
            "GET /lora/job/{id} · /lora/dataset/{sanatci_id}\n"
        )

    @app.get("/health")
    async def health():
        try:
            import torch
            cuda = torch.cuda.is_available()
        except Exception:
            cuda = False
        calisan = sum(1 for v in lora_isleri.values() if v.get("durum") == 0)
        return {
            "ok": True,
            "cuda": cuda,
            "calisan_is": calisan,
            "toplam_is": len(lora_isleri),
            "kok": str(cikti_kok),
        }

    # ----- Dataset upload -----
    @app.post("/lora/dataset/upload")
    async def dataset_upload(
        sanatci_id: str = Form(...),
        dosya: UploadFile = File(...),
        tip: str = Form("ses"),  # ses | lyrics | json
    ):
        """Sanatçı bazlı dataset klasörüne dosya yükler.
        ses → .mp3/.wav/...; lyrics → .lyrics.txt; json → .json"""
        if not sanatci_id or "/" in sanatci_id or "\\" in sanatci_id:
            raise HTTPException(400, "Geçersiz sanatci_id")
        klasor = cikti_kok / "datasetler" / sanatci_id
        klasor.mkdir(parents=True, exist_ok=True)
        ad = Path(dosya.filename or "dosya").name
        # Tip bazlı uzantı doğrulama
        ext = Path(ad).suffix.lower()
        if tip == "ses" and ext not in (".mp3", ".wav", ".flac", ".ogg", ".opus"):
            raise HTTPException(400, f"Desteklenmeyen ses formatı: {ext}")
        if tip == "lyrics" and not (ad.endswith(".lyrics.txt")
                                    or ext == ".txt"):
            raise HTTPException(400, "Lyrics dosyası .lyrics.txt olmalı")
        if tip == "json" and ext != ".json":
            raise HTTPException(400, "JSON uzantısı bekleniyor")
        hedef = klasor / ad
        with hedef.open("wb") as f:
            shutil.copyfileobj(dosya.file, f)
        return {
            "yol": str(hedef),
            "boyut": hedef.stat().st_size,
            "sanatci_id": sanatci_id,
            "tip": tip,
        }

    @app.get("/lora/dataset/{sanatci_id}")
    async def dataset_listele(sanatci_id: str):
        if "/" in sanatci_id or "\\" in sanatci_id:
            raise HTTPException(400, "Geçersiz sanatci_id")
        klasor = cikti_kok / "datasetler" / sanatci_id
        if not klasor.exists():
            return {"sanatci_id": sanatci_id, "klasor": str(klasor),
                    "dosyalar": []}
        dosyalar = []
        for f in sorted(klasor.iterdir()):
            if f.is_file():
                dosyalar.append({
                    "id": f.name,
                    "ad": f.name,
                    "boyut": f.stat().st_size,
                    "uzanti": f.suffix.lower(),
                    "yol": str(f),
                })
        return {
            "sanatci_id": sanatci_id,
            "klasor": str(klasor),
            "dosyalar": dosyalar,
        }

    @app.delete("/lora/dataset/{sanatci_id}/{file_id}")
    async def dataset_dosya_sil(sanatci_id: str, file_id: str):
        if "/" in sanatci_id or "\\" in sanatci_id:
            raise HTTPException(400, "Geçersiz sanatci_id")
        if "/" in file_id or "\\" in file_id or ".." in file_id:
            raise HTTPException(400, "Geçersiz file_id")
        hedef = cikti_kok / "datasetler" / sanatci_id / file_id
        if not hedef.exists() or not hedef.is_file():
            raise HTTPException(404, "Dosya yok")
        try:
            hedef.unlink()
            return {"silindi": True}
        except Exception as e:
            raise HTTPException(500, f"Silme hatası: {e}")

    # ----- Genel upload (ses/lyrics karışık) -----
    @app.post("/upload")
    async def upload_genel(
        dosya: UploadFile = File(...),
        alt_klasor: str = Form("yuklemeler"),
    ):
        if "/" in alt_klasor or "\\" in alt_klasor or ".." in alt_klasor:
            alt_klasor = "yuklemeler"
        hedef_kok = cikti_kok / alt_klasor
        hedef_kok.mkdir(parents=True, exist_ok=True)
        ad = Path(dosya.filename or "dosya").name
        hedef = hedef_kok / f"{int(time.time()*1000)}_{uuid.uuid4().hex[:6]}_{ad}"
        with hedef.open("wb") as f:
            shutil.copyfileobj(dosya.file, f)
        return {"yol": str(hedef), "boyut": hedef.stat().st_size}

    # ----- LoRA eğitim -----
    @app.post("/lora/egit")
    async def lora_egit_baslat(istek: LoraEgitGirdi):
        # Dataset doğrulama
        ds_yol = Path(istek.dataset_klasoru)
        if not ds_yol.is_absolute():
            ds_yol = cikti_kok / "datasetler" / istek.dataset_klasoru
        if not ds_yol.exists():
            raise HTTPException(400, f"Dataset klasörü yok: {ds_yol}")

        job_id = uuid.uuid4().hex[:16]
        girdi = istek.model_dump()
        girdi["dataset_klasoru"] = str(ds_yol)

        lora_isleri[job_id] = {
            "id": job_id,
            "sanatci_id": istek.sanatci_id,
            "lora_isim": istek.lora_isim,
            "base_variant": istek.base_model_variant,
            "train_mode": istek.train_mode,
            "epoch": istek.epoch,
            "durum": 0,
            "ilerleme": 0,
            "adim": 0,
            "toplam_adim": istek.epoch,
            "faz": "Hazırlanıyor",
            "son_loss": 0.0,
            "iptal": False,
            "baslama": time.time(),
        }
        _isleri_kaydet()
        threading.Thread(
            target=_egit_thread, args=(job_id, girdi), 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}

    # ----- LoRA listesi & yönetim -----
    @app.get("/lora/list")
    async def lora_listesi():
        kok = cikti_kok / "loralar"
        liste = []
        if kok.exists():
            for f in sorted(kok.glob("*.safetensors")):
                meta_yolu = f.with_suffix(".json")
                meta: Dict[str, Any] = {}
                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: LoraMetaGuncelle):
        kok = cikti_kok / "loralar"
        meta_yolu = kok / f"{lora_id}.json"
        if not meta_yolu.exists():
            # Otomatik oluştur — eski .safetensors'lara meta atayabilmek için
            sf = kok / f"{lora_id}.safetensors"
            if not sf.exists():
                raise HTTPException(404, "LoRA bulunamadı")
            mevcut: Dict[str, Any] = {}
        else:
            try:
                mevcut = json.loads(meta_yolu.read_text(encoding="utf-8"))
            except Exception:
                mevcut = {}
        for k, v in guncellemeler.model_dump(exclude_none=True).items():
            mevcut[k] = v
        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):
        if "/" in lora_id or "\\" in lora_id or ".." in lora_id:
            raise HTTPException(400, "Geçersiz lora_id")
        kok = 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}

    # ========= LORA IMPORT (internetten/yerel dosya) =========
    _UPLOAD_CHUNK = 4 * 1024 * 1024  # 4 MB

    def _guvenli_isim(ad: str) -> str:
        return Path(ad or "lora").name.replace("..", "_")

    @app.post("/lora/import")
    async def lora_import(
        dosya: UploadFile = File(...),
        isim: str = Form(...),
        karakter_id: Optional[str] = Form(None),
        sanatci_id: Optional[str] = Form(None),
        base_variant: str = Form("turbo"),
    ):
        """ACE-Step LoRA .safetensors dosyasini sisteme import eder.
        cikti_kok/loralar/{lora_id}.safetensors olarak kaydedilir."""
        try:
            ad = _guvenli_isim(dosya.filename or "lora.safetensors")
            if not ad.lower().endswith(".safetensors"):
                return {"basarili": False,
                        "hata": "Sadece .safetensors uzantisi desteklenir"}
            kok = cikti_kok / "loralar"
            kok.mkdir(parents=True, exist_ok=True)
            lora_id = uuid.uuid4().hex[:16]
            hedef = kok / f"{lora_id}.safetensors"
            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)
            meta = {
                "id": lora_id,
                "lora_isim": isim,
                "karakter_id": karakter_id,
                "sanatci_id": sanatci_id,
                "base_variant": base_variant,
                "kaynak": "import",
                "orijinal_dosya": ad,
                "boyut_byte": toplam,
                "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("/lora/import_url")
    async def lora_import_url(
        url: str = Form(...),
        isim: str = Form(...),
        karakter_id: Optional[str] = Form(None),
        sanatci_id: Optional[str] = Form(None),
        base_variant: str = Form("turbo"),
    ):
        """HuggingFace direct URL'den ACE LoRA indir."""
        try:
            if not (url.startswith("http://") or url.startswith("https://")):
                return {"basarili": False, "hata": "Gecersiz URL"}
            import asyncio as _aio
            from urllib.request import urlopen, Request
            kok = cikti_kok / "loralar"
            kok.mkdir(parents=True, exist_ok=True)
            lora_id = uuid.uuid4().hex[:16]
            hedef = kok / f"{lora_id}.safetensors"
            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 = _aio.get_event_loop()
            boyut = await loop.run_in_executor(None, _indir)
            meta = {
                "id": lora_id,
                "lora_isim": isim,
                "karakter_id": karakter_id,
                "sanatci_id": sanatci_id,
                "base_variant": base_variant,
                "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}"}

    return app


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

    import uvicorn
    app = app_olustur(Path(args.cikti))
    print(f"[Sunucu] http://{args.host}:{args.port}/docs")
    uvicorn.run(app, host=args.host, port=args.port, log_level="info")


if __name__ == "__main__":
    main()
