import hashlib
import logging
import secrets
from datetime import datetime, timedelta, timezone
from functools import wraps
from urllib.parse import urlencode

from flask import current_app, flash, redirect, session, url_for

from app import db
from app.models import Account
from app.services.errors import Forbidden
from app.tiktok_api import exchange_code, get_user_info, refresh_token

log = logging.getLogger(__name__)

REFRESH_AHEAD_MINUTES = 10


def pkce_pair():
    """A fresh PKCE code verifier (64 unreserved characters) and its challenge: TikTok wants the
    hex-encoded SHA-256 of the verifier, not the base64url form of RFC 7636."""
    verifier = secrets.token_urlsafe(48)
    return verifier, hashlib.sha256(verifier.encode("ascii")).hexdigest()


def account_allowed(open_id, union_id=None):
    """True when the sign-in allowlist is "*" (or empty outside production) or lists this account's open_id or union_id."""
    allowed = current_app.config.get("TIKTOK_ALLOWED_ACCOUNTS") or []
    if not allowed or "*" in allowed:
        return True
    return open_id in allowed or bool(union_id and union_id in allowed)


def get_auth_url():
    """Build the TikTok Login authorization URL with a per-flow state and PKCE verifier stored in the session."""
    state = secrets.token_hex(16)
    verifier, challenge = pkce_pair()
    session["oauth_state"] = state
    session["oauth_code_verifier"] = verifier
    config = current_app.config
    params = urlencode({
        "client_key": config["TIKTOK_CLIENT_KEY"],
        "scope": ",".join(config["TIKTOK_SCOPES"]),
        "response_type": "code",
        "redirect_uri": config["TIKTOK_REDIRECT_URI"],
        "state": state,
        "code_challenge": challenge,
        "code_challenge_method": "S256",
        "disable_auto_auth": "1",
    })
    return f"https://www.tiktok.com/v2/auth/authorize/?{params}"


def save_account_from_login(code, state):
    """Exchange the OAuth code for tokens, fetch the profile, and upsert the account; raises Forbidden on a bad state."""
    verifier = session.pop("oauth_code_verifier", None)
    if not code or not state or state != session.pop("oauth_state", None) or not verifier:
        raise Forbidden("Login session expired or the state did not match; try again.")

    config = current_app.config
    tokens = exchange_code(
        code, config["TIKTOK_CLIENT_KEY"], config["TIKTOK_CLIENT_SECRET"], config["TIKTOK_REDIRECT_URI"],
        code_verifier=verifier,
    )
    access_token = tokens["access_token"]
    user = get_user_info(access_token) or {}
    open_id = tokens.get("open_id") or user.get("open_id")
    union_id = user.get("union_id")
    if not account_allowed(open_id, union_id):
        log.warning("auth.connect refused not_in_allowlist")
        raise Forbidden(
            "This TikTok account is not allowed to use this app. "
            f"Ask the administrator to add this ID: {union_id or open_id}"
        )

    account = Account.query.filter_by(open_id=open_id).first()
    expires_at = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(seconds=int(tokens.get("expires_in", 86400)))
    if account:
        account.union_id = union_id or account.union_id
        account.display_name = user.get("display_name") or account.display_name
        account.avatar_url = user.get("avatar_url") or account.avatar_url
        account.access_token = access_token
        account.refresh_token = tokens["refresh_token"]
        account.scopes = tokens.get("scope") or account.scopes
        account.expires_at = expires_at
        _apply_stats(account, user)
    else:
        account = Account(
            open_id=open_id,
            union_id=union_id,
            display_name=user.get("display_name") or "Unknown",
            avatar_url=user.get("avatar_url"),
            access_token=access_token,
            refresh_token=tokens["refresh_token"],
            scopes=tokens.get("scope") or "",
            expires_at=expires_at,
        )
        _apply_stats(account, user)
        db.session.add(account)
    db.session.commit()
    log.info("auth.connect ok open_id=%s", account.open_id)
    return account


def _apply_stats(account, user):
    for field in ("follower_count", "following_count", "likes_count", "video_count"):
        if user.get(field) is not None:
            setattr(account, field, int(user[field]))


def ensure_fresh_token(account):
    """Return a valid access token, refreshing (and persisting the rotated tokens) when the
    current one is expired or about to expire. Raises TikTokAPIError when the refresh fails."""
    now = datetime.now(timezone.utc).replace(tzinfo=None)
    if account.expires_at and account.expires_at - timedelta(minutes=REFRESH_AHEAD_MINUTES) > now:
        return account.access_token_plain()

    config = current_app.config
    tokens = refresh_token(
        account.refresh_token_plain(), config["TIKTOK_CLIENT_KEY"], config["TIKTOK_CLIENT_SECRET"]
    )
    account.access_token = tokens["access_token"]
    account.refresh_token = tokens["refresh_token"]
    account.expires_at = now + timedelta(seconds=int(tokens.get("expires_in", 86400)))
    if tokens.get("scope"):
        account.scopes = tokens["scope"]
    db.session.commit()
    return account.access_token_plain()


def get_active_account():
    """The account active in this browser session; None when the visitor has not logged in."""
    account_id = session.get("active_account_id")
    if not account_id:
        return None
    return db.session.get(Account, account_id)


def login_required(f):
    """GUI guard: only a session with an active account passes; there is no first-account fallback."""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        account = get_active_account()
        if not account:
            flash("Please connect a TikTok account first.", "warning")
            return redirect(url_for("auth.login"))
        if not account_allowed(account.open_id, account.union_id):
            session.clear()
            flash("This TikTok account is no longer allowed to use this app.", "error")
            return redirect(url_for("auth.login"))
        return f(*args, **kwargs)
    return decorated_function
