"""Shared utilities for provider implementations."""

from __future__ import annotations

import copy
from dataclasses import replace
from enum import Enum

from ..dtypes import Cost, Message, Usage
from ..models import ModelCost


class NonRetryableError(Exception):
    """Exception for errors that should not be retried (e.g., context length, invalid params)."""

    pass


class ContextTooLongError(NonRetryableError):
    """Exception raised when the context/prompt exceeds the model's maximum token limit.

    This is a recoverable error - the user can clear context, start a new conversation,
    or the system can auto-compact the conversation history.

    Attributes:
        current_tokens: Number of tokens in the current prompt
        max_tokens: Maximum tokens allowed by the model
    """

    def __init__(
        self,
        message: str = "Context too long",
        current_tokens: int | None = None,
        max_tokens: int | None = None,
    ) -> None:
        super().__init__(message)
        self.current_tokens = current_tokens
        self.max_tokens = max_tokens


class FatalEvalError(Exception):
    """Exception for errors that should abort the entire evaluation.

    These are configuration/auth errors where retrying or continuing with other
    samples is pointless - e.g., invalid API key, no credits, invalid model.

    Unlike ProviderError (which marks one sample as failed and continues),
    FatalEvalError should crash the eval loudly so the user can fix the issue.
    """

    pass


class ProviderError(Exception):
    """Exception for provider errors that exhausted retries.

    These errors indicate infrastructure/API issues (rate limits, timeouts, 503s)
    rather than model failures. They should be excluded from accuracy calculations.

    Usage in evaluation:
        try:
            result = await run_agent(...)
        except ProviderError as e:
            status = "provider_error"  # excluded from accuracy
        except Exception as e:
            status = "failed"  # counts against accuracy

    Attributes:
        original_error: The underlying exception that caused the failure
        attempts: Number of retry attempts made before giving up
        provider: Name of the provider (e.g., "anthropic", "openai")
    """

    def __init__(
        self,
        message: str,
        original_error: Exception | None = None,
        attempts: int = 0,
        provider: str = "",
    ) -> None:
        super().__init__(message)
        self.original_error = original_error
        self.attempts = attempts
        self.provider = provider


class VLLMErrorType(Enum):
    """Classification of vLLM server errors."""

    SUCCESS = "success"
    CONTEXT_LENGTH = "context_length"
    INVALID_PARAM = "invalid_param"
    HTTP_ERROR = "http_error"


def _classify_vllm_error(status_code: int, error_body: str) -> VLLMErrorType:
    """Classify vLLM error type. Pure function - no I/O."""
    assert isinstance(status_code, int), f"status_code must be int, got {type(status_code)}"
    assert isinstance(error_body, str), f"error_body must be str, got {type(error_body)}"

    if status_code == 200:
        return VLLMErrorType.SUCCESS
    if "maximum context length" in error_body.lower():
        return VLLMErrorType.CONTEXT_LENGTH
    if "not a valid parameter" in error_body.lower():
        return VLLMErrorType.INVALID_PARAM
    return VLLMErrorType.HTTP_ERROR


def _format_context_length_error(max_tokens: int) -> str:
    """Format context length error message. Pure function."""
    assert isinstance(max_tokens, int), f"max_tokens must be int, got {type(max_tokens)}"
    assert max_tokens > 0, f"max_tokens must be > 0, got {max_tokens}"

    suggested_value = max_tokens // 2
    return (
        "💡 CONTEXT LENGTH ERROR DETECTED:\n"
        "   • This is NOT a server startup failure - server is working correctly\n"
        f"   • Your max_tokens ({max_tokens}) exceeds server's limit\n"
        f"   • FIX: Reduce max_tokens to a smaller value (try {suggested_value})\n"
        "   • OR: Redeploy server with larger --max-model-len\n"
        "🛑 Stopping retries - context length errors cannot be fixed by retrying"
    )


def _format_invalid_param_error(param_keys: list) -> str:
    """Format invalid parameter error message. Pure function."""
    assert isinstance(param_keys, list), f"param_keys must be list, got {type(param_keys)}"

    return (
        "💡 PARAMETER ERROR DETECTED:\n"
        "   • Server doesn't support one of your parameters\n"
        f"   • Your parameters: {param_keys}\n"
        "   • Try removing 'logprobs' or 'echo' parameters"
    )


def sanitize_request_for_logging(params: dict) -> dict:
    """Tiger Style: Sanitize request parameters to remove large base64 image data.

    Vision messages can contain 100KB+ base64 images. Replace with bounded
    placeholders to prevent terminal spam while preserving useful debug info.
    """
    sanitized = copy.deepcopy(params)

    if "messages" in sanitized:
        for msg in sanitized["messages"]:
            if isinstance(msg, dict) and "content" in msg:
                content = msg["content"]
                # Handle vision messages (list of content parts)
                if isinstance(content, list):
                    sanitized_parts = []
                    for part in content:
                        if isinstance(part, dict):
                            if part.get("type") == "image_url":
                                # Replace base64 data with placeholder
                                # image_url field is a dict with url key containing base64
                                url_str = str(part.get("image_url", {}).get("url", ""))
                                if url_str.startswith("data:image") and len(url_str) > 100:
                                    url_preview = f"{url_str[:50]}... ({len(url_str)} chars)"
                                else:
                                    url_preview = url_str
                                sanitized_parts.append({
                                    "type": "image_url",
                                    "image_url": {"url": url_preview},
                                })
                            else:
                                # Keep text parts
                                sanitized_parts.append(part)
                        else:
                            sanitized_parts.append(part)
                    msg["content"] = sanitized_parts
                # Handle long text content
                elif isinstance(content, str) and len(content) > 500:
                    msg["content"] = content[:500] + f"... ({len(content)} chars total)"

    return sanitized


def add_cache_control_to_last_content(
    messages: list[dict], cache_control: dict | None = None, max_cache_controls: int = 4
) -> list[dict]:
    """Adds cache control metadata to the final content block if possible."""
    if cache_control is None:
        cache_control = {"type": "ephemeral"}
    assert cache_control is not None
    assert isinstance(cache_control, dict)
    assert max_cache_controls > 0
    assert max_cache_controls <= 10  # Reasonable upper bound

    if not messages:
        return messages

    assert isinstance(messages, list)
    new_messages = copy.deepcopy(messages)
    assert new_messages is not None

    cache_control_count = sum(
        1
        for msg in new_messages
        for content in (
            msg["content"] if isinstance(msg.get("content"), list) else [msg.get("content")]
        )
        if isinstance(content, dict) and "cache_control" in content
    )

    if cache_control_count >= max_cache_controls:
        return new_messages

    last_message = new_messages[-1]
    if isinstance(last_message.get("content"), list) and last_message["content"]:
        last_content = last_message["content"][-1]
        if (
            isinstance(last_content, dict)
            and "type" in last_content
            and "cache_control" not in last_content
        ):
            last_content["cache_control"] = cache_control
    elif isinstance(last_message.get("content"), dict):
        if "cache_control" not in last_message["content"]:
            last_message["content"]["cache_control"] = cache_control

    assert isinstance(new_messages, list)
    return new_messages


def _prepare_messages_for_llm(messages: list[Message]) -> list[Message]:
    """Strip tool result details before sending to LLM.

    Tiger Style: Explicit filtering, no magic.
    Tools return both `content` (for LLM) and `details` (for UI).
    This function removes `details` to reduce token usage.

    Args:
        messages: List of messages, some may have details field

    Returns:
        New list with details stripped from tool messages
    """
    assert messages is not None
    assert isinstance(messages, list)

    filtered = []
    for msg in messages:
        if msg.role == "tool":
            # Strip details field - UI-only data
            # Keep only content for LLM context
            filtered_msg = replace(msg, details=None)  # Remove details
            filtered.append(filtered_msg)
        else:
            filtered.append(msg)

    assert len(filtered) == len(messages)  # No messages dropped
    return filtered


def verbose(level: int) -> bool:
    """Simple verbose checker - just return True for now."""
    return True


# =============================================================================
# Wide Event Logging Helpers
# =============================================================================
# Per logging_sucks.md: emit one structured event per API call with all context.
# These helpers provide consistent logging across all providers.

import logging

_provider_logger = logging.getLogger(__name__)


def _summarize_messages_for_log(messages: list[dict]) -> list[dict]:
    """Summarize messages for logging without full content."""
    summaries = []
    for msg in messages:
        role = msg.get("role", "unknown")
        content = msg.get("content", "")
        if isinstance(content, str):
            summaries.append({"role": role, "chars": len(content), "preview": content[:100]})
        elif isinstance(content, list):
            summaries.append({"role": role, "blocks": len(content)})
        else:
            summaries.append({"role": role, "type": type(content).__name__})
    return summaries


def log_api_request(
    provider: str,
    model: str,
    api_base: str | None,
    messages: list[dict],
    system_prompt: str | None = None,
    tools: list[str] | None = None,
    temperature: float | None = None,
    max_tokens: int | None = None,
    thinking_enabled: bool = False,
    turn_idx: int = 0,
    **extra: object,
) -> None:
    """Log a wide event for an API request.

    Call this before making the API call. Captures all context needed
    for debugging without logging full message content.
    """
    _provider_logger.debug(
        f"{provider}_api_request",
        extra={
            "event": "api_request",
            "provider": provider,
            "model": model,
            "api_base": api_base,
            "max_tokens": max_tokens,
            "temperature": temperature,
            "thinking_enabled": thinking_enabled,
            "system_prompt_chars": len(system_prompt) if system_prompt else 0,
            "system_prompt_preview": (
                (system_prompt[:200] + "...")
                if system_prompt and len(system_prompt) > 200
                else system_prompt
            ),
            "message_count": len(messages),
            "messages_summary": _summarize_messages_for_log(messages),
            "tool_names": tools or [],
            "turn_idx": turn_idx,
            **extra,
        },
    )


def log_api_attempt(
    provider: str,
    model: str,
    attempt: int,
    max_attempts: int,
) -> None:
    """Log a wide event for an API attempt (retry tracking)."""
    _provider_logger.debug(
        f"{provider}_api_attempt",
        extra={
            "event": "api_attempt",
            "provider": provider,
            "model": model,
            "attempt": attempt,
            "max_attempts": max_attempts,
        },
    )


def log_api_response(
    provider: str,
    model: str,
    attempt: int,
    success: bool,
    input_tokens: int | None = None,
    output_tokens: int | None = None,
    cache_read_tokens: int | None = None,
    cache_write_tokens: int | None = None,
    reasoning_tokens: int | None = None,
    stop_reason: str | None = None,
    has_tool_calls: bool = False,
    error_type: str | None = None,
    error_message: str | None = None,
    **extra: object,
) -> None:
    """Log a wide event for an API response (success or failure)."""
    log_data = {
        "event": "api_response",
        "provider": provider,
        "model": model,
        "attempt": attempt,
        "success": success,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
        "stop_reason": stop_reason,
        "has_tool_calls": has_tool_calls,
        **extra,
    }

    # Add optional token fields only if present
    if cache_read_tokens is not None:
        log_data["cache_read_tokens"] = cache_read_tokens
    if cache_write_tokens is not None:
        log_data["cache_write_tokens"] = cache_write_tokens
    if reasoning_tokens is not None:
        log_data["reasoning_tokens"] = reasoning_tokens

    # Add error fields for failures
    if not success:
        log_data["error_type"] = error_type
        log_data["error_message"] = error_message

    _provider_logger.debug(f"{provider}_api_response", extra=log_data)


def calculate_cost_from_usage(usage: Usage, model_cost: ModelCost | None) -> Cost:
    """Pure function: Usage + ModelCost -> Cost.

    Following CLASSES_VS_FUNCTIONAL: pure math, no class needed.
    Following FAVORITES: explicit inputs/outputs, no hidden state.

    Args:
        usage: Token usage from API response
        model_cost: Pricing info from models.py (per million tokens)

    Returns:
        Cost breakdown in USD
    """
    if model_cost is None:
        return Cost()

    return Cost(
        input=(usage.input_tokens / 1_000_000) * model_cost.input,
        output=(usage.output_tokens / 1_000_000) * model_cost.output,
        cache_read=(usage.cache_read_tokens / 1_000_000) * model_cost.cache_read,
        cache_write=(usage.cache_write_tokens / 1_000_000) * model_cost.cache_write,
    )
