"""Response serialization helpers for gateway HTTP handlers."""

from __future__ import annotations

import json
import uuid
from dataclasses import asdict, replace
from typing import Any, Mapping, Sequence

from aiohttp import web

from ember.models.providers.anthropic_responses import AnthropicParams, build_anthropic_params
from ember.models.providers.utils import ResponsesRequest

from ..domain.model import ChatResponse, StreamingResponse, Usage
from ..infra.middleware.context import RequestContext
from .telemetry import _SYNTHESIS_DEBUG_FIELDS

_DEBUG_ROLES = frozenset({"admin", "operator", "internal"})


def _allow_debug_payload(ctx: RequestContext) -> bool:
    role = (ctx.api_key_role or "").strip().lower()
    return bool(role) and role in _DEBUG_ROLES


def _serialize_response(response: ChatResponse, *, include_debug: bool = False) -> dict[str, Any]:
    choices: list[dict[str, Any]] = []
    for choice in response.choices:
        message: dict[str, Any] = {
            "role": choice.message.role,
            "content": choice.message.content,
        }
        if choice.message.tool_calls:
            message["tool_calls"] = choice.message.tool_calls
        if choice.message.tool_call_id:
            message["tool_call_id"] = choice.message.tool_call_id
        choices.append({
            "index": choice.index,
            "message": message,
            "finish_reason": choice.finish_reason,
        })
    data: dict[str, Any] = {
        "id": response.id,
        "object": "chat.completion",
        "created": response.created,
        "model": response.model,
        "choices": choices,
        "usage": {
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens,
            "total_tokens": response.usage.total_tokens,
        },
    }
    if include_debug:
        data["provider"] = response.provider
        data["billing_record_id"] = response.billing_record_id
        data["router"] = {
            "session_affinity": response.router.session_affinity,
            "score_components": response.router.score_components,
        }
        data["usage"]["cost_usd"] = response.usage.cost_usd
        data["raw"] = response.raw
    return data


def _responses_payload_from_response(
    response: ChatResponse,
    *,
    include_debug: bool = False,
) -> dict[str, Any]:
    raw_payload = response.raw.get("responses_payload") if isinstance(response.raw, dict) else None
    if not isinstance(raw_payload, Mapping):
        raise web.HTTPInternalServerError(text="provider did not return responses payload")
    payload = dict(raw_payload)
    payload.setdefault("id", response.id)
    payload["model"] = response.model
    usage = payload.get("usage")
    if not isinstance(usage, Mapping):
        usage_dict: dict[str, object] = {
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens,
            "total_tokens": response.usage.total_tokens,
        }
        if include_debug:
            usage_dict["cost_usd"] = response.usage.cost_usd
        payload["usage"] = usage_dict
    # Strip EB1 synthesis debug fields from metadata for non-admin users.
    # This ensures regular users see EB1 responses identical to GPT-5/Claude.
    if not include_debug:
        metadata = payload.get("metadata")
        if isinstance(metadata, dict):
            payload["metadata"] = {
                k: v for k, v in metadata.items() if k not in _SYNTHESIS_DEBUG_FIELDS
            }
    return payload


def _messages_payload_from_response(
    response: ChatResponse,
    *,
    include_debug: bool = False,
) -> dict[str, Any]:
    """Convert a ChatResponse to Anthropic Messages API format.

    Response format:
        {
            "id": "msg_...",
            "type": "message",
            "role": "assistant",
            "content": [{"type": "text", "text": "Hello!"}],
            "model": "claude-3-opus-20240229",
            "stop_reason": "end_turn",
            "stop_sequence": null,
            "usage": {"input_tokens": 10, "output_tokens": 5}
        }
    """
    # Extract content from choices
    content: list[dict[str, Any]] = []
    stop_reason = "end_turn"

    if response.choices:
        choice = response.choices[0]
        if choice.message.content:
            content.append({"type": "text", "text": choice.message.content})

        # Map finish_reason to Anthropic's stop_reason
        finish_reason = choice.finish_reason
        if finish_reason == "stop":
            stop_reason = "end_turn"
        elif finish_reason == "length":
            stop_reason = "max_tokens"
        elif finish_reason == "tool_calls":
            stop_reason = "tool_use"
        elif finish_reason:
            stop_reason = finish_reason

    # Build the Anthropic Messages response
    payload: dict[str, Any] = {
        "id": response.id,
        "type": "message",
        "role": "assistant",
        "content": content,
        "model": response.model,
        "stop_reason": stop_reason,
        "stop_sequence": None,
        "usage": {
            "input_tokens": response.usage.prompt_tokens,
            "output_tokens": response.usage.completion_tokens,
        },
    }

    # Include debug info if requested
    if include_debug and response.raw:
        payload["_debug"] = response.raw

    return payload


def _provider_model_name(provider_model: str) -> str:
    if "/" in provider_model:
        return provider_model.split("/", 1)[1]
    return provider_model


def _build_anthropic_params_for_stream(
    responses_req: ResponsesRequest | None,
    provider_model: str,
) -> AnthropicParams | None:
    if responses_req is None:
        return None
    request_for_provider = replace(responses_req, model=provider_model)
    return build_anthropic_params(request_for_provider)


def _tool_use_blocks(tool_calls: Sequence[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
    blocks: list[Mapping[str, Any]] = []
    for call in tool_calls:
        call_id = str(call.get("id") or f"tc_{uuid.uuid4().hex}")
        name = str(call.get("name") or "")
        arguments = call.get("arguments")
        if isinstance(arguments, str):
            try:
                parsed_arguments = json.loads(arguments)
            except json.JSONDecodeError as exc:
                raise RuntimeError("Anthropic tool arguments must be valid JSON") from exc
        else:
            parsed_arguments = arguments
        if parsed_arguments is None:
            parsed_arguments = {}
        if not isinstance(parsed_arguments, Mapping):
            raise RuntimeError("Anthropic tool arguments must be objects")
        blocks.append(
            {
                "type": "tool_use",
                "id": call_id,
                "name": name,
                "input": dict(parsed_arguments),
            }
        )
    return blocks


def _extract_tool_calls(payload: Mapping[str, Any]) -> list[Mapping[str, Any]]:
    required_action = payload.get("required_action")
    if not isinstance(required_action, Mapping):
        return []
    submit = required_action.get("submit_tool_outputs")
    if not isinstance(submit, Mapping):
        return []
    tool_calls = submit.get("tool_calls")
    if not isinstance(tool_calls, Sequence):
        return []
    return [call for call in tool_calls if isinstance(call, Mapping)]


def _build_stream_header_payload(
    response: StreamingResponse,
    *,
    include_debug: bool,
) -> dict[str, object]:
    metadata: dict[str, object] = {
        "id": response.id,
        "model": response.model,
    }
    if include_debug:
        metadata["provider"] = response.provider
        metadata["provider_model"] = response.provider_model
        metadata["router"] = {
            "session_affinity": response.router.session_affinity,
            "score_components": response.router.score_components,
        }
        metadata["prompt_estimate"] = response.prompt_estimate
        metadata["completion_budget"] = response.completion_budget
        metadata["billing"] = {
            "billing_mode": response.billing_mode,
            "price_book_version": response.price_book_version,
            "fbr_id": response.fbr_id,
            "idempotency_key": response.idempotency_key,
            "api_key_id": response.api_key_id,
        }
        metadata["admission"] = asdict(response.admission)
        metadata["rbc"] = _serialize_stream_rbc(response)
        metadata["plan"] = response.plan
        metadata["provider_metadata"] = response.provider_metadata
    return metadata


def _build_stream_done_payload(
    response: StreamingResponse,
    final_usage: Usage,
    *,
    text: str,
    include_debug: bool,
) -> dict[str, object]:
    usage_dict: dict[str, object] = {
        "prompt_tokens": final_usage.prompt_tokens,
        "completion_tokens": final_usage.completion_tokens,
        "total_tokens": final_usage.total_tokens,
    }
    if include_debug:
        usage_dict["cost_usd"] = final_usage.cost_usd
    payload: dict[str, object] = {
        "id": response.id,
        "usage": usage_dict,
        "text": text,
    }
    if include_debug:
        payload["provider_metadata"] = response.provider_metadata
    return payload


def _serialize_stream_rbc(response: StreamingResponse) -> dict[str, object]:
    payload = asdict(response.rbc)
    payload["token"] = response.rbc_token
    return payload


def _rewrite_responses_model(
    event: dict[str, Any], canonical_model: str
) -> bool:
    """Rewrite model fields in Responses API SSE events to hide backend.

    Returns:
        True if any field was modified (caller should re-serialize).
    """
    modified = False
    if "model" in event:
        event["model"] = canonical_model
        modified = True
    resp_obj = event.get("response")
    if isinstance(resp_obj, dict) and "model" in resp_obj:
        resp_obj["model"] = canonical_model
        modified = True
    return modified
