"""
Authentication utilities for Xenfra CLI.
Handles OAuth2 PKCE flow and token management.
"""

from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse

import httpx
import keyring
from rich.console import Console
from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential,
)

from .security import validate_and_get_api_url

console = Console()

# Get validated API URL (includes all security checks)
API_BASE_URL = validate_and_get_api_url()
SERVICE_ID = "xenfra"

# CLI OAuth2 Configuration
CLI_CLIENT_ID = "xenfra-cli"
CLI_REDIRECT_PATH = "/auth/callback"
CLI_LOCAL_SERVER_START_PORT = 8001
CLI_LOCAL_SERVER_END_PORT = 8005

# HTTP request timeout (30 seconds)
HTTP_TIMEOUT = 30.0

# Global storage for OAuth callback data
oauth_data = {"code": None, "state": None, "error": None}


class AuthCallbackHandler(BaseHTTPRequestHandler):
    """HTTP handler for OAuth redirect callback."""

    def do_GET(self):
        global oauth_data
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()

        query_params = parse_qs(urlparse(self.path).query)

        if "code" in query_params:
            oauth_data["code"] = query_params["code"][0]
            oauth_data["state"] = query_params["state"][0] if "state" in query_params else None
            self.wfile.write(
                b"<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>"
            )
        elif "error" in query_params:
            oauth_data["error"] = query_params["error"][0]
            self.wfile.write(
                f"<html><body><h1>Authentication failed!</h1><p>Error: {oauth_data['error']}</p></body></html>".encode()
            )
        else:
            self.wfile.write(
                b"<html><body><h1>Authentication callback received.</h1><p>Waiting for code...</p></body></html>"
            )

        # Shut down the server after processing
        self.server.shutdown()  # type: ignore


def run_local_oauth_server(port: int, redirect_path: str):
    """Start a local HTTP server to capture the OAuth redirect."""
    server_address = ("127.0.0.1", port)
    httpd = HTTPServer(server_address, AuthCallbackHandler)
    httpd.timeout = 30  # seconds
    console.print(
        f"[dim]Listening for OAuth redirect on http://localhost:{port}{redirect_path}...[/dim]"
    )

    # Store the server instance in the handler for shutdown
    AuthCallbackHandler.server = httpd  # type: ignore

    # Handle a single request (blocking call)
    httpd.handle_request()
    console.print("[dim]Local OAuth server shut down.[/dim]")


@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
    reraise=True,
)
def _refresh_token_with_retry(refresh_token: str) -> dict:
    """
    Refresh access token with retry logic.

    Returns token data dictionary.
    """
    with httpx.Client(timeout=HTTP_TIMEOUT) as client:
        response = client.post(
            f"{API_BASE_URL}/auth/refresh",
            data={"refresh_token": refresh_token, "client_id": CLI_CLIENT_ID},
            headers={"Accept": "application/json"},
        )
        response.raise_for_status()

        # Safe JSON parsing with content-type check
        content_type = response.headers.get("content-type", "")
        if "application/json" not in content_type:
            raise ValueError(f"Expected JSON response, got {content_type}")

        try:
            token_data = response.json()
        except (ValueError, TypeError) as e:
            raise ValueError(f"Failed to parse JSON response: {e}")

        return token_data


def get_auth_token() -> str | None:
    """
    Retrieve a valid access token, refreshing it if necessary.

    Returns:
        Valid access token or None if not authenticated
    """
    try:
        access_token = keyring.get_password(SERVICE_ID, "access_token")
        refresh_token = keyring.get_password(SERVICE_ID, "refresh_token")
    except keyring.errors.KeyringError as e:
        console.print(f"[yellow]Warning: Could not access keyring: {e}[/yellow]")
        return None

    if not access_token:
        return None

    # Check if access token is expired
    try:
        from jose import JWTError, jwt

        # Decode without verifying signature to check expiration
        claims = jwt.decode(access_token, options={"verify_signature": False, "verify_exp": True})
    except JWTError:
        claims = None
    except Exception as e:
        console.print(f"[dim]Error decoding access token: {e}[/dim]")
        claims = None

    # Refresh token if expired
    if not claims and refresh_token:
        console.print("[dim]Access token expired. Attempting to refresh...[/dim]")
        try:
            token_data = _refresh_token_with_retry(refresh_token)
            new_access_token = token_data.get("access_token")
            new_refresh_token = token_data.get("refresh_token")

            if new_access_token:
                try:
                    keyring.set_password(SERVICE_ID, "access_token", new_access_token)
                    if new_refresh_token:
                        keyring.set_password(SERVICE_ID, "refresh_token", new_refresh_token)
                    console.print("[bold green]Token refreshed successfully.[/bold green]")
                    return new_access_token
                except keyring.errors.KeyringError as e:
                    console.print(
                        f"[yellow]Warning: Could not save refreshed token to keyring: {e}[/yellow]"
                    )
                    # Return the token anyway, but warn user
                    return new_access_token
            else:
                console.print("[bold red]Failed to get new access token.[/bold red]")
                return None

        except httpx.TimeoutException:
            console.print("[bold red]Token refresh failed: Request timed out.[/bold red]")
            return None
        except httpx.NetworkError:
            console.print("[bold red]Token refresh failed: Network error.[/bold red]")
            return None
        except httpx.HTTPStatusError as exc:
            if exc.response.status_code == 400:
                console.print("[bold red]Refresh token expired. Please log in again.[/bold red]")
            else:
                error_detail = "Unknown error"
                try:
                    if exc.response.content:
                        content_type = exc.response.headers.get("content-type", "")
                        if "application/json" in content_type:
                            error_data = exc.response.json()
                            error_detail = error_data.get("detail", str(error_data))
                except Exception:
                    error_detail = exc.response.text[:200] if exc.response.text else "Unknown error"

                console.print(
                    f"[bold red]Token refresh failed: {exc.response.status_code} - {error_detail}[/bold red]"
                )

            # Clear tokens on refresh failure
            try:
                keyring.delete_password(SERVICE_ID, "access_token")
                keyring.delete_password(SERVICE_ID, "refresh_token")
            except keyring.errors.KeyringError:
                pass  # Ignore errors when clearing
            return None
        except ValueError as e:
            console.print(f"[bold red]Token refresh failed: {e}[/bold red]")
            return None
        except Exception as e:
            console.print(
                f"[bold red]Token refresh failed: Unexpected error - {type(e).__name__}[/bold red]"
            )
            return None

    return access_token


def clear_tokens():
    """Clear stored access and refresh tokens."""
    try:
        keyring.delete_password(SERVICE_ID, "access_token")
        keyring.delete_password(SERVICE_ID, "refresh_token")
    except keyring.errors.PasswordDeleteError:
        pass  # Tokens already cleared
    except keyring.errors.KeyringError as e:
        console.print(f"[yellow]Warning: Could not clear tokens from keyring: {e}[/yellow]")
