"""Долгоживущие токены Meta: обновление и предупреждение о протухании.

Токен живёт 60 дней и обновляется одним запросом — но только пока он ещё жив.
Пропустить окно значит идти переавторизовываться руками через браузер, поэтому
рефреш делается заранее, а не в последний день.
"""
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Callable

import requests

TOKEN_TTL_DAYS = 60
REFRESH_AFTER_DAYS = 50   # запас в 10 дней на неудачные попытки
ALARM_AFTER_DAYS = 53     # три прогона впустую — зовём человека
MAX_RETRIES = 3
BACKOFF_BASE_SECONDS = 2

REFRESH_URLS = {
    "instagram": "https://graph.instagram.com/refresh_access_token",
    "threads": "https://graph.threads.net/refresh_access_token",
}
GRANT_TYPES = {
    "instagram": "ig_refresh_token",
    "threads": "th_refresh_token",
}


class RefreshFailedError(RuntimeError):
    """Обновить токен не удалось."""


class NoTokenError(RuntimeError):
    """Токена нет в базе — нужна первичная авторизация через setup_auth.py."""


def _parse(moment: str) -> datetime:
    return datetime.fromisoformat(moment.replace("Z", "+00:00"))


def _age_days(issued_at: str, now: str) -> float:
    return (_parse(now) - _parse(issued_at)).total_seconds() / 86400


def needs_refresh(issued_at: str, now: str) -> bool:
    return _age_days(issued_at, now) >= REFRESH_AFTER_DAYS


def token_alarm(issued_at: str, now: str) -> bool:
    return _age_days(issued_at, now) >= ALARM_AFTER_DAYS


def refresh(db_path, platform: str, token: str, now: str,
            sleep: Callable[[float], None] = None, session=None) -> str:
    """Меняет живой токен на свежий. Старый в базе не трогает, пока не получен новый."""
    import db as db_module

    sleep = sleep if sleep is not None else __import__("time").sleep
    session = session or requests
    url = REFRESH_URLS[platform]
    last_error = ""
    for attempt in range(MAX_RETRIES):
        try:
            resp = session.get(url, params={
                "grant_type": GRANT_TYPES[platform], "access_token": token}, timeout=60)
            payload = resp.json()
        except (requests.RequestException, ValueError) as exc:
            last_error = str(exc).replace(token, "***")
        else:
            new_token = payload.get("access_token")
            if new_token:
                expires_at = (_parse(now) + timedelta(
                    seconds=payload.get("expires_in", TOKEN_TTL_DAYS * 86400))
                ).astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
                db_module.save_token(db_path, platform, new_token,
                                     issued_at=now, expires_at=expires_at)
                return new_token
            last_error = str((payload.get("error") or {}).get(
                "message", "ответ без access_token")).replace(token, "***")
        if attempt < MAX_RETRIES - 1:
            sleep(BACKOFF_BASE_SECONDS * (2 ** attempt))
    raise RefreshFailedError("{}: {}".format(platform, last_error))


def ensure_token(db_path, platform: str, now: str,
                 sleep: Callable[[float], None] = None, session=None):
    """Отдаёт рабочий токен и, если пора тревожиться, текст предупреждения.

    Неудачный рефреш сам по себе не повод падать: старый токен ещё живой, а
    следующий прогон попробует снова. Кричим, только когда запас подходит к
    концу — чтобы осталось время переавторизоваться руками.
    """
    import db as db_module

    row = db_module.load_token(db_path, platform)
    if row is None:
        raise NoTokenError(
            "нет токена для {} — пройди первичную авторизацию: "
            "python setup_auth.py {}".format(platform, platform))

    token, issued_at = row["token"], row["issued_at"]
    if not needs_refresh(issued_at, now):
        return token, None

    try:
        return refresh(db_path, platform, token, now, sleep=sleep, session=session), None
    except RefreshFailedError as exc:
        if token_alarm(issued_at, now):
            days_left = TOKEN_TTL_DAYS - _age_days(issued_at, now)
            return token, (
                "токен {} не обновляется ({}), протухнет через {} дн. — "
                "нужна переавторизация: python setup_auth.py {}".format(
                    platform, exc, int(days_left), platform))
        return token, None
