"""Görev bazlı LLM yönlendirme — model, sağlayıcı ve veri sınırı tek yerde.

Neden bu dosya var:

1. **Model seçimi dağınıktı.** Model kimlikleri 8 ayrı dosyada string literal
   olarak gömülüydü (`model="claude-haiku-4-5"`). Maliyet ayarı yapmak için
   sekiz dosyayı taramak gerekiyordu; artık tek kayıt (`_TASKS`) var.

2. **Veri sınırı koda gömülü olmalı, yoruma değil.** Patent taslakları
   YAYINLANMAMIŞ buluş açıklamasıdır: başvuru öncesi ifşa yenilik (novelty)
   kaybına yol açabilir ve KVKK/VERBIS yükümlülüğü doğurur. Bu yüzden
   `CONFIDENTIAL` işaretli hiçbir görev Anthropic dışına çıkamaz — kural
   `TaskSpec.__post_init__` içinde import anında doğrulanır, yani hatalı bir
   yönlendirme kod review'a kalmadan uygulama açılışında patlar.

   Yalnızca `PUBLIC` görevler (zaten yayımlanmış patent dokümanları) başka
   sağlayıcıya gidebilir.

3. **cache_control sessizce ölebiliyordu.** Her modelin minimum cache'lenebilir
   prefix uzunluğu farklı; eşiğin altındaki bir system prompt'a `cache_control`
   koymak hata vermez, sadece hiçbir şey yapmaz. `cache_control_for()` bunu
   açık bir karara çevirir.
"""

from __future__ import annotations

import os
from dataclasses import dataclass, replace
from enum import StrEnum


class Sensitivity(StrEnum):
    """Görevin işlediği verinin gizlilik sınıfı."""

    CONFIDENTIAL = "confidential"
    """Yayınlanmamış müvekkil/buluş verisi. Anthropic dışına ÇIKAMAZ."""

    PUBLIC = "public"
    """Zaten yayımlanmış içerik (Espacenet/Google Patents kayıtları vb.)."""


class Provider(StrEnum):
    """LLM sağlayıcısı."""

    ANTHROPIC = "anthropic"
    GOOGLE = "google"


# Modelin minimum cache'lenebilir prefix uzunluğu (token).
#
# Bu eşiğin ALTINDAKİ bir system prompt'a `cache_control` koymak hata vermez —
# API sessizce cache yazmaz (`cache_creation_input_tokens: 0`). Haiku 4.5'in
# eşiği tüm ailenin en yükseği (4096) olduğu için kısa promptlu Haiku
# rotalarında caching fiilen imkânsızdır; Opus 5 ise 512'ye indirir.
_CACHE_FLOOR: dict[str, int] = {
    "claude-opus-5": 512,
    "claude-opus-4-8": 1024,
    "claude-sonnet-5": 1024,
    "claude-sonnet-4-6": 1024,
    "claude-sonnet-4-5": 1024,
    "claude-opus-4-7": 2048,
    "claude-opus-4-6": 4096,
    "claude-haiku-4-5": 4096,
}

_DEFAULT_CACHE_FLOOR = 4096
"""Bilinmeyen model → en muhafazakâr (en yüksek) eşik."""


@dataclass(frozen=True)
class TaskSpec:
    """Bir LLM görevinin yönlendirme kaydı."""

    task: str
    """Kararlı görev kimliği. `record_usage(endpoint=...)` ile aynı string —
    admin panelindeki maliyet kırılımı buna dayanır, yeniden adlandırma kırar."""

    provider: Provider
    model: str
    sensitivity: Sensitivity
    max_tokens: int
    note: str = ""

    def __post_init__(self) -> None:
        # Hukuki sınırın tek zorlayıcısı burasıdır. Yorum değil, invariant.
        if self.sensitivity is Sensitivity.CONFIDENTIAL and self.provider is not Provider.ANTHROPIC:
            raise ValueError(
                f"Görev {self.task!r} CONFIDENTIAL işaretli ama {self.provider} "
                "sağlayıcısına yönlendirilmiş. Yayınlanmamış buluş verisi "
                "Anthropic dışına çıkamaz (bkz. .claude/CLAUDE.md § Hukuki Kurallar). "
                "Sağlayıcı değişikliği veri sınırı değişikliğidir — kod review gerektirir."
            )
        if self.max_tokens <= 0:
            raise ValueError(f"Görev {self.task!r} için max_tokens pozitif olmalı.")


def _spec(
    task: str,
    provider: Provider,
    model: str,
    sensitivity: Sensitivity,
    max_tokens: int,
    note: str = "",
) -> TaskSpec:
    return TaskSpec(
        task=task,
        provider=provider,
        model=model,
        sensitivity=sensitivity,
        max_tokens=max_tokens,
        note=note,
    )


_OPUS = "claude-opus-5"
_SONNET = "claude-sonnet-4-6"
_HAIKU = "claude-haiku-4-5"
_GEMINI_FLASH = "gemini-2.5-flash"


# Görev kaydı. Model katmanlaması .claude/CLAUDE.md § "İzin Verilen LLM
# Modelleri" kuralına uyar: karmaşık iş Opus, orta iş Sonnet, basit iş Haiku.
_TASKS: dict[str, TaskSpec] = {
    # ---- Tarifname / istem / özet üretimi — müvekkil buluşu, en gizli ----
    "drafting.stream": _spec(
        "drafting.stream",
        Provider.ANTHROPIC,
        _OPUS,
        Sensitivity.CONFIDENTIAL,
        16000,
        "Tarifname taslağı. En yüksek kalite gereken iş; buluşun tamamı prompt'ta.",
    ),
    "drafting.claims": _spec(
        "drafting.claims",
        Provider.ANTHROPIC,
        _SONNET,
        Sensitivity.CONFIDENTIAL,
        8000,
        "İstem üretimi. Yapılandırılmış çıktı; Sonnet yeterli.",
    ),
    "drafting.abstract": _spec(
        "drafting.abstract",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        2000,
        "Özet (max 150 kelime). Tarifname zaten hazır, sıkıştırma işi.",
    ),
    "drafting.title_suggestions": _spec(
        "drafting.title_suggestions",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        1500,
        "Buluş başlığı önerileri.",
    ),
    # ---- Intake formu — mucit anlatımı, gizli ----
    "intake.suggestions": _spec(
        "intake.suggestions",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        1024,
        "Form chip önerileri.",
    ),
    "intake.draft_answer": _spec(
        "intake.draft_answer",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        1500,
        "Form alanı AI taslak yanıtı.",
    ),
    # ---- Şekil / şekli inceleme / itiraz — başvuru içeriği, gizli ----
    "figure_extraction": _spec(
        "figure_extraction",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        4000,
        "Tarifnameden şekil referansı ayıklama.",
    ),
    "formality.foreign_terms": _spec(
        "formality.foreign_terms",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        2000,
        "Yabancı terim tespiti. Deterministik kurallar LLM'siz çalışır, bu ek katman.",
    ),
    "objection.extract": _spec(
        "objection.extract",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        4096,
        "İtiraz/Office Action PDF ayıklama. Başvuru numarası + eksiklik maddeleri.",
    ),
    # ---- Prior art ----
    "prior_art.analyze": _spec(
        "prior_art.analyze",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        4000,
        "Buluşu prior-art ile karşılaştırır — buluş açıklaması prompt'ta, gizli.",
    ),
    "translation.query": _spec(
        "translation.query",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        200,
        "Arama sorgusu çevirisi. Sorgu buluş anahtar kelimelerini içerir — "
        "hangi konuda çalışıldığını ifşa eder, bu yüzden PUBLIC DEĞİL.",
    ),
    # ---- Tek PUBLIC görev ----
    "translation.hits": _spec(
        "translation.hits",
        Provider.GOOGLE,
        _GEMINI_FLASH,
        Sensitivity.PUBLIC,
        3000,
        "Espacenet/Google Patents başlık+özet çevirisi. Girdi zaten YAYIMLANMIŞ "
        "patent dokümanı; müvekkil verisi içermez. Toplu, mekanik, yüksek hacim — "
        "Gemini Flash'a yönlendirilen tek görev.",
    ),
    # ---- Sağlık kontrolü ----
    "llm.echo": _spec(
        "llm.echo",
        Provider.ANTHROPIC,
        _HAIKU,
        Sensitivity.CONFIDENTIAL,
        1024,
        "Echo/health endpoint.",
    ),
}


class UnknownTaskError(KeyError):
    """Kayıtlı olmayan görev kimliği."""


def _env_model_override(task: str) -> str | None:
    """`PY_MODEL_<GOREV>` ortam değişkeni ile model override.

    Örn: `PY_MODEL_DRAFTING_STREAM=claude-opus-4-8`

    Yalnızca MODEL override edilebilir; sağlayıcı edilemez. Sağlayıcı
    değiştirmek veri sınırını değiştirmektir ve ortam değişkeniyle
    yapılamamalıdır — kod değişikliği + review gerektirir.
    """
    key = "PY_MODEL_" + task.upper().replace(".", "_")
    value = os.environ.get(key)
    return value.strip() if value and value.strip() else None


def resolve(task: str) -> TaskSpec:
    """Görev kimliğinden yönlendirme kaydını döndür.

    Raises:
        UnknownTaskError: görev kayıtlı değilse.
    """
    try:
        spec = _TASKS[task]
    except KeyError as exc:
        raise UnknownTaskError(
            f"Bilinmeyen LLM görevi: {task!r}. Kayıtlı görevler: {', '.join(sorted(_TASKS))}"
        ) from exc

    override = _env_model_override(task)
    if override and override != spec.model:
        # Override edilmiş spec de aynı invariant'tan geçer.
        spec = replace(spec, model=override)
    return spec


def all_tasks() -> dict[str, TaskSpec]:
    """Tüm kayıt — admin/teşhis amaçlı kopya."""
    return dict(_TASKS)


def cache_control_for(model: str, prompt: str) -> dict[str, str] | None:
    """System prompt cache eşiğini geçiyorsa `cache_control` bloğu döndür.

    Eşiğin altındaki prompt'a marker koymak zararsızdır ama YANILTICIDIR:
    kod caching yapıldığını ima eder, API sessizce yapmaz. Bu fonksiyon
    kararı açık hale getirir.

    Token sayısı yerel olarak ölçülemediği için karakter tabanlı bir tahmin
    kullanılır. Tahmin bilerek YÜKSEK tarafa sapar (len/2.0; Türkçe pratikte
    ~3 karakter/token): yanlışlıkla marker DÜŞÜRMEK gerçek para kaybettirir
    (cache okuma %90 ucuz), yanlışlıkla marker BIRAKMAK ise bedelsizdir.
    """
    floor = _CACHE_FLOOR.get(model, _DEFAULT_CACHE_FLOOR)
    estimated_tokens = len(prompt) / 2.0
    if estimated_tokens < floor:
        return None
    return {"type": "ephemeral"}


def system_block(model: str, prompt: str) -> list[dict[str, object]]:
    """Anthropic `system=` parametresi için tek bloklu liste üretir.

    cache_control yalnızca gerçekten işe yarayacaksa eklenir.
    """
    block: dict[str, object] = {"type": "text", "text": prompt}
    cc = cache_control_for(model, prompt)
    if cc is not None:
        block["cache_control"] = cc
    return [block]
