"""Anthropic Messages API compatible route."""

import asyncio
import json
import uuid
from collections.abc import AsyncGenerator
from datetime import UTC, datetime

from fastapi import APIRouter, HTTPException
from fastapi import Request as FastAPIRequest

from router_maestro.providers import AnthropicProvider, ChatRequest, ProviderError
from router_maestro.routing import Router, get_router
from router_maestro.server.schemas.anthropic import (
    AnthropicCountTokensRequest,
    AnthropicMessagesRequest,
    AnthropicMessagesResponse,
    AnthropicModelInfo,
    AnthropicModelList,
    AnthropicStreamState,
    AnthropicTextBlock,
    AnthropicToolUseBlock,
    AnthropicUsage,
)
from router_maestro.server.streaming import sse_streaming_response
from router_maestro.server.translation import (
    translate_anthropic_to_openai,
    translate_openai_chunk_to_anthropic_events,
)
from router_maestro.utils import (
    count_anthropic_request_tokens,
    get_logger,
    map_openai_stop_reason_to_anthropic,
)
from router_maestro.utils.context_window import resolve_thinking_budget
from router_maestro.utils.token_config import (
    count_tokens_via_anthropic_api,
    get_config_for_provider,
)

logger = get_logger("server.routes.anthropic")

router = APIRouter()


TEST_RESPONSE_TEXT = "This is a test response from Router-Maestro."


async def _resolve_provider_name(model: str) -> str | None:
    """Resolve which provider will handle a model.

    Returns the provider name (e.g. ``"github-copilot"``, ``"anthropic"``)
    or ``None`` if the model cannot be resolved.
    """
    model_router = get_router()
    try:
        provider_name, _actual_model, _provider = await model_router._resolve_provider(model)
        return provider_name
    except ProviderError:
        return None


async def _apply_thinking_budget(
    model_router: Router,
    chat_request: ChatRequest,
    original_model: str,
) -> ChatRequest:
    """Resolve thinking budget from server config and return updated request.

    Uses chat_request.model (the translated/normalized name) for cache
    lookups since translate_anthropic_to_openai may strip date suffixes.
    Falls back to original_model for config key matching.

    Returns the original request unchanged if no adjustment is needed.
    """
    from router_maestro.config import load_priorities_config

    priorities = load_priorities_config()
    thinking_config = priorities.thinking

    # Use translated model name for cache lookup (handles date-suffix stripping)
    translated_model = chat_request.model
    model_info = await model_router.get_model_info(translated_model)
    if model_info is None:
        model_info = await model_router.get_model_info(original_model)

    supports_thinking = model_info.supports_thinking if model_info else False
    max_output = (model_info.max_output_tokens or 16384) if model_info else 16384

    # Use translated model for config lookup (matches cache keys)
    budget, thinking_type = resolve_thinking_budget(
        client_budget=chat_request.thinking_budget,
        client_thinking_type=chat_request.thinking_type,
        model_id=translated_model,
        max_output_tokens=max_output,
        thinking_config=thinking_config,
        supports_thinking=supports_thinking,
    )

    if budget != chat_request.thinking_budget or thinking_type != chat_request.thinking_type:
        return chat_request.with_thinking(thinking_budget=budget, thinking_type=thinking_type)

    return chat_request


def _create_test_response() -> AnthropicMessagesResponse:
    """Create a mock response for test model."""
    return AnthropicMessagesResponse(
        id=f"msg_{uuid.uuid4().hex[:24]}",
        type="message",
        role="assistant",
        content=[AnthropicTextBlock(type="text", text=TEST_RESPONSE_TEXT)],
        model="test",
        stop_reason="end_turn",
        stop_sequence=None,
        usage=AnthropicUsage(input_tokens=10, output_tokens=10),
    )


async def _stream_test_response() -> AsyncGenerator[str, None]:
    """Stream a mock test response."""
    response_id = f"msg_{uuid.uuid4().hex[:24]}"

    # message_start event
    message_start = {
        "type": "message_start",
        "message": {
            "id": response_id,
            "type": "message",
            "role": "assistant",
            "content": [],
            "model": "test",
            "stop_reason": None,
            "stop_sequence": None,
            "usage": {"input_tokens": 10, "output_tokens": 0},
        },
    }
    yield f"event: message_start\ndata: {json.dumps(message_start)}\n\n"

    # content_block_start event
    block_start = {
        "type": "content_block_start",
        "index": 0,
        "content_block": {"type": "text", "text": ""},
    }
    yield f"event: content_block_start\ndata: {json.dumps(block_start)}\n\n"

    # content_block_delta event
    block_delta = {
        "type": "content_block_delta",
        "index": 0,
        "delta": {"type": "text_delta", "text": TEST_RESPONSE_TEXT},
    }
    yield f"event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n"

    # content_block_stop event
    block_stop = {"type": "content_block_stop", "index": 0}
    yield f"event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n"

    # message_delta event
    message_delta = {
        "type": "message_delta",
        "delta": {"stop_reason": "end_turn", "stop_sequence": None},
        "usage": {"output_tokens": 10},
    }
    yield f"event: message_delta\ndata: {json.dumps(message_delta)}\n\n"

    # message_stop event
    yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"


def _has_extended_context_beta(raw_request: FastAPIRequest) -> bool:
    """Check if the request includes an extended-context beta header.

    Claude CLI sends ``anthropic-beta: … context-1m-YYYY-MM-DD …`` (comma-separated)
    when the user selects a ``[1m]`` model variant.
    """
    header = raw_request.headers.get("anthropic-beta", "")
    return "context-1m" in header.lower()


@router.post("/v1/messages")
@router.post("/api/anthropic/v1/messages")
async def messages(request: AnthropicMessagesRequest, raw_request: FastAPIRequest):
    """Handle Anthropic Messages API requests."""
    logger.info(
        "Received Anthropic messages request: model=%s, stream=%s",
        request.model,
        request.stream,
    )

    # Handle test model
    if request.model == "test":
        if request.stream:
            return sse_streaming_response(_stream_test_response())
        return _create_test_response()

    model_router = get_router()

    # If the client signals extended-context (e.g. Claude CLI [1m] suffix),
    # try to resolve the -1m variant of the requested model.
    effective_model = request.model
    if _has_extended_context_beta(raw_request):
        variant = await model_router.find_extended_context_model(request.model)
        if variant is not None:
            logger.info(
                "Extended-context header detected, rewriting model %s -> %s",
                request.model,
                variant,
            )
            effective_model = variant

    # Translate Anthropic request to OpenAI format
    chat_request = translate_anthropic_to_openai(request)
    if effective_model != request.model:
        chat_request = ChatRequest(
            model=effective_model,
            messages=chat_request.messages,
            temperature=chat_request.temperature,
            max_tokens=chat_request.max_tokens,
            stream=chat_request.stream,
            tools=chat_request.tools,
            tool_choice=chat_request.tool_choice,
            thinking_budget=chat_request.thinking_budget,
            thinking_type=chat_request.thinking_type,
            extra=chat_request.extra,
        )

    # Resolve thinking budget from server config if needed
    chat_request = await _apply_thinking_budget(model_router, chat_request, effective_model)

    if request.stream:
        # Resolve provider for accurate token estimation
        provider_name = await _resolve_provider_name(request.model)
        estimated_tokens = _estimate_input_tokens(request, provider_name)
        return sse_streaming_response(
            stream_response(model_router, chat_request, request.model, estimated_tokens),
        )

    try:
        response, provider_name = await model_router.chat_completion(chat_request)

        logger.info(
            "Upstream response from %s: content_len=%s, tool_calls=%s, finish_reason=%s",
            provider_name,
            len(response.content) if response.content else 0,
            len(response.tool_calls) if response.tool_calls else 0,
            response.finish_reason,
        )

        # Build Anthropic response
        content = []
        if response.content:
            content.append(AnthropicTextBlock(type="text", text=response.content))

        # Convert OpenAI-format tool_calls to Anthropic tool_use blocks
        if response.tool_calls:
            for tc in response.tool_calls:
                func = tc.get("function", {})
                arguments = func.get("arguments", "{}")
                if isinstance(arguments, str):
                    try:
                        arguments = json.loads(arguments)
                    except (json.JSONDecodeError, TypeError):
                        arguments = {}
                content.append(
                    AnthropicToolUseBlock(
                        type="tool_use",
                        id=tc.get("id", ""),
                        name=func.get("name", ""),
                        input=arguments,
                    )
                )

        usage = AnthropicUsage(
            input_tokens=response.usage.get("prompt_tokens", 0) if response.usage else 0,
            output_tokens=response.usage.get("completion_tokens", 0) if response.usage else 0,
        )

        # Map finish reason
        stop_reason = map_openai_stop_reason_to_anthropic(response.finish_reason)

        return AnthropicMessagesResponse(
            id=f"msg_{uuid.uuid4().hex[:24]}",
            type="message",
            role="assistant",
            content=content,
            model=response.model,
            stop_reason=stop_reason,
            stop_sequence=None,
            usage=usage,
        )
    except ProviderError as e:
        logger.error("Anthropic messages request failed: %s", e)
        raise HTTPException(status_code=e.status_code, detail=str(e))


@router.post("/v1/messages/count_tokens")
@router.post("/api/anthropic/v1/messages/count_tokens")
async def count_tokens(request: AnthropicCountTokensRequest):
    """Count tokens for a messages request.

    Resolves which provider will handle the model and selects the appropriate
    token-counting configuration. For native Anthropic, attempts an upstream
    API call for an exact count before falling back to local estimation.
    """
    provider_name = await _resolve_provider_name(request.model)
    config = get_config_for_provider(provider_name)

    # For native Anthropic, try the upstream count_tokens API first
    if provider_name == "anthropic":
        model_router = get_router()
        provider = model_router.providers.get("anthropic")
        if isinstance(provider, AnthropicProvider) and provider.is_authenticated():
            try:
                # Serialise messages/tools to plain dicts for the API call
                msgs = [
                    m if isinstance(m, dict) else m.model_dump(exclude_none=True)
                    for m in request.messages
                ]
                system = request.system
                if system is not None and not isinstance(system, str):
                    system = [
                        b if isinstance(b, dict) else b.model_dump(exclude_none=True)
                        for b in system
                    ]
                tools_dicts = None
                if request.tools:
                    tools_dicts = [
                        t if isinstance(t, dict) else t.model_dump(exclude_none=True)
                        for t in request.tools
                    ]
                # Strip provider prefix from model name for upstream API
                model_name = request.model
                if "/" in model_name:
                    model_name = model_name.split("/", 1)[1]

                exact_tokens = await count_tokens_via_anthropic_api(
                    base_url=provider.base_url,
                    api_key=provider._get_api_key(),
                    model=model_name,
                    messages=msgs,
                    system=system,
                    tools=tools_dicts,
                )
                return {"input_tokens": exact_tokens}
            except Exception:
                logger.warning(
                    "Anthropic upstream count_tokens failed, falling back to local estimation",
                    exc_info=True,
                )

    input_tokens = count_anthropic_request_tokens(
        system=request.system,
        messages=request.messages,
        tools=request.tools,
        model=request.model,
        config=config,
    )
    return {"input_tokens": input_tokens}


def _estimate_input_tokens(
    request: AnthropicMessagesRequest,
    provider_name: str | None = None,
) -> int:
    """Estimate input tokens from request content.

    Uses the centralized token estimation function with provider-aware
    configuration for accurate estimates.
    """
    config = get_config_for_provider(provider_name)
    return count_anthropic_request_tokens(
        system=request.system,
        messages=request.messages,
        tools=request.tools,
        model=request.model,
        config=config,
    )


async def stream_response(
    model_router: Router,
    request: ChatRequest,
    original_model: str,
    estimated_input_tokens: int = 0,
) -> AsyncGenerator[str, None]:
    """Stream Anthropic Messages API response."""
    try:
        stream, provider_name = await model_router.chat_completion_stream(request)
        response_id = f"msg_{uuid.uuid4().hex[:24]}"

        state = AnthropicStreamState(estimated_input_tokens=estimated_input_tokens)

        async for chunk in stream:
            # Build OpenAI-style chunk for translation
            openai_chunk = {
                "id": response_id,
                "choices": [
                    {
                        "delta": {
                            "content": chunk.content if chunk.content else None,
                            "tool_calls": chunk.tool_calls,
                        },
                        "finish_reason": chunk.finish_reason,
                    }
                ],
                "usage": chunk.usage,  # Pass through usage info
            }

            events = translate_openai_chunk_to_anthropic_events(openai_chunk, state, original_model)

            for event in events:
                yield f"event: {event['type']}\ndata: {json.dumps(event)}\n\n"

    except ProviderError as e:
        yield _sse_error_event(str(e))
    except asyncio.CancelledError:
        logger.info("Anthropic stream cancelled by client")
        raise
    except Exception:
        logger.error("Unexpected error in Anthropic stream", exc_info=True)
        yield _sse_error_event("Internal server error")


def _sse_error_event(message: str) -> str:
    """Format an Anthropic SSE error event."""
    error_event = {
        "type": "error",
        "error": {
            "type": "api_error",
            "message": message,
        },
    }
    return f"event: error\ndata: {json.dumps(error_event)}\n\n"


def _generate_display_name(model_id: str) -> str:
    """Generate a human-readable display name from model ID.

    Transforms model IDs like 'github-copilot/claude-sonnet-4' into
    'Claude Sonnet 4 (github-copilot)'.
    """
    if "/" in model_id:
        provider, model_name = model_id.split("/", 1)
    else:
        provider = ""
        model_name = model_id

    # Capitalize words and handle common patterns
    words = model_name.replace("-", " ").replace("_", " ").split()
    display_words = []
    for word in words:
        # Keep version numbers as-is
        if word.replace(".", "").isdigit():
            display_words.append(word)
        else:
            display_words.append(word.capitalize())

    display_name = " ".join(display_words)
    if provider:
        display_name = f"{display_name} ({provider})"

    return display_name


@router.get("/api/anthropic/v1/models")
async def list_models(
    limit: int = 20,
    after_id: str | None = None,
    before_id: str | None = None,
) -> AnthropicModelList:
    """List available models in Anthropic format.

    Args:
        limit: Maximum number of models to return (default 20)
        after_id: Return models after this ID (for forward pagination)
        before_id: Return models before this ID (for backward pagination)
    """
    model_router = get_router()
    models = await model_router.list_models()

    # Generate ISO 8601 timestamp for created_at
    # Using current time since actual creation dates aren't tracked
    created_at = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")

    # Convert to Anthropic format
    anthropic_models = [
        AnthropicModelInfo(
            id=model.id,
            created_at=created_at,
            display_name=_generate_display_name(model.id),
            type="model",
            max_prompt_tokens=model.max_prompt_tokens,
            max_output_tokens=model.max_output_tokens,
            max_context_window_tokens=model.max_context_window_tokens,
            supports_thinking=model.supports_thinking or None,
            supports_vision=model.supports_vision or None,
        )
        for model in models
    ]

    # Handle pagination
    start_idx = 0
    if after_id:
        for i, model in enumerate(anthropic_models):
            if model.id == after_id:
                start_idx = i + 1
                break

    end_idx = len(anthropic_models)
    if before_id:
        for i, model in enumerate(anthropic_models):
            if model.id == before_id:
                end_idx = i
                break

    # Apply limit
    paginated = anthropic_models[start_idx : min(start_idx + limit, end_idx)]

    first_id = paginated[0].id if paginated else None
    last_id = paginated[-1].id if paginated else None
    has_more = (start_idx + limit) < end_idx

    return AnthropicModelList(
        data=paginated,
        first_id=first_id,
        last_id=last_id,
        has_more=has_more,
    )
