import base64
import inspect
import time
import urllib.parse
import webbrowser
from collections.abc import Callable
from typing import Any

import aiohttp

from otto.log import get_logger

from .pkce import generate_challenge, generate_state, generate_verifier

# === Constants ===

_CLIENT_ID = base64.b64decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl").decode("ascii")
_AUTHORIZE_URL = "https://claude.ai/oauth/authorize"
_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"
_SCOPES = "org:create_api_key user:profile user:inference"
_EXPIRY_BUFFER_SECONDS = 300

log = get_logger(__name__)


# === Helpers ===


async def _maybe_await(value: Any) -> Any:
    if inspect.isawaitable(value):
        return await value
    return value


def _safe_code(value: str) -> str:
    value = value.strip()
    if len(value) <= 8:
        return "***"
    return f"{value[:4]}...{value[-4:]}"


def _extract_code_and_state(raw_value: Any) -> tuple[str, str]:
    raw = str(raw_value or "").strip()
    if not raw:
        raise RuntimeError("No authorization code provided.")

    parsed = urllib.parse.urlparse(raw)
    query_data = urllib.parse.parse_qs(parsed.query)
    fragment_data = urllib.parse.parse_qs(parsed.fragment)
    query_or_fragment = query_data or fragment_data

    code = query_or_fragment.get("code", [""])[0].strip()
    state = query_or_fragment.get("state", [""])[0].strip()
    if code:
        return code, state

    if "code=" in raw:
        flattened = raw.lstrip("?#").replace("#", "&")
        loose = urllib.parse.parse_qs(flattened)
        code = loose.get("code", [""])[0].strip()
        state = loose.get("state", [""])[0].strip()
        if code:
            return code, state

    if "#" in raw and "=" not in raw:
        code, state = raw.split("#", 1)
        return code.strip(), state.strip()

    return raw, ""


def _build_authorize_url(challenge: str, state: str) -> str:
    params = urllib.parse.urlencode(
        {
            "code": "true",
            "client_id": _CLIENT_ID,
            "response_type": "code",
            "redirect_uri": _REDIRECT_URI,
            "scope": _SCOPES,
            "code_challenge": challenge,
            "code_challenge_method": "S256",
            "state": state,
        }
    )
    return f"{_AUTHORIZE_URL}?{params}"


def _default_open_browser(url: str) -> None:
    print(f"Open this URL in your browser: {url}")
    try:
        webbrowser.open(url)
    except Exception:
        # User can still paste redirect URL manually.
        pass


async def _exchange_token(payload: dict[str, Any], flow_name: str) -> dict[str, Any]:
    async with aiohttp.ClientSession() as session:
        async with session.post(
            _TOKEN_URL,
            json=payload,
            headers={"Content-Type": "application/json"},
        ) as response:
            if response.status != 200:
                text = await response.text()
                raise RuntimeError(f"Anthropic {flow_name} failed ({response.status}): {text}")
            return await response.json()


def _to_credentials(data: dict[str, Any], refresh: str | None = None) -> dict[str, Any]:
    access = str(data.get("access_token", "")).strip()
    refresh_token_value = str(data.get("refresh_token") or refresh or "").strip()

    if not access:
        raise RuntimeError("Anthropic OAuth response did not include access_token.")
    if not refresh_token_value:
        raise RuntimeError("Anthropic OAuth response did not include refresh_token.")

    expires_in = data.get("expires_in")
    try:
        expires_seconds = int(expires_in)
    except (TypeError, ValueError) as exc:
        raise RuntimeError("Anthropic OAuth response did not include valid expires_in.") from exc

    return {
        "provider": "anthropic",
        "type": "oauth",
        "refresh": refresh_token_value,
        "access": access,
        "expires": time.time() + expires_seconds - _EXPIRY_BUFFER_SECONDS,
    }


# === Public API ===


async def login(
    on_auth_url: Callable[[str], None] | None = None,
    on_prompt_code: Callable[[], Any] | None = None,
    **kwargs: Any,
) -> dict[str, Any]:
    """Run Anthropic OAuth authorization-code flow with PKCE."""
    verifier = generate_verifier()
    challenge = generate_challenge(verifier)
    state = generate_state()
    auth_url = _build_authorize_url(challenge, state)

    if callable(on_auth_url):
        on_auth_url(auth_url)
    else:
        _default_open_browser(auth_url)

    prompt_handler = kwargs.get("on_prompt")
    if callable(on_prompt_code):
        raw_auth = await _maybe_await(on_prompt_code())
    elif callable(prompt_handler):
        raw_auth = await _maybe_await(
            prompt_handler(
                {
                    "message": "Paste the full redirect URL (or code)",
                    "allowEmpty": False,
                }
            )
        )
    else:
        raw_auth = input("Paste the full redirect URL (or code): ").strip()

    code, returned_state = _extract_code_and_state(raw_auth)
    if returned_state and returned_state != state:
        raise RuntimeError("Anthropic OAuth state mismatch.")

    token_response = await _exchange_token(
        {
            "grant_type": "authorization_code",
            "client_id": _CLIENT_ID,
            "code": code,
            "state": returned_state or state,
            "redirect_uri": _REDIRECT_URI,
            "code_verifier": verifier,
        },
        flow_name="token exchange",
    )

    credentials = _to_credentials(token_response)
    log.debug("Anthropic OAuth login succeeded", access=_safe_code(credentials["access"]))
    return credentials


async def refresh_token(refresh: str) -> dict[str, Any]:
    """Refresh Anthropic OAuth credentials."""
    token_response = await _exchange_token(
        {
            "grant_type": "refresh_token",
            "client_id": _CLIENT_ID,
            "refresh_token": refresh,
        },
        flow_name="token refresh",
    )

    credentials = _to_credentials(token_response, refresh=refresh)
    log.debug("Anthropic OAuth token refreshed", access=_safe_code(credentials["access"]))
    return credentials
