# Copyright (c) CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
#

"""
Module for making authenticated API calls to endpoints protected by Okta Lambda authorizer.

This module provides functions for making API calls to endpoints that require
Okta authentication via JWT tokens in the Authorization header.

It also provides centralized error detection and handling for all API responses,
including:
- HTTP status code errors (non-200 responses)
- Server-side errors in 200 responses (with "error" field in body)
- Authentication errors (401/403)
- Malformed requests
"""

from typing import Any

import httpx
from rich.console import Console

from src.auth.simple_auth import get_authenticator
from src.settings import Settings

console = Console()


class APIResponseError(Exception):
    """
    Base class for API response errors with detailed error information.

    This exception captures all types of API errors including:
    - HTTP status errors
    - Server-side application errors
    - Authentication failures
    """

    def __init__(
        self,
        message: str,
        status_code: int | None = None,
        error_code: str | None = None,
        error_details: str | None = None,
        server_response: dict | str | None = None,
        is_auth_error: bool = False,
    ):
        super().__init__(message)
        self.message = message
        self.status_code = status_code
        self.error_code = error_code
        self.error_details = error_details
        self.server_response = server_response
        self.is_auth_error = is_auth_error

    def __str__(self):
        parts = [self.message]

        if self.error_code:
            parts.append(f"Error Code: {self.error_code}")

        if self.error_details:
            parts.append(f"Details: {self.error_details}")

        if self.status_code:
            parts.append(f"HTTP Status: {self.status_code}")

        return " | ".join(parts)


def _detect_error_in_response(response: httpx.Response, debug: bool = False) -> APIResponseError | None:
    """
    Centralized error detection for API responses.

    Detects and categorizes errors from:
    1. HTTP status codes (4xx, 5xx)
    2. Response body "error" field (even with 200 status)
    3. Authentication-specific errors
    4. Malformed requests

    Args:
        response: The HTTP response object
        debug: Whether to show debug information

    Returns:
        APIResponseError if an error is detected, None otherwise
    """
    status_code = response.status_code

    # Try to parse response body
    try:
        response_body = response.json()
    except Exception:
        # If we can't parse JSON, treat as text
        response_body = {"_raw_text": response.text}

    # Check for server-side errors in response body (even with 200 status)
    # This handles case #4: Server errors with "error" attribute
    # Note: Only treat as error if "error" field has a truthy value (not None/null)
    if isinstance(response_body, dict) and response_body.get("error"):
        error_msg = response_body.get("error", "Unknown error")
        error_details = response_body.get("details", "")
        error_code = response_body.get("error_code", "SERVER_ERROR")

        if debug:
            console.print(f"[red]DEBUG: Detected server error in response body: {error_msg}[/red]")

        return APIResponseError(
            message=f"Server error: {error_msg}",
            status_code=status_code,
            error_code=error_code,
            error_details=error_details,
            server_response=response_body,
            is_auth_error=False,
        )

    # Check for HTTP status code errors (case #1)
    if status_code >= 400:
        # Determine if this is an authentication error (case #3)
        is_auth_error = status_code in [401, 403]

        # Check for authentication-specific error messages
        if isinstance(response_body, dict):
            error_msg = response_body.get("message") or response_body.get("error") or ""
            if error_msg and ("not authorized" in error_msg.lower() or "authentication" in error_msg.lower()):
                is_auth_error = True

        # Build error message
        if status_code == 400:
            # Case #2: Malformed request
            error_type = "Malformed request"
        elif status_code == 401:
            error_type = "Authentication required"
        elif status_code == 403:
            error_type = "Access denied"
        elif status_code == 404:
            error_type = "Resource not found"
        elif status_code >= 500:
            error_type = "Server error"
        else:
            error_type = f"HTTP {status_code} error"

        # Extract error details from response body
        error_details = None
        error_code = None

        if isinstance(response_body, dict):
            error_details = (
                response_body.get("details")
                or response_body.get("message")
                or response_body.get("error")
                or response_body.get("Message")  # AWS API Gateway format
            )
            error_code = response_body.get("error_code") or response_body.get("errorType")
        elif isinstance(response_body, dict) and "_raw_text" in response_body:
            error_details = response_body["_raw_text"]

        if debug:
            console.print(f"[red]DEBUG: Detected HTTP {status_code} error (auth: {is_auth_error})[/red]")

        return APIResponseError(
            message=f"{error_type}",
            status_code=status_code,
            error_code=error_code,
            error_details=error_details,
            server_response=response_body,
            is_auth_error=is_auth_error,
        )

    # No error detected
    return None


def _attempt_automatic_login(debug: bool = False) -> bool:
    """
    Attempt automatic login when authentication is missing or invalid.

    Args:
        debug: Whether to show debug information

    Returns:
        True if login successful, False otherwise
    """
    try:
        console.print("Authenticating...")

        authenticator = get_authenticator()

        # Attempt authentication without forcing if already authenticated
        token = authenticator.authenticate(force=False, debug=False)  # Always disable debug for clean output

        if token and token.get("access_token"):
            # Get user info to display username
            try:
                user_info = authenticator.get_cached_userinfo()
                if user_info:
                    # Try to get a friendly display name
                    name = user_info.get("name") or user_info.get("email") or "user"
                    console.print(f"Authentication successful as {name}")
                else:
                    console.print("Authentication successful")
            except Exception:
                # If we can't get user info, just show generic success
                console.print("Authentication successful")
            return True
        else:
            console.print("Authentication failed. Please run 'czops auth login' to authenticate")
            return False

    except Exception as e:
        if debug:
            console.print(f"DEBUG: Automatic login error: {e}")
        console.print("Authentication failed. Please run 'czops auth login' to authenticate")
        return False


def make_authenticated_request(
    method: str,
    endpoint_path: str,
    json_data: dict[str, Any] | None = None,
    params: dict[str, Any] | None = None,
    timeout: int = 30,
    debug: bool = False,
    retry_on_401: bool = True,
) -> httpx.Response:
    """
    Make an authenticated API request to an Okta-protected endpoint.

    This function handles:
    - Getting the access token from the authenticator
    - Adding the Authorization header with Bearer token
    - Making the HTTP request to the auth.endpoint
    - Automatically refreshing the token on 401 responses

    Args:
        method: HTTP method (GET, POST, PUT, DELETE, etc.)
        endpoint_path: API endpoint path (e.g., "auth/verify")
        json_data: Optional JSON body data
        params: Optional query parameters
        timeout: Request timeout in seconds
        debug: Whether to print debug information
        retry_on_401: Whether to retry with refreshed token on 401 (default: True)

    Returns:
        httpx.Response object

    Raises:
        ValueError: If not authenticated or unable to get access token
        httpx.HTTPError: If the request fails
    """
    # Get the authenticator and access token
    authenticator = get_authenticator()

    # If not authenticated, attempt automatic login
    if not authenticator.is_authenticated():
        if debug:
            console.print("[yellow]DEBUG: Not authenticated, attempting automatic login[/yellow]")

        if not _attempt_automatic_login(debug=debug):
            raise ValueError("Authentication failed. Please run 'czops auth login' to authenticate.")

    # Get access token - this already handles refresh if token is expired
    access_token = authenticator.get_access_token()
    if not access_token:
        if debug:
            console.print("[yellow]DEBUG: No access token, attempting automatic login[/yellow]")

        # Try automatic login if token is missing
        if not _attempt_automatic_login(debug=debug):
            raise ValueError("Unable to get access token. Please run 'czops auth login' to authenticate.")

        # Try getting token again after login
        access_token = authenticator.get_access_token()
        if not access_token:
            raise ValueError("Unable to get access token after login. Please try 'czops auth login' manually.")

    # Get settings for API endpoint and credentials
    settings = Settings()
    api_url = f"{settings.api_v4.endpoint}v4/{endpoint_path}"

    # Build headers with Authorization header for Lambda authorizer
    # Note: x-api-key is not needed when using JWT/OAuth authentication
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
    }

    if debug:
        console.print("\n[yellow]DEBUG: Authenticated API Request[/yellow]")
        console.print(f"URL: {api_url}")
        console.print(f"Method: {method.upper()}")
        console.print("Headers:")
        for key, value in headers.items():
            if key == "Authorization":
                # Show full JWT token
                jwt_token = value[7:]  # Remove "Bearer " prefix
                console.print(f"  {key}: {value}")

                # Decode JWT payload (without verification for debugging)
                try:
                    import base64
                    import json

                    # JWT has 3 parts: header.payload.signature
                    parts = jwt_token.split(".")
                    if len(parts) == 3:
                        # Decode header
                        header_data = parts[0] + "=" * (4 - len(parts[0]) % 4)  # Add padding
                        header = json.loads(base64.urlsafe_b64decode(header_data))
                        console.print(f"    JWT Header: {json.dumps(header, indent=6)}")

                        # Decode payload
                        payload_data = parts[1] + "=" * (4 - len(parts[1]) % 4)  # Add padding
                        payload = json.loads(base64.urlsafe_b64decode(payload_data))
                        console.print(f"    JWT Payload: {json.dumps(payload, indent=6)}")

                        # Show expiration info
                        if "exp" in payload:
                            from datetime import datetime

                            exp_time = datetime.fromtimestamp(payload["exp"])
                            now = datetime.now()
                            time_left = exp_time - now
                            console.print(f"    Token expires: {exp_time}")
                            console.print(f"    Time remaining: {time_left}")

                except Exception as e:
                    console.print(f"    Failed to decode JWT: {e}")
            else:
                console.print(f"  {key}: {value}")
        if json_data:
            import json

            console.print(f"Body: {json.dumps(json_data, indent=2)}")
        if params:
            console.print(f"Params: {params}")

    # Make the request
    with httpx.Client(timeout=timeout) as client:
        response = client.request(
            method=method.upper(),
            url=api_url,
            headers=headers,
            json=json_data,
            params=params,
        )

    if debug:
        console.print("\n[yellow]DEBUG: API Response[/yellow]")
        console.print(f"Status Code: {response.status_code}")
        console.print("Headers:")
        for key, value in response.headers.items():
            console.print(f"  {key}: {value}")

        try:
            response_json = response.json()
            import json

            console.print("\n[yellow]DEBUG: Response Body[/yellow]")
            console.print(json.dumps(response_json, indent=2))
        except Exception:
            console.print(f"Response Text: {response.text}")

    # Handle 401/403 authentication errors by refreshing token or re-authenticating
    if response.status_code in [401, 403] and retry_on_401:
        if debug:
            console.print(
                f"\n[yellow]DEBUG: Received {response.status_code} authentication error - attempting recovery[/yellow]"
            )

        try:
            # First try token refresh if we have a refresh token
            stored_token = authenticator.get_stored_token()
            if stored_token and "refresh_token" in stored_token:
                # Attempt to refresh the token
                authenticator.refresh_token(stored_token["refresh_token"])

                if debug:
                    console.print("[green]DEBUG: Token refreshed successfully - retrying request[/green]")

                # Retry the request with the new token
                return make_authenticated_request(
                    method=method,
                    endpoint_path=endpoint_path,
                    json_data=json_data,
                    params=params,
                    timeout=timeout,
                    debug=debug,
                    retry_on_401=False,  # Don't retry again to avoid infinite loop
                )
            else:
                if debug:
                    console.print(
                        "[yellow]DEBUG: No refresh token available - attempting full re-authentication[/yellow]"
                    )

                # If no refresh token, attempt full re-authentication
                if _attempt_automatic_login(debug=debug):
                    if debug:
                        console.print("[green]DEBUG: Re-authentication successful - retrying request[/green]")

                    # Retry the request with the new token
                    return make_authenticated_request(
                        method=method,
                        endpoint_path=endpoint_path,
                        json_data=json_data,
                        params=params,
                        timeout=timeout,
                        debug=debug,
                        retry_on_401=False,  # Don't retry again to avoid infinite loop
                    )
                else:
                    if debug:
                        console.print("[red]DEBUG: Re-authentication failed[/red]")

        except Exception as e:
            if debug:
                console.print(f"[red]DEBUG: Authentication recovery failed: {e}[/red]")
            # If all recovery attempts fail, return the original response
            pass

    return response


def post_authenticated(
    endpoint_path: str,
    json_data: dict[str, Any] | None = None,
    params: dict[str, Any] | None = None,
    timeout: int = 30,
    debug: bool = False,
) -> dict[str, Any]:
    """
    Convenience function for making authenticated POST requests with centralized error detection.

    Args:
        endpoint_path: API endpoint path
        json_data: Optional JSON body data
        params: Optional query parameters
        timeout: Request timeout in seconds
        debug: Whether to print debug information

    Returns:
        Response JSON data

    Raises:
        APIResponseError: If the API returns an error (HTTP status or server-side error)
        ValueError: If not authenticated
    """
    from src.exceptions import APIError

    response = make_authenticated_request(
        method="POST",
        endpoint_path=endpoint_path,
        json_data=json_data or {},
        params=params,
        timeout=timeout,
        debug=debug,
    )

    # Use centralized error detection
    error = _detect_error_in_response(response, debug=debug)
    if error:
        # Convert to APIError for backward compatibility
        raise APIError(
            message=str(error),
            status_code=error.status_code,
            server_response=error.server_response,
        )

    return response.json()


def put_authenticated(
    endpoint_path: str,
    json_data: dict[str, Any] | None = None,
    params: dict[str, Any] | None = None,
    timeout: int = 30,
    debug: bool = False,
) -> dict[str, Any]:
    """
    Convenience function for making authenticated PUT requests with centralized error detection.

    Args:
        endpoint_path: API endpoint path
        json_data: Optional JSON body data
        params: Optional query parameters
        timeout: Request timeout in seconds
        debug: Whether to print debug information

    Returns:
        Response JSON data

    Raises:
        APIResponseError: If the API returns an error (HTTP status or server-side error)
        ValueError: If not authenticated
    """
    from src.exceptions import APIError

    response = make_authenticated_request(
        method="PUT",
        endpoint_path=endpoint_path,
        json_data=json_data,
        params=params,
        timeout=timeout,
        debug=debug,
    )

    # Use centralized error detection
    error = _detect_error_in_response(response, debug=debug)
    if error:
        # Convert to APIError for backward compatibility
        raise APIError(
            message=str(error),
            status_code=error.status_code,
            server_response=error.server_response,
        )

    return response.json()


def get_authenticated(
    endpoint_path: str,
    params: dict[str, Any] | None = None,
    timeout: int = 30,
    debug: bool = False,
) -> dict[str, Any]:
    """
    Convenience function for making authenticated GET requests with centralized error detection.

    Args:
        endpoint_path: API endpoint path
        params: Optional query parameters
        timeout: Request timeout in seconds
        debug: Whether to print debug information

    Returns:
        Response JSON data

    Raises:
        APIResponseError: If the API returns an error (HTTP status or server-side error)
        ValueError: If not authenticated
    """
    from src.exceptions import APIError

    response = make_authenticated_request(
        method="GET",
        endpoint_path=endpoint_path,
        params=params,
        timeout=timeout,
        debug=debug,
    )

    # Use centralized error detection
    error = _detect_error_in_response(response, debug=debug)
    if error:
        # Convert to APIError for backward compatibility
        raise APIError(
            message=str(error),
            status_code=error.status_code,
            server_response=error.server_response,
        )

    return response.json()


def delete_authenticated(
    endpoint_path: str,
    params: dict[str, Any] | None = None,
    timeout: int = 30,
    debug: bool = False,
) -> dict[str, Any]:
    """
    Convenience function for making authenticated DELETE requests with centralized error detection.

    Args:
        endpoint_path: API endpoint path
        params: Optional query parameters
        timeout: Request timeout in seconds
        debug: Whether to print debug information

    Returns:
        Response JSON data

    Raises:
        APIResponseError: If the API returns an error (HTTP status or server-side error)
        ValueError: If not authenticated
    """
    from src.exceptions import APIError

    response = make_authenticated_request(
        method="DELETE",
        endpoint_path=endpoint_path,
        params=params,
        timeout=timeout,
        debug=debug,
    )

    # Use centralized error detection
    error = _detect_error_in_response(response, debug=debug)
    if error:
        # Convert to APIError for backward compatibility
        raise APIError(
            message=str(error),
            status_code=error.status_code,
            server_response=error.server_response,
        )

    return response.json()
