from __future__ import annotations

import json
import logging
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
from typing import Any, Callable

from niannian.config import load_providers_config
from niannian.storage.cloud_auth.https_urlopen import default_urlopen
from niannian.storage.manifest import utc_now_iso

logger = logging.getLogger(__name__)

DEFAULT_AUTHORIZE_URL = "https://openapi.baidu.com/oauth/2.0/authorize"
DEFAULT_TOKEN_URL = "https://openapi.baidu.com/oauth/2.0/token"
DEFAULT_REDIRECT_URI = "oob"
DEFAULT_SCOPE = "basic,netdisk"
HttpOpener = Callable[..., Any]


class BaiduOAuthError(Exception):
    pass


def resolve_baidu_oauth_config(config: dict[str, Any] | None = None) -> dict[str, str]:
    """合并 default.yaml / providers.local / 环境变量中的 OAuth 应用凭证。"""
    config = config or {}
    storage = config.get("storage") or {}
    cloud = storage.get("cloud") or {}
    baidu_cloud = cloud.get("baidu") or {}
    oauth_cfg = dict(baidu_cloud.get("oauth") or {})

    providers = load_providers_config()
    provider_oauth = (providers.get("baidu") or {}).get("oauth") or {}
    for key, value in provider_oauth.items():
        if value not in (None, ""):
            oauth_cfg[key] = value

    import os

    env_map = {
        "client_id": os.environ.get("BAIDU_OAUTH_CLIENT_ID") or os.environ.get("BAIDU_APP_KEY"),
        "client_secret": os.environ.get("BAIDU_OAUTH_CLIENT_SECRET") or os.environ.get("BAIDU_SECRET_KEY"),
        "redirect_uri": os.environ.get("BAIDU_OAUTH_REDIRECT_URI"),
    }
    for key, value in env_map.items():
        if value:
            oauth_cfg[key] = value

    client_id = str(oauth_cfg.get("client_id") or oauth_cfg.get("app_key") or "").strip()
    client_secret = str(oauth_cfg.get("client_secret") or oauth_cfg.get("secret_key") or "").strip()
    if not client_id or not client_secret:
        raise BaiduOAuthError("missing baidu oauth client_id/client_secret in providers.local.yaml")

    return {
        "client_id": client_id,
        "client_secret": client_secret,
        "redirect_uri": str(oauth_cfg.get("redirect_uri") or DEFAULT_REDIRECT_URI).strip(),
        "scope": str(oauth_cfg.get("scope") or DEFAULT_SCOPE).strip(),
        "authorize_url": str(oauth_cfg.get("authorize_url") or DEFAULT_AUTHORIZE_URL).strip(),
        "token_url": str(oauth_cfg.get("token_url") or DEFAULT_TOKEN_URL).strip(),
    }


def build_authorize_url(config: dict[str, Any] | None = None, *, state: str = "niannian") -> str:
    oauth = resolve_baidu_oauth_config(config)
    params = {
        "response_type": "code",
        "client_id": oauth["client_id"],
        "redirect_uri": oauth["redirect_uri"],
        "scope": oauth["scope"],
        "display": "popup",
        "state": state,
    }
    return f"{oauth['authorize_url']}?{urllib.parse.urlencode(params)}"


def _post_token(
    oauth: dict[str, str],
    params: dict[str, str],
    *,
    http_opener: HttpOpener | None = None,
) -> dict[str, Any]:
    opener = http_opener or default_urlopen
    body = urllib.parse.urlencode(params).encode("utf-8")
    req = urllib.request.Request(
        oauth["token_url"],
        data=body,
        method="POST",
        headers={"Content-Type": "application/x-www-form-urlencoded", "User-Agent": "niannian/0.2.8"},
    )
    try:
        with opener(req, timeout=30) as resp:
            raw = resp.read().decode("utf-8", "replace")
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", "replace") if exc.fp else str(exc)
        raise BaiduOAuthError(f"token HTTP {exc.code}: {detail[:300]}") from exc
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise BaiduOAuthError(f"invalid token JSON: {raw[:200]}") from exc
    if payload.get("error"):
        desc = payload.get("error_description") or payload.get("error")
        raise BaiduOAuthError(f"token error: {desc}")
    if not payload.get("access_token"):
        raise BaiduOAuthError(f"token response missing access_token: {raw[:200]}")
    return payload


def exchange_authorization_code(
    code: str,
    config: dict[str, Any] | None = None,
    *,
    http_opener: HttpOpener | None = None,
) -> dict[str, Any]:
    oauth = resolve_baidu_oauth_config(config)
    params = {
        "grant_type": "authorization_code",
        "code": code.strip(),
        "client_id": oauth["client_id"],
        "client_secret": oauth["client_secret"],
        "redirect_uri": oauth["redirect_uri"],
    }
    return _post_token(oauth, params, http_opener=http_opener)


def refresh_access_token(
    refresh_token: str,
    config: dict[str, Any] | None = None,
    *,
    http_opener: HttpOpener | None = None,
) -> dict[str, Any]:
    oauth = resolve_baidu_oauth_config(config)
    params = {
        "grant_type": "refresh_token",
        "refresh_token": refresh_token.strip(),
        "client_id": oauth["client_id"],
        "client_secret": oauth["client_secret"],
    }
    return _post_token(oauth, params, http_opener=http_opener)


def apply_token_response(entry: dict[str, Any], token_data: dict[str, Any]) -> dict[str, Any]:
    now = datetime.now(timezone.utc)
    expires_in = int(token_data.get("expires_in") or 2592000)
    entry["access_token"] = str(token_data["access_token"])
    refresh = token_data.get("refresh_token")
    if refresh:
        entry["refresh_token"] = str(refresh)
    entry["status"] = "connected"
    entry["auth_kind"] = "bearer"
    entry["expires_at"] = (now + timedelta(seconds=expires_in)).replace(microsecond=0).isoformat().replace("+00:00", "Z")
    entry["updated_at"] = utc_now_iso()
    entry.pop("error", None)
    return entry
