"""Composição determinística da resposta ao cliente após cancelamento de VAS avulso.

FONTE ÚNICA das regras da fala ao cliente neste fluxo. Antes, o texto era gerado por
uma chamada LLM (capability ``fluxo_vas_cancelamento_resposta_cliente``, hoje
REMOVIDA) que apenas renderizava prosa a partir do payload. Como cada ramo é função
pura de campos já conhecidos (``sms_sent``, ``contested_items``,
``contestation_error_description`` etc.), a composição vive aqui: mesma saída
canônica, sem prompt, sem LLM, sem não-determinismo. O runtime entrega o texto
verbatim ao cliente.

Regras (mantenha esta seção como o spec ao alterar comportamento):
- Ordem de montagem: (a) resultado [regras 1.1, 2–5] → (b) SMS [regra 6] →
  (c) protocolo [regra 7].
- Regra 1.1: itens já contestados + itens contestados nesta solicitação (partes
  independentes, concordância por lista); destino do valor por ``sms_sent``.
- Regra 2 (``sms_sent=false``): crédito na próxima fatura.
- Regra 3 (``sms_sent=true``): a confirmação canônica de cancelamento, retirada
  do valor e envio do novo código por SMS prevalece mesmo sem itens novos.
- Regra 5: itens que não puderam ser contestados.
- Regra 6: SMS 6.1 (ok) / 6.2 (falha) mutuamente exclusivas por ``sms_not_send_error``.
- Regra 7: protocolo(s) em forma canônica, sempre ao final.
- Regra 8: erro terminal → copia ``contestation_error_description`` literal + protocolo;
  para item já contestado, informa antes os cancelamentos efetivados.
- No-match de cancelamento (``nao_encontrados`` não vazio): serviço não localizado
  na plataforma por divergência de nome → cancelamento não executado, mas o valor
  foi contestado. Estrutura própria (``_compose_no_match``): (a) valor total
  creditado/retirado [+ SMS] → (b) cancelamentos efetivados (``cancelados``, caso
  misto) → (c) no-match + orientação App Meu TIM (opção Gerenciar Benefícios) → protocolo.
- Residual: sucesso sem itens detalhados → confirmação genérica neutra + protocolo.
"""

from __future__ import annotations

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


def _clean(value: Any) -> str:
    return str(value or "").strip()


def _item_name(item: Any) -> str:
    if isinstance(item, Mapping):
        return _clean(
            item.get("itemName")
            or item.get("item_name")
            or item.get("name")
            or item.get("servico")
            or item.get("service")
        )
    return _clean(item)


def _names(items: Any) -> list[str]:
    if not isinstance(items, Sequence) or isinstance(items, (str, bytes)):
        return []
    return [name for name in (_item_name(item) for item in items) if name]


def _join_e(names: Sequence[str]) -> str:
    """Junção natural pt-BR: "A"; "A e B"; "A, B e C"."""
    names = list(names)
    if not names:
        return ""
    if len(names) == 1:
        return names[0]
    return f"{', '.join(names[:-1])} e {names[-1]}"


def _is_already_disputed_error(description: str) -> bool:
    normalized = description.casefold()
    return any(
        marker in normalized
        for marker in (
            "já contestado",
            "já foi contestado",
            "já foram contestados",
        )
    )


def _cancellation_success_sentence(canceled: Sequence[str]) -> str:
    if len(canceled) == 1:
        return f"O cancelamento do item {canceled[0]} foi concluído com sucesso."
    return (
        f"O cancelamento dos itens {_join_e(canceled)} foi concluído com sucesso."
    )


def _protocol_sentence(payload: Mapping[str, Any]) -> str:
    """Regra 7 — protocolo em forma canônica (ex.: ``PRT475D8F1C1F``)."""
    protocols = [_clean(p) for p in (payload.get("contestacao_protocols") or [])]
    protocols = [p for p in protocols if p]
    single = _clean(payload.get("contestacao_protocol"))
    if not protocols and single:
        protocols = [single]
    if not protocols:
        return ""
    if len(protocols) == 1:
        return f"Seu número de protocolo é {protocols[0]}."
    return f"Seus números de protocolo são {_join_e(protocols)}."


def _sms_sentence(payload: Mapping[str, Any]) -> str:
    """Regra 6 — mensagens 6.1/6.2 mutuamente exclusivas por ``sms_not_send_error``."""
    if bool(payload.get("sms_not_send_error")):
        return (
            "Identificamos uma instabilidade no envio do SMS com o novo código "
            "para pagamento. Você pode consultar o código atualizado e o prazo "
            "de pagamento diretamente no app Meu TIM."
        )
    return (
        "Enviamos um SMS com o novo código de barras e o valor atualizado. "
        "O prazo para pagamento é de 4 dias."
    )


def _compose_no_match(
    payload: Mapping[str, Any],
    *,
    amount: str,
    sms_sent: bool,
    contested: list[str],
    not_contested: list[str],
    canceled: list[str],
    no_match: list[str],
    protocol: str,
) -> str:
    """No-match de cancelamento: serviço(s) não localizado(s) na plataforma por
    divergência de nome → o cancelamento não foi executado pelo canal, mas o valor
    foi contestado (ajuste em fatura). Estrutura: (a) valor [+ SMS] → (b) lista de
    cancelamentos com sucesso (se houver) → (c) no-match + orientação App Meu TIM →
    (d) protocolo. A frase de encerramento é anexada pelo runtime, não aqui.
    """
    parts: list[str] = []

    # (a) Destino do valor contestado — só quando houve contestação de fato.
    if contested or amount not in ("", "0,00"):
        if sms_sent:
            parts.append(f"O valor total de R$ {amount} foi retirado da sua fatura.")
            parts.append(_sms_sentence(payload))
        else:
            parts.append(
                f"O valor total de R$ {amount} ficou registrado como crédito para "
                "sua próxima fatura."
            )

    # (b) Cancelamentos efetivados (caso misto).
    if canceled:
        if len(canceled) == 1:
            parts.append(
                f"O cancelamento do serviço {canceled[0]} foi realizado com sucesso."
            )
        else:
            parts.append(
                f"O cancelamento dos serviços {_join_e(canceled)} foram realizados "
                "com sucesso."
            )

    # (c) No-match + orientação para o App Meu TIM. A forma da frase difere entre
    # o caso misto (há lista de sucesso antes) e o caso tudo-no-match.
    if canceled:
        if len(no_match) == 1:
            parts.append(
                f"Não consegui realizar por aqui o cancelamento do serviço "
                f"{no_match[0]}. Você pode cancelá-lo pelo App Meu TIM, na opção "
                "Gerenciar Benefícios."
            )
        else:
            parts.append(
                f"Não consegui realizar por aqui o cancelamento dos serviços "
                f"{_join_e(no_match)}. Você pode cancelá-los pelo App Meu TIM, na "
                "opção Gerenciar Benefícios."
            )
    else:
        if len(no_match) == 1:
            parts.append(
                f"Sobre o cancelamento do serviço {no_match[0]}, não consegui "
                "realizar por aqui. Você pode cancelar pelo App Meu TIM, na opção "
                "Gerenciar Benefícios."
            )
        else:
            parts.append(
                f"Sobre o cancelamento dos serviços {_join_e(no_match)}, não "
                "consegui realizar por aqui. Você pode cancelar pelo App Meu TIM, "
                "na opção Gerenciar Benefícios."
            )

    # Regra 5 — itens que não puderam ser contestados (raro combinado com no-match).
    if not_contested:
        parts.append(
            f"Não foi possível contestar o item {', '.join(not_contested)}."
        )

    if protocol:
        parts.append(protocol)

    return " ".join(part.strip() for part in parts if part.strip()).strip()


def compose_vas_cancellation_message(payload: Mapping[str, Any]) -> str:
    """Monta a resposta ao cliente a partir do payload estruturado.

    Retorna ``""`` apenas quando não há sucesso nem nenhuma regra aplicável
    (ex.: erro sem descrição). Nesse caso o backend aplica sua mensagem de erro
    genérica — não há chamada LLM em nenhum caminho.
    """
    if not isinstance(payload, Mapping):
        return ""

    error_desc = _clean(payload.get("contestation_error_description"))
    protocol = _protocol_sentence(payload)
    canceled = _names(payload.get("cancelados"))

    # Regra 8 — contestação recusada pela API: preserva os cancelamentos que já
    # ocorreram, mas deixa claro que uma nova contestação não pôde ser registrada.
    if error_desc:
        parts: list[str] = []
        if canceled and _is_already_disputed_error(error_desc):
            parts.append(_cancellation_success_sentence(canceled))
        if not error_desc.endswith((".", "!", "?")) and protocol:
            error_desc = f"{error_desc}."
        parts.append(error_desc)
        if protocol:
            parts.append(protocol)
        return " ".join(parts)

    amount = _clean(payload.get("contested_invoice_amount_open"))
    sms_sent = bool(payload.get("sms_sent"))
    contested = _names(payload.get("contested_items"))
    not_contested = _names(payload.get("not_contested_items"))
    already = [_clean(x) for x in (payload.get("itens_ja_contestados") or [])]
    already = [x for x in already if x]
    no_match = _names(payload.get("nao_encontrados"))

    # No-match de cancelamento (nome divergente na plataforma): o cancelamento não
    # foi executado, mas o valor foi contestado. Assume a estrutura própria
    # (lidera pelo valor + orienta App Meu TIM), distinta das regras 1.1/2–5.
    if no_match:
        return _compose_no_match(
            payload,
            amount=amount,
            sms_sent=sms_sent,
            contested=contested,
            not_contested=not_contested,
            canceled=canceled,
            no_match=no_match,
            protocol=protocol,
        )

    # Regra 3 — o envio do novo boleto por SMS é o sinal definitivo sobre o
    # destino do ajuste. Ele prevalece sobre ``contested_items`` vazio (por
    # exemplo, quando a API não devolve os itens detalhados) e nunca pode cair
    # no texto de crédito para a próxima fatura.
    if sms_sent and not bool(payload.get("sms_not_send_error")):
        parts = [
            "O cancelamento foi concluído com sucesso. O valor contestado foi "
            "retirado da sua fatura.",
            _sms_sentence(payload),
        ]
        if not_contested:
            parts.append(
                f"Não foi possível contestar o item {', '.join(not_contested)}."
            )
        if protocol:
            parts.append(protocol)
        return " ".join(part.strip() for part in parts if part.strip()).strip()

    # Sem nenhum item detalhado (contestado, não contestado ou já contestado):
    # não há como aplicar as regras 1.1/2–5. Se o cancelamento teve sucesso,
    # devolve uma confirmação genérica neutra (não promete crédito nem boleto,
    # que dependem dos itens); o protocolo entra pela regra 7 (ou pelo append do
    # backend, quando só há protocolo de cancelamento). Sem sucesso, devolve ""
    # para o backend aplicar sua mensagem de erro genérica.
    if not (contested or not_contested or already):
        if not bool(payload.get("success")):
            return ""
        generico = "O cancelamento foi concluído com sucesso."
        return f"{generico} {protocol}".strip() if protocol else generico

    def _ja_contestados_notice() -> str:
        # Parte 1 da regra 1.1 (concordância pela própria lista).
        if len(already) == 1:
            return (
                f"Identifiquei que o item {already[0]} já havia sido contestado "
                "anteriormente e, por esse motivo, não é possível registrar uma "
                "nova contestação para ele."
            )
        return (
            f"Identifiquei que os itens {_join_e(already)} já haviam sido "
            "contestados anteriormente e, por esse motivo, não é possível "
            "registrar uma nova contestação para eles."
        )

    parts: list[str] = []
    emit_sms = False

    if already and contested:
        # Regra 1.1 — misto: parte por lista própria (concordância independente).
        parts.append(_ja_contestados_notice())
        if len(contested) == 1:
            parts.append(
                f"Já a contestação do item {contested[0]} no valor de R$ {amount} "
                "foi concluída com sucesso."
            )
        else:
            parts.append(
                f"Já a contestação dos itens {_join_e(contested)} no valor total "
                f"de R$ {amount} foi concluída com sucesso."
            )
        if sms_sent:
            parts.append("O valor contestado já foi retirado da sua fatura.")
            emit_sms = True
        else:
            parts.append(
                "O valor contestado ficou registrado como crédito para sua "
                "próxima fatura."
            )
        # Regras 2 e 3 NÃO se aplicam neste caso.
    elif already and not contested:
        # Gap fora das regras 1.1/2/3/4 (só itens já contestados, sem SMS): nada de
        # novo foi contestado, então só o aviso "já contestado" — não afirma
        # cancelamento/crédito que não ocorreu.
        parts.append(_ja_contestados_notice())
    elif sms_sent:
        # Regra 3 — valor retirado da fatura (novo boleto por SMS).
        if len(contested) == 1:
            parts.append(
                f"O cancelamento do item {contested[0]} no valor de R$ {amount} "
                "foi concluído com sucesso. O valor contestado já foi retirado da "
                "sua fatura."
            )
        else:
            parts.append(
                "O cancelamento dos itens foi concluído com sucesso. O valor total "
                f"de R$ {amount} foi retirado da sua fatura."
            )
        emit_sms = True
    else:
        # Regra 2 — crédito na próxima fatura (sms_sent=false).
        if len(contested) == 1:
            parts.append(
                f"O cancelamento do item {contested[0]} no valor de R$ {amount} "
                "foi concluído com sucesso. O valor contestado ficou registrado "
                "como crédito para sua próxima fatura."
            )
        else:
            parts.append(
                "O cancelamento dos itens foi feito com sucesso. O crédito no valor "
                f"total de R$ {amount} ficou registrado como crédito para sua "
                "próxima fatura."
            )

    if not parts:
        return ""

    # Regra 5 — itens que não puderam ser contestados (parte (a) do resultado).
    if not_contested:
        parts.append(
            f"Não foi possível contestar o item {', '.join(not_contested)}."
        )

    # (b) Notificação de SMS.
    if emit_sms:
        parts.append(_sms_sentence(payload))

    # (c) Protocolo, sempre.
    if protocol:
        parts.append(protocol)

    return " ".join(part.strip() for part in parts if part.strip()).strip()
