"""Authentication client implementing OAuth 2.0 Device Authorization Grant (RFC 8628)."""

import json
import logging
import sys
import time
import warnings
from collections.abc import Callable
from typing import Any

import httpx

from .exceptions import (
    AuthError,
    DeviceFlowDeniedError,
    DeviceFlowExpiredError,
    DeviceFlowTimeoutError,
    InvalidCompletionCodeError,
    RefreshError,
    SessionNotFoundError,
    SessionRevocationForbiddenError,
)
from .models import DeviceAuthResponse, TokenData, TokenResponse
from .storage import DEFAULT_STORAGE_PATH, TokenStorage

logger = logging.getLogger(__name__)

# Separate logger for HTTP debug output
http_logger = logging.getLogger(f"{__name__}.http")


def _is_debug_http_enabled() -> bool:
    """Check if DEBUG_HTTP is enabled via environment variable."""
    import os
    return os.environ.get("DEBUG_HTTP", "").lower() in ("true", "1", "yes")


def _sanitize_auth_body(body: dict[str, Any]) -> dict[str, Any]:
    """Sanitize sensitive fields in auth request/response bodies."""
    if not isinstance(body, dict):
        return body

    sanitized = body.copy()
    sensitive_fields = {
        "device_code",
        "completion_code",
        "access_token",
        "refresh_token",
        "id_token",
        "user_code",
    }

    for field in sensitive_fields:
        if field in sanitized and isinstance(sanitized[field], str):
            value = sanitized[field]
            if len(value) > 20:
                sanitized[field] = f"{value[:8]}...{value[-4:]}"
            else:
                sanitized[field] = "[REDACTED]"

    return sanitized


def _log_auth_request(method: str, url: str, body: dict[str, Any] | None = None, headers: dict[str, str] | None = None) -> None:
    """Log an auth HTTP request in cURL-style format."""
    if not _is_debug_http_enabled():
        return

    parts = [f"curl -X {method} '{url}'"]

    if headers:
        for name, value in headers.items():
            if name.lower() == "authorization":
                if value.startswith("Bearer ") and len(value) > 27:
                    value = f"Bearer {value[7:15]}...{value[-4:]}"
                else:
                    value = "[REDACTED]"
            parts.append(f"-H '{name}: {value}'")

    if body:
        sanitized = _sanitize_auth_body(body)
        body_str = json.dumps(sanitized, indent=2).replace("'", "'\"'\"'")
        parts.append(f"--data '{body_str}'")

    curl_cmd = " \\\n  ".join(parts)
    http_logger.debug(f">>> Auth Request:\n{curl_cmd}")


def _log_auth_response(status_code: int, body: str, elapsed_ms: float = 0) -> None:
    """Log an auth HTTP response."""
    if not _is_debug_http_enabled():
        return

    lines = [f"# Response: {status_code} ({elapsed_ms:.0f}ms)"]

    try:
        parsed = json.loads(body)
        sanitized = _sanitize_auth_body(parsed)
        lines.append("# Body:")
        for line in json.dumps(sanitized, indent=2).split("\n"):
            lines.append(f"#   {line}")
    except json.JSONDecodeError:
        # Truncate non-JSON responses
        truncated = body[:500] if len(body) > 500 else body
        lines.append(f"# Body: {truncated}")

    http_logger.debug(f"<<< Auth Response:\n" + "\n".join(lines))

DEFAULT_CLIENT_ID = "platform-mcp"


def _default_completion_code_callback() -> str:
    """Default callback that prompts for completion code via stdin.

    Supports two input methods:
    1. Direct paste (may fail with very long codes in some terminals)
    2. File path: Enter @/path/to/file to read code from file
    """
    import os

    sys.stderr.flush()
    sys.stdout.flush()

    print("Enter completion code (or @filepath to read from file):", file=sys.stderr)

    # Use sys.stdin.readline() which handles long input better than input()
    code = sys.stdin.readline()

    if not code:
        # Try reading all available input
        code = sys.stdin.read()

    code = code.strip()

    # Check if it's a file path
    if code.startswith("@"):
        filepath = os.path.expanduser(code[1:])
        if os.path.isfile(filepath):
            with open(filepath, "r") as f:
                code = f.read().strip()
            logger.debug(f"Read {len(code)} chars from file")
        else:
            logger.error(f"File not found: {filepath}")
            return ""
    else:
        logger.debug(f"Input received, length={len(code)}")

    sys.stderr.flush()
    return code


class AuthClient:
    """Authentication client for Platform-MCP.

    Implements OAuth 2.0 Device Authorization Grant (RFC 8628) for authenticating
    MCP clients through the Platform-2Steps BFF.

    Usage:
        auth = AuthClient(bff_url="https://ap-api.agendaprodev.com/platform-2steps-bff")

        # Authenticate (will prompt user if needed)
        tokens = auth.authenticate()

        # Get valid access token (refreshes automatically if needed)
        token = auth.get_valid_token()
    """

    def __init__(
        self,
        bff_url: str,
        client_id: str = DEFAULT_CLIENT_ID,
        token_storage_path: str = DEFAULT_STORAGE_PATH,
        timeout: float = 30.0,
        completion_code_callback: Callable[[], str] | None = None,
    ):
        """Initialize the authentication client.

        Args:
            bff_url: Base URL for Platform-2Steps BFF (e.g., https://ap-api.agendaprodev.com/platform-2steps-bff)
            client_id: OAuth client ID (default: platform-mcp)
            token_storage_path: Path to store tokens (default: ~/.platform-mcp/tokens.json)
            timeout: HTTP request timeout in seconds
            completion_code_callback: Callable that returns the completion code from user.
                                     Default prompts via stdin.
        """
        self.bff_url = bff_url.rstrip("/")
        self.client_id = client_id
        self.storage = TokenStorage(token_storage_path)
        self._http = httpx.Client(timeout=timeout)
        self._tokens: TokenData | None = None
        self._completion_code_callback = completion_code_callback or _default_completion_code_callback

    def close(self) -> None:
        """Close the HTTP client."""
        self._http.close()

    def authenticate(self) -> TokenData:
        """Perform full authentication flow.

        Returns cached tokens if valid, refreshes if possible, otherwise
        starts device authorization flow.

        Returns:
            TokenData with valid access token
        """
        # Try loading cached tokens
        tokens = self._load_cached_tokens()
        if tokens and not tokens.is_expired():
            self._tokens = tokens
            logger.info("Using cached tokens")
            return tokens

        # Try refreshing if we have a refresh token
        if tokens and tokens.refresh_token:
            try:
                logger.info("Refreshing expired token")
                return self._refresh_token(tokens.refresh_token)
            except RefreshError:
                logger.info("Refresh failed, starting device flow")

        # Full device authorization flow
        return self._device_auth_flow()

    def get_valid_token(self) -> str:
        """Get a valid access token, refreshing if needed.

        Returns:
            Valid access token string
        """
        if not self._tokens:
            self._tokens = self.authenticate()

        # Refresh if expiring within 5 minutes
        if self._tokens.is_expiring_soon(minutes=5):
            if self._tokens.refresh_token:
                try:
                    self._tokens = self._refresh_token(self._tokens.refresh_token)
                except RefreshError:
                    self._tokens = self._device_auth_flow()
            else:
                self._tokens = self._device_auth_flow()

        return self._tokens.access_token

    def get_id_token(self) -> str | None:
        """Get a valid ID token, refreshing if needed.

        The ID token is used for API Gateway authentication (CognitoUserPoolsAuthorizer).
        This method ensures tokens are valid before returning, similar to get_valid_token().

        Returns:
            ID token string or None if not available (e.g., legacy tokens)
        """
        if not self._tokens:
            self._tokens = self.authenticate()

        # Refresh if expiring within 5 minutes
        if self._tokens.is_expiring_soon(minutes=5):
            if self._tokens.refresh_token:
                try:
                    self._tokens = self._refresh_token(self._tokens.refresh_token)
                except RefreshError:
                    self._tokens = self._device_auth_flow()
            else:
                self._tokens = self._device_auth_flow()

        return self._tokens.id_token

    def clear_tokens(self) -> None:
        """Clear stored tokens (logout)."""
        self._tokens = None
        self.storage.clear()
        logger.info("Tokens cleared")

    def revoke_session(self, session_id: str) -> None:
        """Revoke a session (logout from a specific session).

        This calls the BFF endpoint to revoke the session server-side,
        then clears local tokens if successful.

        Args:
            session_id: The session ID to revoke.

        Raises:
            AuthError: If no active session exists.
            SessionNotFoundError: If the session was not found.
            SessionRevocationForbiddenError: If attempting to revoke another user's session.
        """
        if not self._tokens:
            raise AuthError("No active session")

        url = f"{self.bff_url}/v1/auth/sessions/{session_id}"
        headers = {"Authorization": f"Bearer {self._tokens.access_token}"}

        logger.debug(f"Revoking session at {url}")
        _log_auth_request("DELETE", url, headers=headers)

        start_time = time.time()
        response = self._http.delete(url, headers=headers)
        elapsed_ms = (time.time() - start_time) * 1000

        _log_auth_response(response.status_code, response.text, elapsed_ms)

        if response.status_code == 200:
            self.clear_tokens()
            logger.info(f"Session {session_id} revoked successfully")
        elif response.status_code == 403:
            raise SessionRevocationForbiddenError()
        elif response.status_code == 404:
            raise SessionNotFoundError(session_id)
        else:
            raise AuthError(f"Failed to revoke session: {response.text}")

    def start_device_auth(self) -> DeviceAuthResponse:
        """Start the device authorization flow.

        Returns:
            DeviceAuthResponse with codes and verification URI
        """
        url = f"{self.bff_url}/v1/auth/device"
        body = {"client_id": self.client_id}

        logger.debug(f"Starting device auth at {url}")
        _log_auth_request("POST", url, body=body)

        start_time = time.time()
        response = self._http.post(url, json=body)
        elapsed_ms = (time.time() - start_time) * 1000

        _log_auth_response(response.status_code, response.text, elapsed_ms)

        if response.status_code != 200:
            raise AuthError(f"Failed to start device auth: {response.text}")

        return DeviceAuthResponse(**response.json())

    def exchange_completion_code(self, device_code: str, completion_code: str) -> TokenData:
        """Exchange device_code and completion_code for tokens (single request).

        This is the browser-mediated completion flow where the user pastes
        the completion code they received after authenticating in the browser.

        Args:
            device_code: Device code from start_device_auth
            completion_code: Completion code provided by user from browser

        Returns:
            TokenData on successful exchange

        Raises:
            InvalidCompletionCodeError: If the completion code is invalid
            DeviceFlowExpiredError: If the device code has expired
            DeviceFlowDeniedError: If authorization was denied
        """
        url = f"{self.bff_url}/v1/auth/token"
        body = {
            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
            "device_code": device_code,
            "client_id": self.client_id,
            "completion_code": completion_code,
        }

        logger.debug(f"Exchanging completion code at {url}")
        _log_auth_request("POST", url, body=body)

        start_time = time.time()
        response = self._http.post(url, json=body)
        elapsed_ms = (time.time() - start_time) * 1000

        logger.debug(f"Token exchange response: {response.status_code}")
        _log_auth_response(response.status_code, response.text, elapsed_ms)

        if response.status_code == 200:
            token_response = TokenResponse(**response.json())
            tokens = TokenData.from_token_response(token_response)
            self._save_tokens(tokens)
            return tokens

        # Handle error responses
        try:
            error_data = response.json()
            error = error_data.get("error")
        except Exception:
            # Response is not JSON (e.g., HTML error page)
            raise AuthError(
                f"Server returned non-JSON response (status {response.status_code}): "
                f"{response.text[:200]}..."
            )

        if error == "invalid_grant":
            raise InvalidCompletionCodeError()
        elif error == "expired_token":
            raise DeviceFlowExpiredError()
        elif error == "access_denied":
            raise DeviceFlowDeniedError()
        else:
            raise AuthError(
                f"Authentication error: {error}",
                error_code=error,
            )

    def poll_for_token(self, device_code: str, interval: int = 5, timeout: int = 1800) -> TokenData:
        """Poll the token endpoint until authorization completes.

        .. deprecated::
            Use :meth:`exchange_completion_code` with browser-mediated completion flow instead.

        Args:
            device_code: Device code from start_device_auth
            interval: Polling interval in seconds
            timeout: Maximum time to wait in seconds

        Returns:
            TokenData on successful authorization

        Raises:
            DeviceFlowExpiredError: If the device code expires
            DeviceFlowDeniedError: If the user denies authorization
            DeviceFlowTimeoutError: If polling times out
        """
        warnings.warn(
            "poll_for_token is deprecated. Use exchange_completion_code with "
            "browser-mediated completion flow instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        poll_start_time = time.time()
        current_interval = interval

        url = f"{self.bff_url}/v1/auth/token"
        body = {
            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
            "device_code": device_code,
            "client_id": self.client_id,
        }

        while time.time() - poll_start_time < timeout:
            time.sleep(current_interval)

            logger.debug(f"Polling for token at {url}")
            _log_auth_request("POST", url, body=body)

            request_start = time.time()
            response = self._http.post(url, json=body)
            elapsed_ms = (time.time() - request_start) * 1000

            _log_auth_response(response.status_code, response.text, elapsed_ms)

            if response.status_code == 200:
                token_response = TokenResponse(**response.json())
                tokens = TokenData.from_token_response(token_response)
                self._save_tokens(tokens)
                return tokens

            # Handle error responses
            error_data = response.json()
            error = error_data.get("error")

            if error == "authorization_pending":
                continue  # Keep polling
            elif error == "slow_down":
                current_interval += 5  # Slow down as requested
                logger.debug(f"Slowing down polling to {current_interval}s")
            elif error == "expired_token":
                raise DeviceFlowExpiredError()
            elif error == "access_denied":
                raise DeviceFlowDeniedError()
            else:
                raise AuthError(
                    f"Authentication error: {error}",
                    error_code=error,
                )

        raise DeviceFlowTimeoutError()

    def refresh_token(self, refresh_token: str) -> TokenData:
        """Refresh the access token using a refresh token.

        Args:
            refresh_token: The refresh token

        Returns:
            New TokenData with fresh access token

        Raises:
            RefreshError: If refresh fails
        """
        return self._refresh_token(refresh_token)

    def _device_auth_flow(self) -> TokenData:
        """Execute the complete device authorization flow."""
        # Start device authorization
        logger.debug("Starting device auth...")
        device_info = self.start_device_auth()
        logger.debug("Device auth started, got device_code")

        # Display to user
        self._display_auth_prompt(device_info)

        # Wait for user to provide completion code
        logger.debug("Waiting for completion code input...")
        completion_code = self._completion_code_callback()
        logger.debug(f"Got completion code ({len(completion_code)} chars), exchanging...")

        # Exchange completion code for tokens
        tokens = self.exchange_completion_code(
            device_code=device_info.device_code,
            completion_code=completion_code,
        )
        logger.debug("Token exchange complete")

        self._print_success()
        return tokens

    def _refresh_token(self, refresh_token: str) -> TokenData:
        """Internal refresh token implementation."""
        url = f"{self.bff_url}/v1/auth/refresh"
        body = {
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "client_id": self.client_id,
        }

        logger.debug(f"Refreshing token at {url}")
        _log_auth_request("POST", url, body=body)

        start_time = time.time()
        response = self._http.post(url, json=body)
        elapsed_ms = (time.time() - start_time) * 1000

        _log_auth_response(response.status_code, response.text, elapsed_ms)

        if response.status_code != 200:
            raise RefreshError(f"Token refresh failed: {response.text}")

        token_response = TokenResponse(**response.json())
        tokens = TokenData.from_token_response(token_response)
        self._save_tokens(tokens)
        return tokens

    def _load_cached_tokens(self) -> TokenData | None:
        """Load tokens from storage."""
        return self.storage.load()

    def _save_tokens(self, tokens: TokenData) -> None:
        """Save tokens to storage and cache."""
        self._tokens = tokens
        self.storage.save(tokens)

    def _display_auth_prompt(self, device_info: DeviceAuthResponse) -> None:
        """Display authentication prompt to the user."""
        # Use verification_uri_complete if available, otherwise construct it
        if device_info.verification_uri_complete:
            login_url = device_info.verification_uri_complete
        else:
            # Construct URL with user_code as query parameter
            separator = "&" if "?" in device_info.verification_uri else "?"
            login_url = f"{device_info.verification_uri}{separator}user_code={device_info.user_code}"

        print("\n" + "=" * 60, file=sys.stderr)
        print("AUTHENTICATION REQUIRED", file=sys.stderr)
        print("=" * 60, file=sys.stderr)
        print(f"\n1. Open your browser to:", file=sys.stderr)
        print(f"   {login_url}", file=sys.stderr)
        print(f"\n2. After authenticating, copy the completion code from", file=sys.stderr)
        print("   the browser and paste it below.", file=sys.stderr)
        print(f"\nThis code expires in {device_info.expires_in // 60} minutes.", file=sys.stderr)
        print("=" * 60 + "\n", file=sys.stderr)

    def _print_success(self) -> None:
        """Print authentication success message."""
        print("\nAuthentication successful!", file=sys.stderr)
