"""Клиент к внутреннему API Viktor (api.viktor.com) для статуса agent-run.

Надёжнее текстовых эвристик: у каждого треда (= нашего Slack-сообщения) есть
статус. Пока Viktor работает — статус null ('running'); когда выдал ВСЁ
(включая «детали ниже») — 'idle'. Авторизация — JWT (issuer zetalabs.ai, ~14 дней).
JWT протух/недоступен -> используем текстовый фолбэк в openai_proxy.
"""
from __future__ import annotations

import httpx

_BASE = "https://api.viktor.com"

# ── кодирование reasoning в model ID (как делает веб-интерфейс Viktor) ──
_VIDYA_PREFIX = "#ReasoningLevel:"  # префикс: model_id#ReasoningLevel:high
_RL_VK_TO_BRIDGE = {"none": "N", "low": "L", "medium": "M", "high": "H"}
_RL_BRIDGE_TO_VK = {"N": "none", "L": "low", "M": "medium", "H": "high"}


# Preset-модели Viktor (kind=preset) — у них reasoning фиксирован и не кодируется в ID
_PRESET_MODELS = {"smartest", "balanced"}


def _encode_model_with_reasoning(model_id: str, reasoning_level: str,
                                 is_preset: bool = False) -> str:
    """Закодировать модель + reasoning в строку API:
    encode('claude-opus-4-8', 'H') → 'claude-opus-4-8#ReasoningLevel:high'
    Для preset-моделей (smartest, balanced) reasoning не добавляется.
    """
    if is_preset or model_id in _PRESET_MODELS:
        return model_id
    if not reasoning_level or reasoning_level == "N":
        return model_id  # N = без суффикса
    vk_level = _RL_BRIDGE_TO_VK.get(reasoning_level, "none")
    return f"{model_id}{_VIDYA_PREFIX}{vk_level}"


def _decode_model_reasoning(composite: str) -> tuple[str, str]:
    """Раскодировать: 'claude-opus-4-8#ReasoningLevel:high' → ('claude-opus-4-8', 'H').
    Если суффикса нет → (composite, 'N').
    """
    idx = composite.find(_VIDYA_PREFIX)
    if idx < 0:
        return composite, "N"
    model_id = composite[:idx]
    level_raw = composite[idx + len(_VIDYA_PREFIX):]  # "high", "none", etc.
    # обработка "minimal" → "none"
    if level_raw == "minimal":
        level_raw = "none"
    rl = _RL_VK_TO_BRIDGE.get(level_raw, "N")
    return model_id, rl


class ViktorApi:
    def __init__(self, jwt: str) -> None:
        self.jwt = jwt
        self.http = httpx.AsyncClient(
            timeout=15, headers={"Authorization": f"Bearer {jwt}"}
        )

    async def aclose(self) -> None:
        await self.http.aclose()

    async def me(self) -> dict:
        """Профиль аккаунта (/api/me): email, preferences.slack_user_id и пр."""
        r = await self.http.get(f"{_BASE}/api/me")
        r.raise_for_status()
        return r.json()

    async def credits(self) -> int:
        """Доступные кредиты команды: credits_balance + reward_credits_balance.

        0 — аккаунт исчерпан (пора переключаться). Бросает при ошибке HTTP/авторизации.
        """
        r = await self.http.get(f"{_BASE}/api/team")
        r.raise_for_status()
        j = r.json()
        return int(j.get("credits_balance", 0) or 0) + int(j.get("reward_credits_balance", 0) or 0)

    async def thread_status(self, slack_ts: str) -> str | None:
        """Статус agent-run треда по ts нашего Slack-сообщения.

        Возвращает: None — тред не найден (ещё не создан / не наш);
        'running' — тред есть, Viktor работает; 'idle' — Viktor всё выдал.
        Бросает при ошибке HTTP/авторизации (вызывающий ловит и уходит в фолбэк).
        """
        key = slack_ts.replace(".", "_")
        r = await self.http.get(f"{_BASE}/api/flow/tasks?path=/")
        r.raise_for_status()
        for t in r.json().get("threads", []):
            if key in (t.get("path") or ""):
                return t.get("status") or "running"  # null во время работы
        return None

    async def _find_thread_id(self, slack_ts: str) -> str | None:
        """Найти ID треда по Slack-таймстемпу нашего сообщения."""
        key = slack_ts.replace(".", "_")
        r = await self.http.get(f"{_BASE}/api/flow/tasks?path=/")
        r.raise_for_status()
        for t in r.json().get("threads", []):
            if key in (t.get("path") or ""):
                return t.get("id")
        return None

    async def get_reasoning(self, slack_ts: str) -> list[dict[str, str]]:
        """Извлечь reasoning_summary из треда Viktor'а по Slack-таймстемпу."""
        tid = await self._find_thread_id(slack_ts)
        if not tid:
            return []
        r = await self.http.get(f"{_BASE}/api/threads/{tid}/messages")
        r.raise_for_status()
        messages = r.json()
        result: list[dict[str, str]] = []
        for m in messages:
            if m.get("role") != "reasoning":
                continue
            for s in m.get("summary", []):
                if s.get("type") == "reasoning_summary" and s.get("text"):
                    result.append({"id": m["id"], "text": s["text"]})
        return result

    async def get_model_settings(self) -> dict:
        """Текущие настройки: default_agent_model (чистый), reasoning_level (N/L/M/H), options."""
        r = await self.http.get(f"{_BASE}/api/coworker/model-settings")
        r.raise_for_status()
        raw = r.json()
        composite = raw.get("default_agent_model", "")
        model_id, rl = _decode_model_reasoning(composite)
        raw["default_agent_model_decoded"] = model_id
        raw["reasoning_level"] = rl
        return raw

    async def get_available_models(self) -> list[dict]:
        """Список доступных моделей из GET /api/coworker/model-settings → options."""
        try:
            ms = await self.get_model_settings()
            models = _parse_model_list(ms)
            if models:
                return models
            cur = ms.get("default_agent_model_decoded") or ms.get("default_agent_model") or ""
            if cur:
                return [{"id": cur, "name": _pretty_model_name(cur), "reasoning": ["N","L","M","H"]}]
        except Exception:  # noqa: BLE001
            pass
        return []

    async def debug_models(self) -> dict:
        """Отладка: сырые ответы всех модельных эндпоинтов."""
        result: dict = {"model_settings": None, "endpoints": {}}
        try:
            r = await self.http.get(f"{_BASE}/api/coworker/model-settings")
            result["model_settings"] = r.json() if r.status_code == 200 else f"HTTP {r.status_code}"
        except Exception as e:  # noqa: BLE001
            result["model_settings"] = str(e)
        for path in ("/api/coworker/models", "/api/coworker/agent-models", "/api/models",
                     "/api/coworker/settings", "/api/coworker/models/list"):
            try:
                r = await self.http.get(f"{_BASE}{path}")
                result["endpoints"][path] = r.json() if r.status_code == 200 else f"HTTP {r.status_code}"
            except Exception as e:  # noqa: BLE001
                result["endpoints"][path] = str(e)[:200]
        return result

    async def update_model_settings(self, data: dict) -> dict:
        """Обновить модель (+ reasoning) через PUT /api/coworker/model-settings.

        Принимает:
        - data["default_agent_model"] = "claude-opus-4-8" (чистый) + data["reasoning_level"] = "H"
        - ИЛИ data["default_agent_model"] = "claude-opus-4-8#ReasoningLevel:high" (уже composite)
        """
        model_id = data.get("default_agent_model") or data.get("model") or ""
        rl = data.get("reasoning_level") or data.get("reasoning_lvl") or ""
        if not model_id:
            raise RuntimeError("Нечего слать: нужен default_agent_model")

        # Если model_id уже содержит #ReasoningLevel: — декодируем и перекодируем
        clean_id, existing_rl = _decode_model_reasoning(model_id)
        if existing_rl != "N":
            model_id = clean_id
            if not rl:
                rl = existing_rl

        composite = _encode_model_with_reasoning(model_id, rl)
        print(f"[viktor_api] PUT /coworker/model-settings ← {composite!r}", flush=True)
        for method in ("PUT", "PATCH"):
            try:
                r = await self.http.request(
                    method, f"{_BASE}/api/coworker/model-settings",
                    json={"default_agent_model": composite}
                )
                if r.status_code in (200, 201, 204):
                    try:
                        return r.json()
                    except Exception:  # noqa: BLE001
                        return {"status": "ok"}
            except Exception:  # noqa: BLE001
                continue
        raise RuntimeError("Не удалось обновить модель")


# ── парсинг списка моделей из ответа API ──

# Известные красивые имена моделей (id → display name)
_KNOWN_MODEL_NAMES: dict[str, str] = {
    "smartest": "Smartest (Opus 4.6)",
    "balanced": "Balanced (Sonnet 4.6)",
    "claude-opus-4-6": "Claude Opus 4.6",
    "claude-opus-4.6": "Claude Opus 4.6",
    "claude-opus-4-7": "Claude Opus 4.7",
    "claude-opus-4.7": "Claude Opus 4.7",
    "claude-opus-4-8": "Claude Opus 4.8",
    "claude-opus-4.8": "Claude Opus 4.8",
    "claude-sonnet-4-6": "Claude Sonnet 4.6",
    "claude-sonnet-4.6": "Claude Sonnet 4.6",
    "gpt-5.4": "GPT-5.4",
    "gpt-5-4": "GPT-5.4",
    "gpt-5.5": "GPT-5.5",
    "gpt-5-5": "GPT-5.5",
    "gemini-flash-3": "Gemini Flash 3",
    "gemini-2.5-flash": "Gemini Flash 3",
    "kimi-k2.6": "Kimi K2.6",
    "kimi-k2-6": "Kimi K2.6",
}


def _pretty_model_name(raw: str) -> str:
    """Красивое имя модели. Сначала словарь, потом эвристика."""
    if raw in _KNOWN_MODEL_NAMES:
        return _KNOWN_MODEL_NAMES[raw]
    # убираем версионный суффикс (-20250514) и префикс провайдера если есть
    import re
    s = re.sub(r"-\d{8,}$", "", raw)
    s = re.sub(r"^(anthropic/|openai/|google/)", "", s)
    # делаем заглавные буквы
    s = re.sub(r"-+", " ", s).strip()
    s = " ".join(w[0].upper() + w[1:] if w.islower() or w.isupper() else w for w in s.split())
    return s if s else raw


def _parse_model_list(data: Any) -> list[dict]:
    """Структурно извлечь список моделей из ответа API.

    Ищет МАССИВЫ объектов, у которых есть поля id/model/name.
    НЕ делает wildcard-поиск по строкам.
    """
    if isinstance(data, list):
        models: list[dict] = []
        for entry in data:
            if isinstance(entry, dict):
                mid, mname, kind = _extract_model_entry(entry)
                if mid:
                    reasoning = _extract_reasoning_levels(entry)
                    entry_dr = entry.get("default_reasoning", "")
                    default_rl = _RL_VK_TO_BRIDGE.get(entry_dr, "M")
                    models.append({"id": mid, "name": mname, "reasoning": reasoning,
                                   "default_reasoning": default_rl, "kind": kind})
        return models
    if isinstance(data, dict):
        # ищем массивы внутри объекта
        for key in ("models", "data", "items", "list", "available_models",
                     "agent_models", "model_options", "options"):
            val = data.get(key)
            if isinstance(val, list) and val:
                result = _parse_model_list(val)
                if result:
                    return result
        # рекурсивно в других ключах
        for k, v in data.items():
            if k in ("models", "data", "items", "list"):
                continue  # уже проверили
            sub = _parse_model_list(v)
            if sub:
                return sub
    return []


def _extract_model_entry(obj: dict) -> tuple[str, str, str]:
    """Извлечь (id, display_name, kind) из объекта модели.
    kind: 'preset' | 'model' | ''
    """
    mid = ""
    for f in ("id", "model", "model_id", "agent_model", "code", "slug", "key"):
        v = obj.get(f)
        if isinstance(v, str) and v.strip():
            mid = v.strip()
            break
    if not mid:
        return "", "", ""
    # display name
    name = ""
    for f in ("display_name", "name", "label", "title"):
        v = obj.get(f)
        if isinstance(v, str) and v.strip():
            name = v.strip()
            break
    if not name:
        name = _pretty_model_name(mid)
    kind = obj.get("kind", "") or ""
    return mid, name, kind


def _extract_reasoning_levels(obj: dict) -> list[str]:
    """Извлечь доступные уровни reasoning из объекта модели.

    Viktor API возвращает reasoning_levels как массив строк: ["none","low","medium","high"].
    Возвращаем в формате моста: ["N", "L", "M", "H"].
    """
    for f in ("reasoning_levels", "reasoning", "reasoning_options", "thinking_levels"):
        v = obj.get(f)
        if isinstance(v, list):
            levels: list[str] = []
            for lv in v:
                if isinstance(lv, str):
                    # Viktor использует полные слова: "none", "low", "medium", "high"
                    bridge = _RL_VK_TO_BRIDGE.get(lv, lv.upper()[:1] if len(lv) <= 1 else lv)
                    levels.append(bridge)
                elif isinstance(lv, dict):
                    lvid = lv.get("id") or lv.get("level") or lv.get("value") or ""
                    if lvid:
                        bridge = _RL_VK_TO_BRIDGE.get(lvid.lower(), lvid.upper()[:1])
                        levels.append(bridge)
            return levels if levels else ["N", "L", "M", "H"]
    return ["N", "L", "M", "H"]  # дефолт: все 4 уровня
