from __future__ import annotations

import base64
import hashlib
import json
import os
import secrets
import webbrowser
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urlencode

import httpx

from mikoshi.entitlements import DEFAULT_FEATURES, DEFAULT_PLAN


AUTH_FILENAME = "auth.json"
CONFIG_FILENAME = "config.json"
DEFAULT_API_BASE_URL = "https://neet.gg"


class AuthError(RuntimeError):
    pass


@dataclass(frozen=True)
class AuthState:
    access_token: str
    expires_at: str
    plan: str
    features: list[str]

    def to_dict(self) -> dict[str, Any]:
        return {
            "access_token": self.access_token,
            "expires_at": self.expires_at,
            "plan": self.plan,
            "features": list(self.features),
        }

    @staticmethod
    def from_dict(data: dict[str, Any]) -> "AuthState":
        features = data.get("features") or []
        if isinstance(features, str):
            features = [features]
        return AuthState(
            access_token=str(data.get("access_token", "")),
            expires_at=str(data.get("expires_at", "")),
            plan=str(data.get("plan", DEFAULT_PLAN)),
            features=[str(item) for item in features],
        )


@dataclass(frozen=True)
class BrokerConfig:
    api_base_url: str


def _index_root() -> Path:
    return Path(os.getenv("MIKOSHI_INDEX_ROOT", "~/.mikoshi")).expanduser()


def auth_path() -> Path:
    return _index_root() / AUTH_FILENAME


def config_path() -> Path:
    return _index_root() / CONFIG_FILENAME


def load_broker_config() -> BrokerConfig:
    path = config_path()
    if not path.exists():
        return BrokerConfig(api_base_url=DEFAULT_API_BASE_URL)
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return BrokerConfig(api_base_url=DEFAULT_API_BASE_URL)
    if not isinstance(data, dict):
        return BrokerConfig(api_base_url=DEFAULT_API_BASE_URL)
    api_base_url = str(data.get("api_base_url", "")).strip().rstrip("/")
    if not api_base_url:
        api_base_url = DEFAULT_API_BASE_URL
    return BrokerConfig(api_base_url=api_base_url)


def save_broker_config(api_base_url: str) -> None:
    clean_url = api_base_url.strip().rstrip("/") or DEFAULT_API_BASE_URL
    path = config_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = json.dumps({"api_base_url": clean_url}, indent=2, sort_keys=True)
    temp_path = path.with_suffix(".json.tmp")
    fd = os.open(temp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w", encoding="utf-8") as handle:
        handle.write(payload)
        handle.write("\n")
    os.replace(temp_path, path)
    os.chmod(path, 0o600)


def load_auth_state() -> AuthState | None:
    path = auth_path()
    if not path.exists():
        return None
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return None
    if not isinstance(data, dict):
        return None
    return AuthState.from_dict(data)


def save_auth_state(state: AuthState) -> None:
    path = auth_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = json.dumps(state.to_dict(), indent=2, sort_keys=True)
    temp_path = path.with_suffix(".json.tmp")
    fd = os.open(temp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w", encoding="utf-8") as handle:
        handle.write(payload)
        handle.write("\n")
    os.replace(temp_path, path)
    os.chmod(path, 0o600)


def clear_auth_state() -> None:
    path = auth_path()
    if path.exists():
        path.unlink()


def _parse_expires_at(value: str) -> datetime | None:
    if not value:
        return None
    try:
        if value.endswith("Z"):
            value = value[:-1] + "+00:00"
        return datetime.fromisoformat(value)
    except Exception:
        return None


def is_expired(state: AuthState) -> bool:
    expires_at = _parse_expires_at(state.expires_at)
    if not expires_at:
        return True
    return datetime.now(timezone.utc) >= expires_at


def email_from_token(access_token: str) -> str | None:
    try:
        parts = access_token.split(".")
        if len(parts) < 2:
            return None
        payload = parts[1] + "=" * (-len(parts[1]) % 4)
        decoded = base64.urlsafe_b64decode(payload.encode("utf-8"))
        data = json.loads(decoded.decode("utf-8"))
        for key in ("email", "user_email", "preferred_username"):
            value = data.get(key)
            if value:
                return str(value)
        return None
    except Exception:
        return None


def _generate_state() -> str:
    return secrets.token_urlsafe(32).rstrip("=")


def _generate_code_verifier() -> str:
    return secrets.token_urlsafe(64).rstrip("=")


def _code_challenge(code_verifier: str) -> str:
    digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
    return base64.urlsafe_b64encode(digest).decode("utf-8").rstrip("=")


def _login_url(api_base_url: str, state: str, challenge: str) -> str:
    query = {
        "response_type": "code",
        "client_id": "mikoshi",
        "code_challenge": challenge,
        "code_challenge_method": "S256",
        "state": state,
    }
    return f"{api_base_url.rstrip('/')}/login?{urlencode(query)}"


def _parse_paste_payload(text: str) -> tuple[str, str]:
    try:
        data = json.loads(text)
    except Exception as exc:
        raise AuthError("Invalid JSON response.") from exc
    if not isinstance(data, dict):
        raise AuthError("Invalid JSON response.")
    code = str(data.get("code", "")).strip()
    state = str(data.get("state", "")).strip()
    if not code or not state:
        raise AuthError("Invalid JSON response.")
    return code, state


def _ensure_state_match(expected: str, actual: str) -> None:
    if expected != actual:
        raise AuthError("State mismatch. Please retry login.")


def _exchange_code(
    api_base_url: str, code: str, code_verifier: str, state: str
) -> dict[str, Any]:
    payload = {"code": code, "code_verifier": code_verifier, "state": state}
    url = f"{api_base_url.rstrip('/')}/cli/exchange"
    with httpx.Client(timeout=10.0) as client:
        response = client.post(url, json=payload)
    if response.status_code >= 400:
        raise AuthError("Login failed. Please try again.")
    data = response.json()
    if not isinstance(data, dict):
        raise AuthError("Login failed. Invalid response.")
    return data


def login() -> str:
    config = load_broker_config()
    api_base_url = config.api_base_url
    state = _generate_state()
    code_verifier = _generate_code_verifier()
    challenge = _code_challenge(code_verifier)
    login_url = _login_url(api_base_url, state, challenge)

    print("🔐 Starting authentication...")
    print("🌐 Opening authentication page in your browser...")
    opened = webbrowser.open(login_url, new=1, autoraise=True)
    if not opened:
        raise AuthError("Unable to open authentication page.")

    raw = input("Paste the JSON response here: ").strip()
    code, returned_state = _parse_paste_payload(raw)
    _ensure_state_match(state, returned_state)

    data = _exchange_code(api_base_url, code, code_verifier, returned_state)
    access_token = str(data.get("access_token", "")).strip()
    expires_at = str(data.get("expires_at", "")).strip()
    email = str(data.get("email", "")).strip()
    if not access_token or not expires_at or not email:
        raise AuthError("Login failed. Invalid response.")
    plan = str(data.get("plan") or DEFAULT_PLAN)
    features = data.get("features") or list(DEFAULT_FEATURES)
    if isinstance(features, str):
        features = [features]

    state_obj = AuthState(
        access_token=access_token,
        expires_at=expires_at,
        plan=plan,
        features=[str(item) for item in features],
    )
    save_auth_state(state_obj)
    return email
