"""Shared helper functions for error handling.

These functions are used by both sync and async clients to parse error responses
and raise appropriate exceptions. They contain no I/O operations.
"""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any, Optional

import httpx

from langsmith.sandbox._exceptions import (
    QuotaExceededError,
    ResourceCreationError,
    ResourceNotFoundError,
    ResourceTimeoutError,
    SandboxAPIError,
    SandboxAuthenticationError,
    SandboxClientError,
    SandboxConnectionError,
    SandboxNotReadyError,
    SandboxOperationError,
    ValidationError,
)

# =============================================================================
# Header Utilities
# =============================================================================


def merge_headers(
    base_headers: Optional[Mapping[str, str]] = None,
    override_headers: Optional[Mapping[str, str]] = None,
) -> dict[str, str]:
    """Merge request headers, giving precedence to overrides.

    Names are normalized to lowercase so an override replaces a base header that
    differs only in casing. HTTP header names are case-insensitive, so keeping
    both would be ambiguous — a server reading the first value would see the
    base header instead of the override.
    """
    merged: dict[str, str] = {}
    for headers in (base_headers, override_headers):
        for name, value in (headers or {}).items():
            merged[name.lower()] = value
    return merged


# =============================================================================
# Input Validation
# =============================================================================


def validate_service_params(port: int, expires_in_seconds: int) -> None:
    """Validate parameters for service URL generation.

    Args:
        port: Target port inside the sandbox.
        expires_in_seconds: Token TTL.

    Raises:
        ValueError: If port or TTL is out of range.
    """
    if not isinstance(port, int) or port <= 0:
        raise ValueError(f"port must be a positive integer, got {port!r}")
    if not isinstance(expires_in_seconds, int) or not (
        1 <= expires_in_seconds <= 86400
    ):
        raise ValueError(
            f"expires_in_seconds must be between 1 and 86400, "
            f"got {expires_in_seconds!r}"
        )


def validate_ttl(value: Optional[int], name: str) -> None:
    """Validate a TTL value for sandbox create/update.

    Args:
        value: TTL in seconds (None means unset, 0 disables).
        name: Parameter name for error messages.

    Raises:
        ValueError: If value is negative or not a multiple of 60.
    """
    if value is None:
        return
    if value < 0:
        raise ValueError(f"{name} must be >= 0, got {value}")
    if value != 0 and value % 60 != 0:
        raise ValueError(f"{name} must be a multiple of 60 seconds, got {value}")


# =============================================================================
# Error Response Parsing
# =============================================================================


def parse_error_response(error: httpx.HTTPStatusError) -> dict[str, Any]:
    """Parse standardized error response.

    Expected format: {"detail": {"error": "...", "message": "..."}}

    Returns a dict with:
    - error_type: The error type (e.g., "ImagePull", "CrashLoop")
    - message: Human-readable error message
    """
    try:
        data = error.response.json()
        detail = data.get("detail")

        # Standardized format: {"detail": {"error": "...", "message": "..."}}
        if isinstance(detail, dict):
            return {
                "error_type": detail.get("error"),
                "message": detail.get("message", str(error)),
            }

        # Pydantic validation error format: {"detail": [{"loc": [...], "msg": "..."}]}
        if isinstance(detail, list) and detail:
            messages = [d.get("msg", str(d)) for d in detail if isinstance(d, dict)]
            return {
                "error_type": None,
                "message": "; ".join(messages) if messages else str(error),
            }

        # Fallback for plain string detail
        return {"error_type": None, "message": detail or str(error)}
    except Exception:
        return {"error_type": None, "message": str(error)}


def parse_error_response_simple(error: httpx.HTTPStatusError) -> dict[str, Any]:
    """Parse error response (simplified version for sandbox operations).

    Returns a dict with:
    - error_type: The error type
    - message: Human-readable error message
    """
    try:
        data = error.response.json()
        detail = data.get("detail")

        if isinstance(detail, dict):
            return {
                "error_type": detail.get("error"),
                "message": detail.get("message", str(error)),
            }

        return {"error_type": None, "message": detail or str(error)}
    except Exception:
        return {"error_type": None, "message": str(error)}


def parse_validation_error(error: httpx.HTTPStatusError) -> list[dict]:
    """Parse Pydantic validation error response.

    Returns a list of validation error details, each containing:
    - loc: Location of the error (e.g., ["body", "resources", "cpu"])
    - msg: Human-readable error message
    - type: Error type (e.g., "value_error")
    """
    try:
        data = error.response.json()
        detail = data.get("detail", [])
        if isinstance(detail, list):
            return detail
        return []
    except Exception:
        return []


def extract_quota_type(message: str) -> Optional[str]:
    """Extract quota type from error message.

    Returns one of: "sandbox_count", "cpu", "memory", "storage", or None.
    """
    message_lower = message.lower()
    # Check for sandbox count quota
    if "sandbox" in message_lower and (
        "count" in message_lower or "limit" in message_lower
    ):
        return "sandbox_count"
    elif "cpu" in message_lower:
        return "cpu"
    elif "memory" in message_lower:
        return "memory"
    elif "storage" in message_lower:
        return "storage"
    return None


# =============================================================================
# Client Error Handlers
# =============================================================================


def raise_creation_error(
    data: dict[str, Any],
    error: httpx.HTTPStatusError,
    resource_type: str = "sandbox",
) -> None:
    """Raise ResourceCreationError with the error_type from the API response.

    The error_type indicates the specific failure reason:
    - ImagePull: Image pull failed
    - CrashLoop: Container crashed during startup
    - SandboxConfig: Configuration error
    - Unschedulable: Cannot be scheduled
    """
    raise ResourceCreationError(
        data.get("message", f"{resource_type.title()} creation failed"),
        resource_type=resource_type,
        error_type=data.get("error_type"),
    ) from error


def handle_sandbox_creation_error(error: httpx.HTTPStatusError) -> None:
    """Handle HTTP errors specific to sandbox creation.

    Maps API error responses to specific exception types:
    - 408: ResourceTimeoutError (sandbox didn't become ready in time)
    - 422: ValidationError (bad input) or ResourceCreationError (runtime)
    - 429: QuotaExceededError (org limits exceeded)
    - 503: ResourceCreationError (no resources available)
    - Other: Falls through to generic error handling
    """
    status = error.response.status_code
    data = parse_error_response(error)

    if status == 408:
        # Timeout - include the message which contains last known status
        raise ResourceTimeoutError(data["message"], resource_type="sandbox") from error
    elif status == 422:
        # Check if this is a Pydantic validation error (bad input) vs creation error
        details = parse_validation_error(error)
        if details and any(d.get("type") == "value_error" for d in details):
            # Pydantic validation error (bad input - exceeds server limits)
            field = details[0].get("loc", [None])[-1] if details else None
            raise ValidationError(
                message=data["message"],
                field=field,
                details=details,
            ) from error
        else:
            # Sandbox creation failed (runtime error like image pull failure)
            raise_creation_error(data, error)
    elif status == 429:
        # Organization quota exceeded
        quota_type = extract_quota_type(data["message"])
        raise QuotaExceededError(
            message=data["message"],
            quota_type=quota_type,
        ) from error
    elif status == 503:
        # Service Unavailable - scheduling failed
        raise ResourceCreationError(
            data["message"],
            resource_type="sandbox",
            error_type=data.get("error_type") or "Unschedulable",
        ) from error
    else:
        # Fall through to generic handling
        handle_client_http_error(error)


def handle_client_http_error(error: httpx.HTTPStatusError) -> None:
    """Handle HTTP errors and raise appropriate exceptions (for client operations)."""
    data = parse_error_response(error)
    message = data["message"]
    error_type = data.get("error_type")
    status = error.response.status_code

    if status in (401, 403):
        raise SandboxAuthenticationError(message) from error
    if status == 404:
        raise ResourceNotFoundError(message) from error

    # Handle validation errors (invalid resource values, formats, etc.)
    if status == 422:
        details = parse_validation_error(error)
        field = details[0].get("loc", [None])[-1] if details else None
        raise ValidationError(
            message=message,
            field=field,
            details=details,
        ) from error

    # Handle quota exceeded errors (org limits)
    if status == 429:
        quota_type = extract_quota_type(message)
        raise QuotaExceededError(
            message=message,
            quota_type=quota_type,
        ) from error

    if status == 502 and error_type == "ConnectionError":
        raise SandboxConnectionError(message) from error
    if status == 500:
        raise SandboxAPIError(message) from error
    raise SandboxClientError(message) from error


# =============================================================================
# Sandbox Operation Error Handlers
# =============================================================================


def handle_sandbox_http_error(error: httpx.HTTPStatusError) -> None:
    """Handle HTTP errors for sandbox operations (run, read, write).

    Maps API error types to specific exceptions:
    - WriteError -> SandboxOperationError (operation="write")
    - ReadError -> SandboxOperationError (operation="read")
    - CommandError -> SandboxOperationError (operation="command")
    - ConnectionError (502) -> SandboxConnectionError
    - FileNotFound / 404 -> ResourceNotFoundError (resource_type="file")
    - NotReady (400) -> SandboxNotReadyError
    - ServiceUnavailable (503) -> SandboxNotReadyError
    - 403 -> SandboxOperationError (permission denied)
    """
    data = parse_error_response_simple(error)
    message = data["message"]
    error_type = data.get("error_type")
    status = error.response.status_code

    # Operation-specific errors (from sandbox runtime)
    if error_type == "WriteError":
        raise SandboxOperationError(
            message, operation="write", error_type=error_type
        ) from error
    if error_type == "ReadError":
        raise SandboxOperationError(
            message, operation="read", error_type=error_type
        ) from error
    if error_type == "CommandError":
        raise SandboxOperationError(
            message, operation="command", error_type=error_type
        ) from error

    # Permission denied
    if status == 403:
        raise SandboxOperationError(
            message, operation=None, error_type="PermissionDenied"
        ) from error

    # Connection to sandbox failed
    if status == 502 and error_type == "ConnectionError":
        raise SandboxConnectionError(message) from error

    # Not ready / not found
    if (status == 400 and error_type == "NotReady") or (
        status == 503 and error_type == "ServiceUnavailable"
    ):
        raise SandboxNotReadyError(message) from error
    if status == 404 or error_type == "FileNotFound":
        raise ResourceNotFoundError(message, resource_type="file") from error

    raise SandboxClientError(message) from error
