"""Cloud authentication for Recall cloud services (Gmail, etc.)."""

import base64
import binascii
import json
import os
import sys
import time
import webbrowser
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Literal, NotRequired, TypedDict

import httpx

from . import __version__
from .logging import log_event

# Type alias for supported cloud providers
Provider = Literal["gmail"]

# Default cloud backend URL
DEFAULT_CLOUD_URL = "https://recall-ai.fly.dev"
CREDENTIALS_FILE = Path.home() / ".config" / "recall" / "credentials.json"


class CloudCredentialsDict(TypedDict):
    """TypedDict for serialized credentials."""

    session_token: str
    expires_at: NotRequired[float | None]
    refresh_token: NotRequired[str | None]
    refresh_expires_at: NotRequired[float | None]


@dataclass
class CloudCredentials:
    """Credentials for Recall cloud services."""

    session_token: str
    expires_at: float | None = None  # Unix timestamp
    refresh_token: str | None = None
    refresh_expires_at: float | None = None  # Unix timestamp

    def is_expired(self) -> bool:
        """Check if credentials are expired (with 5 minute buffer)."""
        if self.expires_at is None:
            return False
        return time.time() > (self.expires_at - 300)

    def refresh_is_expired(self) -> bool:
        """Check if refresh token is expired (with 5 minute buffer)."""
        if self.refresh_expires_at is None:
            return True
        return time.time() > (self.refresh_expires_at - 300)

    def to_dict(self) -> CloudCredentialsDict:
        return {
            "session_token": self.session_token,
            "expires_at": self.expires_at,
            "refresh_token": self.refresh_token,
            "refresh_expires_at": self.refresh_expires_at,
        }

    @staticmethod
    def from_dict(data: dict[str, object]) -> "CloudCredentials":
        token = data.get("session_token")
        if not token or not isinstance(token, str):
            raise CloudAuthError("Missing or invalid session_token in credentials")
        expires_at = data.get("expires_at")
        if expires_at is not None and not isinstance(expires_at, (int, float)):
            raise CloudAuthError("Invalid expires_at in credentials")
        refresh_token = data.get("refresh_token")
        if refresh_token is not None and not isinstance(refresh_token, str):
            raise CloudAuthError("Invalid refresh_token in credentials")
        refresh_expires_at = data.get("refresh_expires_at")
        if refresh_expires_at is not None and not isinstance(refresh_expires_at, (int, float)):
            raise CloudAuthError("Invalid refresh_expires_at in credentials")
        return CloudCredentials(
            session_token=token,
            expires_at=float(expires_at) if expires_at is not None else None,
            refresh_token=refresh_token,
            refresh_expires_at=float(refresh_expires_at) if refresh_expires_at is not None else None,
        )


@dataclass
class WrapperSession:
    """Short-lived token for wrapper API calls."""

    token: str
    expires_at: float  # Unix timestamp

    def is_expired(self) -> bool:
        """Check if session is expired (with 30 second buffer)."""
        return time.time() > (self.expires_at - 30)


class CloudAuthError(Exception):
    """Raised when cloud authentication fails."""

    pass


def _decode_jwt_payload(token: str) -> dict[str, object]:
    parts = token.split(".")
    if len(parts) < 2:
        return {}
    payload = parts[1]
    padding = "=" * (-len(payload) % 4)
    try:
        decoded = base64.urlsafe_b64decode(payload + padding)
    except (ValueError, binascii.Error):
        return {}
    try:
        data = json.loads(decoded.decode("utf-8"))
    except (ValueError, UnicodeDecodeError):
        return {}
    if isinstance(data, dict):
        return data
    return {}


def _extract_access_exp(token: str) -> float | None:
    data = _decode_jwt_payload(token)
    exp = data.get("exp")
    if isinstance(exp, (int, float)):
        return float(exp)
    return None


def get_cloud_url() -> str:
    """Get the cloud backend URL from environment or default."""
    return os.environ.get("RECALL_CLOUD_URL", DEFAULT_CLOUD_URL)


def load_credentials() -> CloudCredentials | None:
    """Load saved credentials from disk.

    Returns:
        CloudCredentials if valid credentials exist, None otherwise.

    Raises:
        CloudAuthError: If credentials file exists but is corrupted.
    """
    # Check environment variable first
    if env_token := os.environ.get("RECALL_CLOUD_SESSION"):
        return CloudCredentials(session_token=env_token, expires_at=_extract_access_exp(env_token))

    if not CREDENTIALS_FILE.exists():
        return None

    try:
        with open(CREDENTIALS_FILE) as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        log_event("credentials_parse_failed", path=str(CREDENTIALS_FILE), error=str(e))
        raise CloudAuthError(f"Corrupted credentials file: {e}")
    except OSError as e:
        log_event("credentials_read_failed", path=str(CREDENTIALS_FILE), error=str(e))
        raise CloudAuthError(f"Cannot read credentials file: {e}")

    creds = CloudCredentials.from_dict(data)
    if creds.expires_at is None:
        creds.expires_at = _extract_access_exp(creds.session_token)
    if creds.session_token and not creds.is_expired():
        return creds
    return None


def save_credentials(creds: CloudCredentials) -> None:
    """Save credentials to disk."""
    CREDENTIALS_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(CREDENTIALS_FILE, "w") as f:
        json.dump(creds.to_dict(), f, indent=2)
    # Set restrictive permissions
    CREDENTIALS_FILE.chmod(0o600)


def clear_credentials() -> None:
    """Remove saved credentials."""
    if CREDENTIALS_FILE.exists():
        CREDENTIALS_FILE.unlink()


@dataclass
class DeviceLinkStart:
    """Response payload for device link start."""

    device_code: str
    user_code: str
    verification_url: str
    expires_at: float
    interval_seconds: int

    @staticmethod
    def from_dict(data: dict[str, object], cloud_url: str) -> "DeviceLinkStart":
        device_code = data.get("device_code")
        if not device_code or not isinstance(device_code, str):
            raise CloudAuthError("Missing or invalid device_code in link response")

        user_code = data.get("user_code")
        if not user_code or not isinstance(user_code, str):
            raise CloudAuthError("Missing or invalid user_code in link response")

        verification_url = data.get("verification_url")
        if not isinstance(verification_url, str) or not verification_url.strip():
            verification_url = f"{cloud_url}/link"

        expires_at_raw = data.get("expires_at")
        expires_at = time.time() + 600
        if isinstance(expires_at_raw, str) and expires_at_raw.strip():
            expires_at = _parse_rfc3339(expires_at_raw)

        interval_seconds = data.get("interval_seconds")
        try:
            interval = int(interval_seconds) if interval_seconds is not None else 5
        except (TypeError, ValueError):
            interval = 5

        return DeviceLinkStart(
            device_code=device_code,
            user_code=user_code,
            verification_url=verification_url,
            expires_at=expires_at,
            interval_seconds=max(1, interval),
        )


def _parse_rfc3339(value: str) -> float:
    try:
        dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
        return dt.timestamp()
    except ValueError as exc:
        raise CloudAuthError(f"Invalid RFC3339 timestamp '{value}': {exc}")


def _safe_json(response: httpx.Response) -> dict[str, object]:
    try:
        payload = response.json()
    except ValueError:
        return {}
    if isinstance(payload, dict):
        return payload
    return {}


def _start_device_link(cloud_url: str) -> DeviceLinkStart:
    request_body = {
        "device_name": "rcl-rlm",
        "app_version": __version__,
    }

    try:
        with httpx.Client(timeout=30.0) as client:
            response = client.post(
                f"{cloud_url}/api/v1/devices/link/start",
                headers={"Content-Type": "application/json"},
                json=request_body,
            )
            response.raise_for_status()
            data = response.json()
    except httpx.HTTPStatusError as exc:
        log_event(
            "device_link_start_failed",
            status_code=exc.response.status_code,
            error=exc.response.text,
        )
        raise CloudAuthError(f"Failed to start device link: {exc.response.text}")
    except httpx.RequestError as exc:
        log_event("device_link_start_error", error=str(exc))
        raise CloudAuthError(f"Network error starting device link: {exc}")
    except ValueError as exc:
        log_event("device_link_start_parse_failed", error=str(exc))
        raise CloudAuthError(f"Invalid device link response: {exc}")

    if not isinstance(data, dict):
        raise CloudAuthError("Invalid device link response format")

    return DeviceLinkStart.from_dict(data, cloud_url)


def _poll_device_link(cloud_url: str, device_code: str) -> dict[str, object]:
    request_body = {"device_code": device_code}
    try:
        with httpx.Client(timeout=30.0) as client:
            response = client.post(
                f"{cloud_url}/api/v1/devices/link/poll",
                headers={"Content-Type": "application/json"},
                json=request_body,
            )
    except httpx.RequestError as exc:
        log_event("device_link_poll_error", error=str(exc))
        raise CloudAuthError(f"Network error polling device link: {exc}")

    data = _safe_json(response)

    if response.status_code >= 500:
        log_event(
            "device_link_poll_failed",
            status_code=response.status_code,
            error=response.text,
        )
        raise CloudAuthError("Cloud service error while polling device link")

    if response.status_code >= 400 and not data:
        raise CloudAuthError(f"Device link poll failed: {response.text}")

    return data


def _refresh_session(creds: CloudCredentials) -> CloudCredentials:
    if not creds.refresh_token:
        raise CloudAuthError("Missing refresh token. Please re-authenticate.")

    cloud_url = get_cloud_url()
    request_body = {"refresh_token": creds.refresh_token}

    try:
        with httpx.Client(timeout=30.0) as client:
            response = client.post(
                f"{cloud_url}/api/v1/auth/refresh",
                headers={"Content-Type": "application/json"},
                json=request_body,
            )
    except httpx.RequestError as exc:
        log_event("refresh_token_error", error=str(exc))
        raise CloudAuthError(f"Network error refreshing session: {exc}")

    if response.status_code in (400, 401):
        clear_credentials()
        raise CloudAuthError("Session expired. Please re-authenticate.")

    try:
        response.raise_for_status()
        data = response.json()
    except httpx.HTTPStatusError as exc:
        log_event(
            "refresh_token_failed",
            status_code=exc.response.status_code,
            error=exc.response.text,
        )
        raise CloudAuthError(f"Failed to refresh session: {exc.response.text}")
    except ValueError as exc:
        log_event("refresh_token_parse_failed", error=str(exc))
        raise CloudAuthError(f"Invalid refresh response: {exc}")

    if not isinstance(data, dict):
        raise CloudAuthError("Invalid refresh response format")

    access_token = data.get("access_token")
    if not isinstance(access_token, str) or not access_token:
        raise CloudAuthError("Missing access_token in refresh response")

    refresh_token = data.get("refresh_token")
    if refresh_token is not None and not isinstance(refresh_token, str):
        raise CloudAuthError("Invalid refresh_token in refresh response")

    refresh_expires_at = None
    refresh_expires_at_raw = data.get("refresh_expires_at")
    if isinstance(refresh_expires_at_raw, str) and refresh_expires_at_raw.strip():
        refresh_expires_at = _parse_rfc3339(refresh_expires_at_raw)

    return CloudCredentials(
        session_token=access_token,
        expires_at=_extract_access_exp(access_token),
        refresh_token=refresh_token or creds.refresh_token,
        refresh_expires_at=refresh_expires_at or creds.refresh_expires_at,
    )


def run_oauth_flow(verbose: bool = False) -> CloudCredentials:
    """Run device-link authentication flow to get cloud credentials.

    Opens browser to cloud device link page and polls for a session token.

    Args:
        verbose: Whether to print progress messages

    Returns:
        CloudCredentials with session token

    Raises:
        CloudAuthError: If authentication fails
    """
    cloud_url = get_cloud_url()
    link = _start_device_link(cloud_url)

    print("To link your Recall account:", file=sys.stderr)
    print(f"1) Open {link.verification_url}", file=sys.stderr)
    print(f"2) Enter code: {link.user_code}", file=sys.stderr)

    webbrowser.open(link.verification_url)

    if verbose:
        print("Waiting for authorization...", file=sys.stderr)

    deadline = link.expires_at
    interval = link.interval_seconds

    while time.time() < deadline:
        data = _poll_device_link(cloud_url, link.device_code)
        status = data.get("status")
        if status == "authorized":
            token = data.get("access_token")
            if not token or not isinstance(token, str):
                raise CloudAuthError("Missing access_token from device link response")
            refresh_token = data.get("refresh_token")
            if refresh_token is not None and not isinstance(refresh_token, str):
                raise CloudAuthError("Invalid refresh_token from device link response")
            refresh_expires_at = None
            refresh_expires_at_raw = data.get("refresh_expires_at")
            if isinstance(refresh_expires_at_raw, str) and refresh_expires_at_raw.strip():
                refresh_expires_at = _parse_rfc3339(refresh_expires_at_raw)
            creds = CloudCredentials(
                session_token=token,
                expires_at=_extract_access_exp(token),
                refresh_token=refresh_token,
                refresh_expires_at=refresh_expires_at,
            )
            save_credentials(creds)
            if verbose:
                print("Authentication successful!", file=sys.stderr)
            return creds
        if status in ("authorization_pending", "pending", "issuing"):
            time.sleep(interval)
            continue
        if status == "expired":
            raise CloudAuthError("Device link code expired. Please run the command again.")
        if status == "consumed":
            raise CloudAuthError("Device link code already used. Please run the command again.")
        if status == "invalid":
            raise CloudAuthError("Invalid device link code. Please run the command again.")
        if status:
            raise CloudAuthError(f"Unexpected device link status: {status}")
        time.sleep(interval)

    raise CloudAuthError("Device link timed out. Please run the command again.")


@dataclass
class ConnectedAccount:
    """A connected cloud account (Gmail, etc.)."""

    provider: str
    account_id: str
    primary_email: str
    status: str

    @staticmethod
    def from_dict(data: dict[str, object]) -> "ConnectedAccount":
        account_id = data.get("account_id")
        if not account_id or not isinstance(account_id, str):
            raise CloudAuthError(f"Missing or invalid account_id in account data: {data}")
        provider = data.get("provider")
        if not isinstance(provider, str):
            provider = "unknown"
        primary_email = data.get("primary_email")
        if not isinstance(primary_email, str):
            primary_email = ""
        status = data.get("status")
        if not isinstance(status, str):
            status = "unknown"
        return ConnectedAccount(
            provider=provider,
            account_id=account_id,
            primary_email=primary_email,
            status=status,
        )


def list_connected_accounts(
    creds: CloudCredentials,
    provider: Provider = "gmail",
) -> list[ConnectedAccount]:
    """List connected accounts for a provider.

    Args:
        creds: Cloud credentials with session token
        provider: Data source provider (currently only "gmail")

    Returns:
        List of connected accounts

    Raises:
        CloudAuthError: If request fails
    """
    cloud_url = get_cloud_url()

    try:
        with httpx.Client(timeout=30.0) as client:
            response = client.get(
                f"{cloud_url}/api/v1/integrations/accounts",
                params={"provider": provider},
                headers={"Authorization": f"Bearer {creds.session_token}"},
            )

            if response.status_code == 401:
                clear_credentials()
                raise CloudAuthError("Session expired. Please re-authenticate.")

            response.raise_for_status()
            data = response.json()

    except httpx.HTTPStatusError as exc:
        log_event(
            "list_accounts_failed",
            status_code=exc.response.status_code,
            error=exc.response.text,
        )
        raise CloudAuthError(f"Failed to list accounts: {exc.response.text}")
    except httpx.RequestError as exc:
        log_event("list_accounts_error", error=str(exc))
        raise CloudAuthError(f"Network error listing accounts: {exc}")

    accounts = data.get("accounts", [])
    return [ConnectedAccount.from_dict(a) for a in accounts]


def get_wrapper_session(
    creds: CloudCredentials,
    provider: Provider = "gmail",
    account_id: str | None = None,
) -> WrapperSession:
    """Get a short-lived wrapper session token for API calls.

    Args:
        creds: Cloud credentials with session token
        provider: Data source provider (currently only "gmail")
        account_id: Optional specific account ID (for multi-account).
                   If not provided, uses the first connected account.

    Returns:
        WrapperSession with short-lived token

    Raises:
        CloudAuthError: If session request fails or no accounts connected
    """
    cloud_url = get_cloud_url()

    # Auto-fetch account_id if not provided
    if not account_id:
        accounts = list_connected_accounts(creds, provider)
        if not accounts:
            raise CloudAuthError(
                f"No {provider} account connected. "
                f"Please connect your {provider} account at {cloud_url}/settings"
            )
        account_id = accounts[0].account_id
        log_event("using_account", provider=provider, account_id=account_id, email=accounts[0].primary_email)

    request_body = {"provider": provider, "account_id": account_id}

    try:
        with httpx.Client(timeout=30.0) as client:
            response = client.post(
                f"{cloud_url}/api/v1/integrations/wrapper/session",
                headers={
                    "Authorization": f"Bearer {creds.session_token}",
                    "Content-Type": "application/json",
                },
                json=request_body,
            )

            if response.status_code == 401:
                # Session expired, need to re-authenticate
                clear_credentials()
                raise CloudAuthError("Session expired. Please re-authenticate.")

            if response.status_code == 404:
                raise CloudAuthError(
                    f"No {provider} account connected. "
                    f"Please connect your {provider} account at {cloud_url}/settings"
                )

            response.raise_for_status()
            data = response.json()

    except httpx.HTTPStatusError as exc:
        log_event(
            "wrapper_session_failed",
            status_code=exc.response.status_code,
            error=exc.response.text,
        )
        raise CloudAuthError(f"Failed to get wrapper session: {exc.response.text}")
    except httpx.RequestError as exc:
        log_event("wrapper_session_error", error=str(exc))
        raise CloudAuthError(f"Network error getting wrapper session: {exc}")

    token = data.get("token")
    expires_at_str = data.get("expires_at")

    if not token:
        raise CloudAuthError("Missing 'token' in wrapper session response")

    if not expires_at_str:
        raise CloudAuthError("Missing 'expires_at' in wrapper session response")

    # Parse ISO timestamp to Unix timestamp
    try:
        dt = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
        expires_at = dt.timestamp()
    except ValueError as e:
        raise CloudAuthError(f"Invalid expires_at format '{expires_at_str}': {e}")

    return WrapperSession(token=token, expires_at=expires_at)


def ensure_authenticated(verbose: bool = False) -> CloudCredentials:
    """Ensure we have valid cloud credentials, prompting OAuth if needed.

    Args:
        verbose: Whether to print progress messages

    Returns:
        Valid CloudCredentials

    Raises:
        CloudAuthError: If authentication fails
    """
    creds = load_credentials()
    if creds:
        if not creds.is_expired():
            return creds
        if creds.refresh_token and not creds.refresh_is_expired():
            refreshed = _refresh_session(creds)
            save_credentials(refreshed)
            return refreshed

    # Need to authenticate
    return run_oauth_flow(verbose=verbose)
