"""Мульти-аккаунт Viktor с авто-failover по балансу кредитов.

Каждый аккаунт = свой Slack-DM с Viktor + свой JWT к api.viktor.com. Когда у
активного аккаунта кончаются кредиты (/api/team → 0), менеджер переключается на
следующий рабочий аккаунт. Конфиг — accounts.json (в gitignore), правится через
браузерный дашборд. Если accounts.json нет — сидируется одним аккаунтом из .env
(обратная совместимость).
"""
from __future__ import annotations

import json
import os
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from . import viktor_api
from .config import Settings
from .slack_connector import SlackConnector

# Поля, которые персистим в accounts.json (без рантайм-состояния).
_PERSIST = ("name", "slack_token", "slack_cookie_d", "viktor_dm", "viktor_user", "jwt", "enabled", "model", "reasoning_level")


@dataclass
class Account:
    name: str
    slack_token: str = ""
    slack_cookie_d: str = ""
    viktor_dm: str = ""
    viktor_user: str = ""
    jwt: str = ""
    enabled: bool = True
    model: str = ""             # выбранная модель (пусто = дефолт Viktor)
    reasoning_level: str = "N"  # уровень reasoning: N/L/M/H

    def to_dict(self) -> dict[str, Any]:
        return {k: getattr(self, k) for k in _PERSIST}


def _mask(secret: str) -> str:
    """Замаскировать секрет для отдачи в дашборд (не светим целиком)."""
    s = secret or ""
    if len(s) <= 10:
        return "•" * len(s)
    return f"{s[:6]}…{s[-4:]} ({len(s)})"


async def _find_viktor_dm(sc: SlackConnector) -> tuple[str, str]:
    """Найти DM-канал с ботом Viktor и его user_id по Slack-токену.

    Перебираем личные диалоги (conversations.list types=im) и ищем партнёра-бота
    с именем «Viktor». Возвращает (dm_channel_id, viktor_user_id) или ('','').
    """
    js = await sc._call("conversations.list", {"types": "im", "limit": "500"})
    fallback = ("", "")
    for ch in js.get("channels", []):
        uid = ch.get("user")
        if not uid:
            continue
        try:
            info = await sc._call("users.info", {"user": uid})
        except Exception:  # noqa: BLE001
            continue
        u = info.get("user", {}) or {}
        prof = u.get("profile", {}) or {}
        nm = (u.get("real_name") or u.get("name") or prof.get("real_name") or "").lower()
        if "viktor" in nm:
            if u.get("is_bot") or u.get("is_app_user"):
                return ch.get("id", ""), uid  # точное совпадение бота
            fallback = (ch.get("id", ""), uid)
    return fallback


class AccountManager:
    def __init__(self, path: str, settings: Settings) -> None:
        self.path = Path(path)
        self.settings = settings
        self.accounts: list[Account] = []
        self.active_name: str = ""
        self.mode: str = "auto"  # "auto" — failover по кредитам; "manual" — ручной выбор
        self._slack: dict[str, SlackConnector] = {}
        self._vapi: dict[str, viktor_api.ViktorApi] = {}
        self._credits: dict[str, tuple[float, int]] = {}  # name -> (monotonic_ts, credits)
        self._status: dict[str, str] = {}                 # name -> последний статус
        self.load()

    # ------------------------------------------------------------ персистенция
    def load(self) -> None:
        data: dict[str, Any] = {}
        if self.path.exists():
            try:
                data = json.loads(self.path.read_text(encoding="utf-8"))
            except Exception:  # noqa: BLE001
                data = {}
        self.accounts = []
        for a in data.get("accounts", []):
            if not a.get("name"):
                continue
            # миграция: reasoning_enabled (bool) → reasoning_level (str)
            if "reasoning_enabled" in a and "reasoning_level" not in a:
                a["reasoning_level"] = "H" if a["reasoning_enabled"] else "N"
            acct = Account(**{k: a[k] for k in _PERSIST if k in a})
            self.accounts.append(acct)
        self.active_name = data.get("active", "")
        self.mode = data.get("mode", "auto") if data.get("mode") in ("auto", "manual") else "auto"
        # Сидируем единственным аккаунтом из .env, если файла/аккаунтов нет.
        if not self.accounts and self.settings.slack_token:
            self.accounts = [Account(
                name="default",
                slack_token=self.settings.slack_token,
                slack_cookie_d=self.settings.slack_cookie_d,
                viktor_dm=self.settings.viktor_dm,
                viktor_user=self.settings.viktor_user,
                jwt=self.settings.viktor_jwt,
            )]
        if not self.active_name and self.accounts:
            self.active_name = self.accounts[0].name

    def save(self) -> None:
        payload = {"active": self.active_name, "mode": self.mode,
                   "accounts": [a.to_dict() for a in self.accounts]}
        tmp = self.path.with_suffix(".json.tmp")
        try:
            tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
            os.replace(tmp, self.path)
        except Exception:  # noqa: BLE001
            pass

    # ------------------------------------------------------------ доступ
    def get(self, name: str) -> Account | None:
        return next((a for a in self.accounts if a.name == name), None)

    def slack_for(self, acct: Account) -> SlackConnector:
        sc = self._slack.get(acct.name)
        if sc is None or sc.token != acct.slack_token or sc.cookie != acct.slack_cookie_d \
                or sc._dm != acct.viktor_dm or sc._user != acct.viktor_user:
            sc = SlackConnector(self.settings, token=acct.slack_token,
                                cookie=acct.slack_cookie_d, viktor_dm=acct.viktor_dm,
                                viktor_user=acct.viktor_user)
            self._slack[acct.name] = sc
        return sc

    def vapi_for(self, acct: Account) -> viktor_api.ViktorApi | None:
        if not acct.jwt:
            return None
        v = self._vapi.get(acct.name)
        if v is None or v.jwt != acct.jwt:
            v = viktor_api.ViktorApi(acct.jwt)
            self._vapi[acct.name] = v
        return v

    # ------------------------------------------------------------ кредиты / failover
    async def credits(self, acct: Account, force: bool = False) -> int | None:
        """Баланс кредитов аккаунта (кэш 30с). None — JWT нет/ошибка."""
        v = self.vapi_for(acct)
        if v is None:
            self._status[acct.name] = "нет JWT"
            return None
        now = time.monotonic()
        if not force:
            cached = self._credits.get(acct.name)
            if cached and now - cached[0] < 30:
                return cached[1]
        try:
            val = await v.credits()
            self._credits[acct.name] = (now, val)
            self._status[acct.name] = "ok"
            return val
        except Exception as e:  # noqa: BLE001
            self._status[acct.name] = f"ошибка API: {str(e)[:60]}"
            return None

    def _order_from(self, start: str, enabled: list[Account]) -> list[Account]:
        names = [a.name for a in enabled]
        if start in names:
            i = names.index(start)
            return enabled[i:] + enabled[:i]
        return enabled

    async def active(self, min_credits: int) -> Account | None:
        """Аккаунт для текущего запроса с учётом режима.

        manual — строго выбранный пользователем (без авто-переключения, даже если
        кредитов мало). auto — текущий активный, пока у него кредитов > min_credits,
        иначе крутим список по кругу до первого рабочего. None — все исчерпаны/нет.
        Если кредиты неизвестны (нет JWT / ошибка API) — аккаунт считается рабочим
        (иначе аккаунт без JWT никогда бы не выбрался). Факт «сменился ли аккаунт»
        считается выше (по сессии), не здесь.
        """
        enabled = [a for a in self.accounts if a.enabled]
        if not enabled:
            return None
        if self.mode == "manual":
            a = self.get(self.active_name)
            if a and a.enabled:
                return a
            self.active_name = enabled[0].name  # выбранный выключен/удалён -> первый
            self.save()
            return enabled[0]
        for a in self._order_from(self.active_name, enabled):  # auto + failover
            c = await self.credits(a)
            if c is not None and c <= min_credits:  # известный 0 -> пропускаем; None -> пробуем
                continue
            if a.name != self.active_name:
                self.active_name = a.name
                self.save()
            return a
        return None

    def set_mode(self, mode: str) -> bool:
        if mode not in ("auto", "manual"):
            return False
        self.mode = mode
        self.save()
        return True

    # ------------------------------------------------------------ CRUD (дашборд)
    def upsert(self, data: dict[str, Any]) -> Account:
        name = str(data.get("name", "")).strip()
        if not name:
            raise ValueError("имя аккаунта обязательно")
        old = str(data.get("old_name", "")).strip()
        if old and old != name and self.get(old):  # переименование существующего
            if self.get(name):
                raise ValueError(f"имя «{name}» уже занято")
            acct = self.get(old)
            acct.name = name
            if self.active_name == old:
                self.active_name = name
            self._invalidate(old)
        else:
            acct = self.get(name)
            if acct is None:
                acct = Account(name=name)
                self.accounts.append(acct)
        for k in _PERSIST:
            if k in data and k != "name":
                setattr(acct, k, data[k])
        if not self.active_name:
            self.active_name = acct.name
        self._invalidate(name)
        self.save()
        return acct

    def remove(self, name: str) -> bool:
        before = len(self.accounts)
        self.accounts = [a for a in self.accounts if a.name != name]
        if self.active_name == name:
            self.active_name = self.accounts[0].name if self.accounts else ""
        self._invalidate(name)
        self.save()
        return len(self.accounts) != before

    def set_active(self, name: str) -> bool:
        if self.get(name):
            self.active_name = name
            self.save()
            return True
        return False

    def _invalidate(self, name: str) -> None:
        self._credits.pop(name, None)
        self._credits.pop(f"models_{name}", None)  # кэш моделей
        self._slack.pop(name, None)
        self._vapi.pop(name, None)

    # ------------------------------------------------------------ детект (дашборд)
    async def detect(self, slack_token: str, slack_cookie_d: str, jwt: str) -> dict[str, Any]:
        """По введённым кредам вытащить email/баланс (JWT) и Viktor-DM/user (Slack-токен)."""
        out: dict[str, Any] = {}
        if jwt:
            v = viktor_api.ViktorApi(jwt)
            try:
                me = await v.me()
                out["email"] = me.get("email")
                out["name"] = me.get("display_name") or me.get("email")
                out["credits"] = await v.credits()
            except Exception as e:  # noqa: BLE001
                out["jwt_error"] = str(e)[:120]
            finally:
                await v.aclose()
        if slack_token:
            sc = SlackConnector(self.settings, token=slack_token, cookie=slack_cookie_d,
                                viktor_dm="-", viktor_user="-")
            try:
                a = await sc.auth_test()
                out["slack_user"] = a.get("user")
                out["slack_user_id"] = a.get("user_id")
                dm, vuser = await _find_viktor_dm(sc)
                out["viktor_dm"] = dm
                out["viktor_user"] = vuser
            except Exception as e:  # noqa: BLE001
                out["slack_error"] = str(e)[:120]
            finally:
                await sc.aclose()
        return out

    # ------------------------------------------------------------ управление моделью
    async def _sync_model_from_api(self, acct: Account) -> None:
        """Синхронизировать model + reasoning_level из API Viktor (декодируя composite ID)."""
        from .viktor_api import _decode_model_reasoning
        if not acct.jwt:
            return
        v = self.vapi_for(acct)
        if v is None:
            return
        try:
            ms = await v.get_model_settings()
            composite = ms.get("default_agent_model", "")
            if composite:
                model_id, rl = _decode_model_reasoning(composite)
                if model_id and rl in ("N", "L", "M", "H"):
                    if model_id != acct.model or rl != acct.reasoning_level:
                        print(f"[model] авто-синхронизация: ({acct.model!r},{acct.reasoning_level!r}) "
                              f"→ ({model_id!r},{rl!r})", flush=True)
                        acct.model = model_id
                        acct.reasoning_level = rl
                        self.save()
        except Exception as e:  # noqa: BLE001
            print(f"[model] авто-синхронизация: ошибка {e}", flush=True)

    async def get_models(self, name: str) -> dict[str, Any]:
        """Список доступных моделей (с именами и reasoning-уровнями) + текущие настройки."""
        acct = self.get(name)
        if not acct:
            return {"models": [], "current_model": "", "reasoning_level": "N", "error": "Аккаунт не найден"}
        if not acct.jwt:
            return {"models": [], "current_model": acct.model,
                    "reasoning_level": acct.reasoning_level, "error": "Нет JWT"}
        v = self.vapi_for(acct)
        if v is None:
            return {"models": [], "current_model": acct.model,
                    "reasoning_level": acct.reasoning_level, "error": "API недоступен"}
        try:
            # Синхронизируем текущую модель + reasoning из API
            await self._sync_model_from_api(acct)
            models = await v.get_available_models()
            existing_ids = {m["id"] for m in models}
            if acct.model and acct.model not in existing_ids:
                models.insert(0, {"id": acct.model, "name": acct.model,
                                  "reasoning": ["N", "L", "M", "H"]})
            return {"models": models, "current_model": acct.model,
                    "reasoning_level": acct.reasoning_level}
        except Exception as e:  # noqa: BLE001
            fallback = [{"id": acct.model, "name": acct.model,
                         "reasoning": ["N", "L", "M", "H"]}] if acct.model else []
            return {"models": fallback, "current_model": acct.model,
                    "reasoning_level": acct.reasoning_level, "error": str(e)[:120]}

    async def set_model(self, name: str, model: str = "",
                        reasoning_level: str | None = None) -> dict[str, Any]:
        """Установить модель и/или reasoning (→ API Viktor через кодирование в model_id).

        Viktor кодирует reasoning в ID модели суффиксом #ReasoningLevel:
        «claude-opus-4-8» + reasoning H → «claude-opus-4-8#ReasoningLevel:high»
        """
        from .viktor_api import _decode_model_reasoning

        acct = self.get(name)
        if not acct:
            return {"ok": False, "error": f"Аккаунт «{name}» не найден"}
        print(f"[model] set_model({name!r}, model={model!r}, rl={reasoning_level!r}) "
              f"current=({acct.model!r}, {acct.reasoning_level!r})", flush=True)

        # Определяем целевые значения
        new_model = model or acct.model
        new_rl: str

        # Preset-модели имеют фиксированный reasoning
        _PRESET_RL = {"smartest": "H", "balanced": "M"}
        if new_model in _PRESET_RL:
            new_rl = _PRESET_RL[new_model]
            print(f"[model]   preset {new_model!r} → reasoning {new_rl!r}", flush=True)
        else:
            new_rl = reasoning_level if reasoning_level is not None else acct.reasoning_level

        if new_model == acct.model and new_rl == acct.reasoning_level:
            return {"ok": True, "model": acct.model, "reasoning_level": acct.reasoning_level,
                    "api_result": "без изменений"}

        # Синхронизация с API Viktor: кодируем модель + reasoning в composite ID
        api_msg = ""
        if acct.jwt and new_model:
            v = self.vapi_for(acct)
            if v:
                try:
                    payload = {"default_agent_model": new_model,
                               "reasoning_level": new_rl}
                    print(f"[model]   → PUT {payload}", flush=True)
                    result = await v.update_model_settings(payload)
                    print(f"[model]   ← {result}", flush=True)
                    api_msg = "✓ сервер"
                    # Декодируем ответ API чтобы рассинхрона не было
                    if "default_agent_model" in result:
                        decoded_model, decoded_rl = _decode_model_reasoning(
                            result["default_agent_model"])
                        if decoded_model and decoded_rl in ("N", "L", "M", "H"):
                            new_model = decoded_model
                            new_rl = decoded_rl
                            print(f"[model]   API вернул: model={decoded_model!r} rl={decoded_rl!r}",
                                  flush=True)
                except Exception as e:  # noqa: BLE001
                    print(f"[model]   ✗ ошибка API: {e}", flush=True)
                    api_msg = f"⚠ сервер: {str(e)[:60]}"
        elif new_model:
            api_msg = "только локально (нет JWT)"

        # Сохраняем
        acct.model = new_model
        acct.reasoning_level = new_rl
        self._invalidate(name)
        self.save()
        print(f"[model]   → сохранено: model={acct.model!r} rl={acct.reasoning_level!r}", flush=True)
        return {"ok": True, "model": acct.model, "reasoning_level": acct.reasoning_level,
                "api_result": api_msg}

    # ------------------------------------------------------------ для дашборда
    async def _get_models_cached(self, acct: Account) -> list[dict]:
        """Список моделей с кэшированием (один запрос к API при промахе)."""
        cache_key = f"models_{acct.name}"
        now = time.monotonic()
        cached = self._credits.get(cache_key)  # переиспользуем словарь для кэша моделей
        if cached and now - cached[0] < 120:  # кэш 2 минуты
            return cached[1]
        v = self.vapi_for(acct)
        if v:
            try:
                models = await v.get_available_models()
                self._credits[cache_key] = (now, models)
                return models
            except Exception:  # noqa: BLE001
                pass
        return []

    async def snapshot(self, min_credits: int) -> list[dict[str, Any]]:
        """Список аккаунтов с балансом/статусом + моделями (для дашборда).

        Запросы к API — только для активного аккаунта. Для остальных — кэш.
        """
        rows = []
        for a in self.accounts:
            c = await self.credits(a) if a.enabled else None
            is_active = a.name == self.active_name

            # Модель: синхронизация ТОЛЬКО для активного аккаунта
            if a.jwt and is_active:
                try:
                    await self._sync_model_from_api(a)
                except Exception:  # noqa: BLE001
                    pass

            # Список моделей: API только для активного, для остальных — кэш
            avail: list[dict] = []
            if a.jwt:
                if is_active:
                    try:
                        avail = await self._get_models_cached(a)
                    except Exception:  # noqa: BLE001
                        avail = []
                else:
                    avail = self._get_cached_models_safe(a)
            if a.model and a.model not in [m["id"] for m in avail]:
                avail.insert(0, {"id": a.model, "name": a.model, "reasoning": ["N","L","M","H"]})
            rows.append({
                "name": a.name,
                "enabled": a.enabled,
                "active": is_active,
                "viktor_dm": a.viktor_dm,
                "viktor_user": a.viktor_user,
                "slack_token": _mask(a.slack_token),
                "slack_cookie_d": _mask(a.slack_cookie_d),
                "jwt": _mask(a.jwt),
                "has_jwt": bool(a.jwt),
                "has_slack": bool(a.slack_token),
                "credits": c,
                "exhausted": c is not None and c <= min_credits,
                "status": self._status.get(a.name, "—"),
                "model": a.model,
                "reasoning_level": a.reasoning_level,
                "available_models": avail,
            })
        return rows

    def _get_cached_models_safe(self, acct: Account) -> list[dict]:
        """Взять список моделей из кэша без запроса к API."""
        cache_key = f"models_{acct.name}"
        cached = self._credits.get(cache_key)
        if cached:
            return cached[1]
        return []

    async def aclose(self) -> None:
        for sc in self._slack.values():
            try:
                await sc.aclose()
            except Exception:  # noqa: BLE001
                pass
        for v in self._vapi.values():
            try:
                await v.aclose()
            except Exception:  # noqa: BLE001
                pass
