"""
senzib.integrations.amocrm.auth

Чистые помощники для OAuth 2.0 с amoCRM.

Модуль:
- НЕ знает про Flask, БД, app.config и т.п.
- НЕ сохраняет токены сам по себе.
- Только общается с amoCRM и возвращает словари с токенами.

Вы сами решаете, где хранить токены (БД, файл, Redis и т.д.).
"""

from __future__ import annotations

import time
import logging
from typing import Any, Dict, Mapping, Optional

import requests

logger = logging.getLogger(__name__)

# Если amo не вернул expires_in, используем сутки по умолчанию
DEFAULT_EXPIRES_IN = 86400


class AmoAuthError(Exception):
    """Ошибка при обмене кода / обновлении токенов amoCRM."""


TokenDict = Dict[str, Any]


def _compute_expires_at(expires_in: Optional[int]) -> int:
    """
    Возвращает unix-timestamp, когда истекает токен.
    Если expires_in не указан, берём DEFAULT_EXPIRES_IN.
    """
    return int(time.time()) + int(expires_in or DEFAULT_EXPIRES_IN)


def exchange_code_for_tokens(
    domain: str,
    client_id: str,
    client_secret: str,
    code: str,
    redirect_uri: str,
    *,
    timeout: int = 30,
) -> TokenDict:
    """
    Обмен authorization_code на набор токенов.

    :param domain: example.amocrm.ru
    :param client_id: ID интеграции (из настроек amoCRM)
    :param client_secret: Секрет интеграции
    :param code: authorization_code, который amoCRM прислал на redirect_uri
    :param redirect_uri: Тот же redirect_uri, что указан в интеграции
    :param timeout: таймаут HTTP-запроса в секундах
    :return: dict с токенами (access_token, refresh_token, token_type,
             expires_in, expires_at, и т.п.)
    :raises AmoAuthError: при неуспешном ответе amoCRM
    """
    url = f"https://{domain}/oauth2/access_token"
    payload = {
        "client_id": client_id,
        "client_secret": client_secret,
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": redirect_uri,
    }

    logger.info("Requesting amoCRM tokens via authorization_code for %s", domain)
    try:
        resp = requests.post(url, json=payload, timeout=timeout)
    except requests.RequestException as exc:
        raise AmoAuthError(f"Network error while requesting tokens: {exc}") from exc

    if resp.status_code != 200:
        raise AmoAuthError(
            f"Failed to exchange code for tokens: HTTP {resp.status_code}, body={resp.text}"
        )

    data: TokenDict = resp.json() or {}
    data["expires_at"] = _compute_expires_at(data.get("expires_in"))
    return data


def refresh_tokens(
    domain: str,
    client_id: str,
    client_secret: str,
    refresh_token: str,
    *,
    timeout: int = 30,
) -> TokenDict:
    """
    Обновление access_token по refresh_token.

    :param domain: example.amocrm.ru
    :param client_id: ID интеграции
    :param client_secret: Секрет интеграции
    :param refresh_token: refresh_token из ранее сохранённых токенов
    :param timeout: таймаут HTTP-запроса
    :return: dict с новым набором токенов (обновлённые access/refresh + expires_at)
    :raises AmoAuthError: при неуспешном ответе amoCRM
    """
    url = f"https://{domain}/oauth2/access_token"
    payload = {
        "client_id": client_id,
        "client_secret": client_secret,
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
    }

    logger.info("Refreshing amoCRM tokens for %s", domain)
    try:
        resp = requests.post(url, json=payload, timeout=timeout)
    except requests.RequestException as exc:
        raise AmoAuthError(f"Network error while refreshing tokens: {exc}") from exc

    if resp.status_code != 200:
        raise AmoAuthError(
            f"Failed to refresh tokens: HTTP {resp.status_code}, body={resp.text}"
        )

    data: TokenDict = resp.json() or {}
    data["expires_at"] = _compute_expires_at(data.get("expires_in"))
    return data


def is_token_expired(token_data: Mapping[str, Any], *, skew: int = 60) -> bool:
    """
    Проверяет, истёк ли токен (или истечёт в ближайшее время).

    :param token_data: dict с токенами (ожидается поле expires_at или expires_in)
    :param skew: секундный "запас" — считаем токен истёкшим чуть заранее
    :return: True, если токен нужно обновить
    """
    expires_at = token_data.get("expires_at")
    if expires_at is None:
        # Если библиотека получила только expires_in — вычислим "на лету"
        expires_in = token_data.get("expires_in")
        if expires_in is None:
            # Нету ни expires_at, ни expires_in — считаем, что токен "подозрительный", лучше обновить
            return True
        expires_at = _compute_expires_at(int(expires_in))

    try:
        exp_ts = int(expires_at)
    except (TypeError, ValueError):
        # Если там мусор — лучше тоже обновить
        return True

    now = int(time.time())
    return now >= (exp_ts - skew)


"""
ПРИМЕР ИСПОЛЬЗОВАНИЯ В ПРИЛОЖЕНИИ

from senzib.integrations.amocrm.auth import exchange_code_for_tokens, refresh_tokens, is_token_expired

tokens = exchange_code_for_tokens(domain, AMO_CLIENT_ID, AMO_CLIENT_SECRET, code, AMO_REDIRECT_URI)
save_tokens_to_db(account_id, tokens)

# позже:
tokens = load_tokens_from_db(account_id)
if is_token_expired(tokens):
    tokens = refresh_tokens(domain, AMO_CLIENT_ID, AMO_CLIENT_SECRET, tokens["refresh_token"])
    save_tokens_to_db(account_id, tokens)

access_token = tokens["access_token"]
"""