"""Клиент Битрикс24 REST: OAuth-обмен/refresh токенов и вызовы методов
под учёткой оператора (вариант Б)."""
from __future__ import annotations

import time
from typing import Any

import httpx

from config import settings, PortalConfig
import store


class BitrixError(RuntimeError):
    pass


def _portal_cfg(domain: str) -> PortalConfig:
    cfg = settings.portals.get(domain)
    if not cfg:
        raise BitrixError(f"Портал {domain} отсутствует в конфигурации")
    return cfg


async def exchange_code(domain: str, code: str) -> dict:
    """Обмен authorization_code на токены (срок жизни кода ~30 сек)."""
    cfg = _portal_cfg(domain)
    params = {
        "grant_type": "authorization_code",
        "client_id": cfg.client_id,
        "client_secret": cfg.client_secret,
        "code": code,
    }
    async with httpx.AsyncClient(timeout=20) as client:
        r = await client.get(settings.oauth_token_url, params=params)
    data = r.json()
    if "access_token" not in data:
        raise BitrixError(f"OAuth обмен не удался: {data}")
    store.upsert_portal_tokens(
        domain=domain,
        access=data["access_token"],
        refresh=data["refresh_token"],
        expires_in=data.get("expires_in", 3600),
        member_id=data.get("member_id"),
        app_token=data.get("application_token"),
    )
    return data


async def _refresh(domain: str) -> str:
    cfg = _portal_cfg(domain)
    p = store.get_portal(domain)
    if not p or not p.get("refresh_token"):
        raise BitrixError(f"Нет refresh-токена для {domain}; нужна повторная установка приложения")
    params = {
        "grant_type": "refresh_token",
        "client_id": cfg.client_id,
        "client_secret": cfg.client_secret,
        "refresh_token": p["refresh_token"],
    }
    async with httpx.AsyncClient(timeout=20) as client:
        r = await client.get(settings.oauth_token_url, params=params)
    data = r.json()
    if "access_token" not in data:
        raise BitrixError(f"Refresh не удался для {domain}: {data}")
    store.upsert_portal_tokens(
        domain=domain,
        access=data["access_token"],
        refresh=data["refresh_token"],
        expires_in=data.get("expires_in", 3600),
        member_id=data.get("member_id"),
        app_token=None,
    )
    return data["access_token"]


async def _access_token(domain: str) -> str:
    p = store.get_portal(domain)
    if not p:
        raise BitrixError(f"Портал {domain} не авторизован")
    if int(p.get("expires_at") or 0) <= int(time.time()):
        return await _refresh(domain)
    return p["access_token"]


async def call(domain: str, method: str, payload: dict[str, Any] | None = None,
               *, as_bot: bool = False) -> Any:
    """Вызов REST-метода. По умолчанию — под токеном оператора (вариант Б).

    Токен передаётся в теле запроса (никогда в URL — это требование приватности).
    """
    token = await _access_token(domain)
    body = dict(payload or {})
    body["auth"] = token
    url = f"{_portal_cfg(domain).rest_base}/{method}"
    async with httpx.AsyncClient(timeout=30) as client:
        r = await client.post(url, json=body)
    data = r.json()
    if isinstance(data, dict) and data.get("error"):
        # Истёкший токен — одна попытка обновиться и повторить.
        if data.get("error") in {"expired_token", "invalid_token"}:
            token = await _refresh(domain)
            body["auth"] = token
            async with httpx.AsyncClient(timeout=30) as client:
                r = await client.post(url, json=body)
            data = r.json()
        if isinstance(data, dict) and data.get("error"):
            raise BitrixError(f"{method}: {data.get('error')} {data.get('error_description', '')}")
    return data.get("result") if isinstance(data, dict) else data


# ---------- высокоуровневые обёртки ----------

async def whoami(domain: str) -> dict:
    return await call(domain, "profile")


async def recent_dialogs(domain: str) -> list[dict]:
    res = await call(domain, "im.recent.get", {"SKIP_OPENLINES": "Y"})
    # Формат может быть {"items": [...]} или просто [...] в зависимости от версии.
    if isinstance(res, dict):
        return res.get("items", [])
    return res or []


async def dialog_messages(domain: str, dialog_id: str, limit: int) -> list[dict]:
    res = await call(domain, "im.dialog.messages.get",
                     {"DIALOG_ID": dialog_id, "LIMIT": limit})
    if isinstance(res, dict):
        return res.get("messages", [])
    return res or []


async def send_as_user(domain: str, dialog_id: str, text: str) -> Any:
    """Отправка сообщения в чат ОТ ИМЕНИ ОПЕРАТОРА. Вызывается только после
    явного подтверждения."""
    return await call(domain, "im.message.add", {"DIALOG_ID": dialog_id, "MESSAGE": text})


# ---------- регистрация чат-бота согласования ----------

async def register_bot(domain: str) -> str:
    cfg = _portal_cfg(domain)
    res = await call(domain, "imbot.register", {
        "CODE": "draft_approver",
        "TYPE": "B",
        "EVENT_HANDLER": settings.event_handler_url,
        "OPENLINE": "N",
        "PROPERTIES": {"NAME": "Черновики", "WORK_POSITION": "Согласование ответов"},
        "CLIENT_ID": cfg.client_id,
    })
    bot_id = str(res)
    store.set_portal_field(domain, "bot_id", bot_id)
    # Команды для кнопок (HIDDEN=Y — только для клавиатуры).
    for cmd in ("approve", "edit", "skip"):
        try:
            await call(domain, "imbot.command.register", {
                "BOT_ID": bot_id,
                "COMMAND": cmd,
                "COMMON": "N",
                "HIDDEN": "Y",
                "EXTRANET_SUPPORT": "N",
                "CLIENT_ID": cfg.client_id,
                "LANG": [{"LANGUAGE_ID": "ru", "TITLE": cmd, "PARAMS": ""}],
                "EVENT_COMMAND_ADD": settings.event_handler_url,
            })
        except BitrixError:
            pass  # уже зарегистрировано
    return bot_id


async def bot_message(domain: str, dialog_id: str, text: str,
                      keyboard: list[dict] | None = None) -> Any:
    p = store.get_portal(domain)
    payload: dict[str, Any] = {"DIALOG_ID": dialog_id, "MESSAGE": text}
    if p and p.get("bot_id"):
        payload["BOT_ID"] = p["bot_id"]
    if keyboard:
        payload["KEYBOARD"] = keyboard
    return await call(domain, "imbot.message.add", payload)
