from __future__ import annotations

import json
import logging
from dataclasses import dataclass
from typing import Any, Dict, Mapping

from fastapi import WebSocket

from app.common.call_config import is_fake_agent_stress_test, resolve_fake_agent_overrides


@dataclass(frozen=True, slots=True)
class StartSessionContext:
    payload: Dict[str, Any]
    data: Dict[str, Any]
    agent_data: Dict[str, Any]
    audio_format: Dict[str, Any]
    call_config: Dict[str, Any]
    session_data: Dict[str, Any]
    intro: str
    nudge: str
    agent_starts_conversation: bool = False
    stress_test: bool = False
    debug_events_enabled: bool = False


def _pick_value(payload: Mapping[str, Any], *keys: str) -> str:
    for key in keys:
        value = payload.get(key)
        if value not in (None, ""):
            return str(value).strip()

    lowered = {str(key).lower(): value for key, value in payload.items()}
    for key in keys:
        value = lowered.get(str(key).lower())
        if value not in (None, ""):
            return str(value).strip()

    return ""


def _normalize_agent_name(value: Any) -> str:
    raw = str(value or "").strip().lower()
    aliases = {
        "conta": "conta",
        "contas": "conta",
        "ofert": "oferta",
        "oferta": "oferta",
        "ofertas": "oferta",
        "cobra": "cobranca",
        "cobranca": "cobranca",
        "cobrancas": "cobranca",
    }
    return aliases.get(raw, raw)


def should_agent_start_conversation(agent_name: str) -> bool:
    # O backend remoto é responsável pela saudação inicial em todos os agentes.
    del agent_name
    return True


def _mapping_or_error(payload: Mapping[str, Any], key: str) -> Dict[str, Any]:
    value = payload.get(key)
    if not isinstance(value, Mapping):
        raise RuntimeError(f"Campo obrigatório inválido: {key}")
    return dict(value)


def _optional_mapping(payload: Mapping[str, Any], key: str) -> Dict[str, Any]:
    value = payload.get(key)
    if value in (None, ""):
        return {}
    if not isinstance(value, Mapping):
        raise RuntimeError(f"Campo opcional inválido: {key}")
    return dict(value)


def _required_value(payload: Mapping[str, Any], *keys: str) -> str:
    value = _pick_value(payload, *keys)
    if not value:
        joined = ", ".join(keys)
        raise RuntimeError(f"Campo obrigatório ausente: {joined}")
    return value


def parse_start_payload(payload: Mapping[str, Any]) -> StartSessionContext:
    if payload.get("type") != "start":
        raise RuntimeError(
            f"Primeira mensagem esperada type='start', veio: {payload.get('type')}"
        )

    start_payload = dict(payload)
    data = _mapping_or_error(start_payload, "data")
    agent_data = _optional_mapping(data, "agentData")
    audio_format = _optional_mapping(start_payload, "audioFormat")
    call_config = _optional_mapping(start_payload, "callConfig")
    try:
        resolve_fake_agent_overrides(call_config)
        stress_test = is_fake_agent_stress_test(call_config)
    except ValueError as exc:
        raise RuntimeError(f"callConfig invalido: {exc}") from exc
    # Structured diagnostics may contain high-volume, implementation-specific
    # data. Forward them to the WS client only for an explicitly scripted fake
    # agent call; client-provided debug flags alone must not enable forwarding.
    debug_events_enabled = stress_test

    data["agent"] = _normalize_agent_name(_required_value(data, "agent"))
    _required_value(data, "ani")
    _required_value(data, "gsm")
    _required_value(data, "routerCallKeyDay")
    _required_value(data, "routerCallKey")
    _required_value(data, "callIdGed")
    session_id = _required_value(data, "session_id", "sessionId")
    data["session_id"] = session_id

    if data["agent"] == "conta":
        _required_value(
            agent_data,
            "idFatura",
            "current_invoice_number",
            "currentInvoiceNumber",
        )
    elif data["agent"] == "oferta":
        data["protocolo"] = _required_value(
            data,
            "protocolo",
            "protocolNumber",
            "protocol",
        )

    session_data = dict(data)
    if agent_data:
        session_data["agentData"] = dict(agent_data)
    if audio_format:
        session_data["audioFormat"] = dict(audio_format)
    if session_data.get("gsm"):
        session_data["msisdn"] = str(session_data["gsm"]).strip()
        session_data["phone"] = str(session_data["gsm"]).strip()

    start_payload["data"] = dict(data)
    agent_starts_conversation = should_agent_start_conversation(data["agent"])
    intro = ""
    nudge = "Alô, você ainda está aí?"

    return StartSessionContext(
        payload=start_payload,
        data=data,
        agent_data=agent_data,
        audio_format=audio_format,
        call_config=call_config,
        session_data=session_data,
        intro=intro,
        nudge=nudge,
        agent_starts_conversation=agent_starts_conversation,
        stress_test=stress_test,
        debug_events_enabled=debug_events_enabled,
    )


def parse_transferencia_session_id_payload(payload: Mapping[str, Any]) -> str:
    if payload.get("type") != "transferencia_session_id":
        raise RuntimeError(
            "Mensagem esperada type='transferencia_session_id', "
            f"veio: {payload.get('type')}"
        )

    data = _mapping_or_error(payload, "data")
    return _required_value(data, "session_id", "sessionId")


def _json_for_log(payload: Any) -> str:
    try:
        return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
    except TypeError:
        return str(payload)


def _redact_fake_responses(payload: Mapping[str, Any]) -> Dict[str, Any]:
    sanitized = dict(payload)
    call_config = sanitized.get("callConfig")
    if not isinstance(call_config, Mapping):
        return sanitized
    safe_call_config = dict(call_config)
    agent_fake = safe_call_config.get("agentFake")
    if isinstance(agent_fake, Mapping):
        safe_agent_fake = dict(agent_fake)
        raw = safe_agent_fake.pop("responses", None)
        if raw not in (None, ""):
            lengths = [len(item.strip()) for item in str(raw).split(";")]
            safe_agent_fake["responseCount"] = len(lengths)
            safe_agent_fake["responseLengths"] = lengths
        safe_call_config["agentFake"] = safe_agent_fake
    sanitized["callConfig"] = safe_call_config
    return sanitized


def _log_start_request(logger: logging.Logger | None, payload: Mapping[str, Any]) -> None:
    if logger is None:
        return

    logger.info(
        "WS_START_REQUEST\npayload=%s\ncall_config=%s",
        _json_for_log(_redact_fake_responses(payload)),
        _json_for_log(_redact_fake_responses(payload).get("callConfig") or {}),
    )


async def recv_start_message(
    ws: WebSocket,
    logger: logging.Logger | None = None,
) -> StartSessionContext:
    pending_session_id = ""

    while True:
        raw_text = await ws.receive_text()
        payload = json.loads(raw_text)

        if payload.get("type") == "transferencia_session_id":
            pending_session_id = parse_transferencia_session_id_payload(payload)
            continue

        if payload.get("type") == "start" and pending_session_id:
            start_payload = dict(payload)
            raw_data = start_payload.get("data")
            if isinstance(raw_data, Mapping):
                data = dict(raw_data)
                if not _pick_value(data, "session_id", "sessionId"):
                    data["session_id"] = pending_session_id
                    start_payload["data"] = data
            payload = start_payload

        _log_start_request(logger, payload)
        return parse_start_payload(payload)



def build_remote_agent_context(
    *,
    data: Mapping[str, Any],
    agent_data: Mapping[str, Any],
) -> Dict[str, Any]:
    agent_name = _normalize_agent_name(_pick_value(data, "agent"))

    context = {
        "agent": agent_name,
        "RouterCallKeyDay": _pick_value(data, "routerCallKeyDay"),
        "RouterCallKey": _pick_value(data, "routerCallKey"),
        "ANI": _pick_value(data, "ani"),
        "GSM": _pick_value(data, "gsm"),
        "msisdn": _pick_value(data, "gsm"),
        "callIdGed": _pick_value(data, "callIdGed"),
    }

    explicit_session_id = (
        _pick_value(data, "session_id", "sessionId")
        or _pick_value(agent_data, "session_id", "sessionId")
    )
    if explicit_session_id:
        context["session_id"] = explicit_session_id
        context["sessionId"] = explicit_session_id

    explicit_message_id = (
        _pick_value(data, "message_id", "messageId")
        or _pick_value(agent_data, "message_id", "messageId")
    )
    if explicit_message_id:
        context["message_id"] = explicit_message_id

    explicit_protocol_id = (
        _pick_value(data, "protocol_id", "protocolId")
        or _pick_value(agent_data, "protocol_id", "protocolId")
    )
    protocol_number = (
        explicit_protocol_id
        or _pick_value(data, "protocolo", "protocolNumber", "protocol")
        or _pick_value(agent_data, "protocolo", "protocolNumber", "protocol")
    )
    if protocol_number:
        context["protocolo"] = protocol_number
        context["protocolNumber"] = protocol_number
        if explicit_protocol_id:
            context["protocol_id"] = explicit_protocol_id

    channel_id = (
        _pick_value(data, "channelId", "channel_id")
        or _pick_value(agent_data, "channelId", "channel_id")
    )
    if channel_id:
        context["channelId"] = channel_id

    asset_id = (
        _pick_value(data, "assetId", "asset_id")
        or _pick_value(agent_data, "assetId", "asset_id")
    )
    if asset_id:
        context["assetId"] = asset_id

    if agent_name == "conta":
        invoice_number = (
            _pick_value(
                agent_data,
                "current_invoice_number",
                "currentInvoiceNumber",
                "idFatura",
            )
            or _pick_value(
                data,
                "current_invoice_number",
                "currentInvoiceNumber",
                "idFatura",
            )
        )
        if invoice_number:
            context["ID_FATURA"] = invoice_number
            context["current_invoice_number"] = invoice_number

        channel = (
            _pick_value(agent_data, "channel", "Channel", "canal")
            or _pick_value(data, "channel", "Channel", "canal")
        )
        if channel:
            context["channel"] = channel

    return context
