from __future__ import annotations

from contextlib import nullcontext
from decimal import Decimal, ROUND_HALF_UP
import logging
import os
import re
import unicodedata as ud
from typing import Any

_CENT = Decimal("0.01")
_GUARDRAIL_ACTION = "abrir_contestacao_cliente"
_GUARDRAIL_CODE = "CVAL"
_STRATEGIC_SERVICE_ALIASES = (
    "apple music",
    "deezer",
    "disney",
    "fuze",
    "forge",
    "hbo",
    "looke",
    "netflix",
    "paramount",
    "paramount+",
    "paramount plus",
    "tim cloud gaming",
    "youtube",
    "youtube premium",
)

logger = logging.getLogger(__name__)


def _money(value: Decimal) -> Decimal:
    return value.quantize(_CENT, rounding=ROUND_HALF_UP)


def _parse_amount(value: str) -> Decimal | None:
    """Parse monetary values without assuming that every dot is a thousands separator.

    Accepted examples include Brazilian and API/JSON representations such as
    ``R$ 19,99``, ``19.99``, ``1.999,99`` and ``1,999.99``.
    """
    if value is None:
        return None

    cleaned = str(value).strip().replace("R$", "").replace(" ", "")
    if not cleaned:
        return None

    # Keep only a numeric sign and decimal/grouping separators. This avoids
    # accidentally feeding currency labels or other text to Decimal.
    cleaned = re.sub(r"[^0-9, .+\-]", "", cleaned).replace(" ", "")
    if not cleaned:
        return None

    comma = cleaned.rfind(",")
    dot = cleaned.rfind(".")

    if comma >= 0 and dot >= 0:
        # The rightmost separator is the decimal separator; the other one is
        # grouping. This supports both 1.999,99 and 1,999.99.
        if comma > dot:
            cleaned = cleaned.replace(".", "").replace(",", ".")
        else:
            cleaned = cleaned.replace(",", "")
    elif comma >= 0:
        # pt-BR decimal notation. Multiple commas are treated conservatively
        # by preserving only the last one as decimal separator.
        if cleaned.count(",") > 1:
            head, tail = cleaned.rsplit(",", 1)
            cleaned = head.replace(",", "") + "." + tail
        else:
            cleaned = cleaned.replace(",", ".")
    elif dot >= 0:
        # A single dot followed by 1-2 digits is decimal notation (the form
        # normally returned by JSON/backends). For multiple dots, keep the
        # last one as decimal only when it looks like cents; otherwise treat
        # them as grouping separators.
        if cleaned.count(".") > 1:
            head, tail = cleaned.rsplit(".", 1)
            if 1 <= len(tail) <= 2:
                cleaned = head.replace(".", "") + "." + tail
            else:
                cleaned = cleaned.replace(".", "")

    try:
        return Decimal(cleaned)
    except Exception:
        return None


def _decimal_from_any(value: Any) -> Decimal | None:
    if value is None or isinstance(value, bool):
        return None
    if isinstance(value, Decimal):
        return value
    if isinstance(value, (int, float)):
        return Decimal(str(value))
    return _parse_amount(str(value or ""))


def _first_decimal_from_mapping(data: dict[str, Any], *keys: str) -> Decimal | None:
    for key in keys:
        if key not in data:
            continue
        value = _decimal_from_any(data.get(key))
        if value is not None:
            return value
    return None


def _normalize_number_text(value: Any, *, default: str = "0") -> str:
    text = str(value).strip()
    if not text:
        return default
    cleaned = text.replace("R$", "").replace(" ", "")
    if "," in cleaned:
        cleaned = cleaned.replace(".", "").replace(",", ".")
    try:
        normalized = format(Decimal(cleaned), "f")
    except Exception:
        return default
    if "." in normalized:
        normalized = normalized.rstrip("0").rstrip(".")
    return normalized or default


def _normalize_match_text(value: Any) -> str:
    text = re.sub(r"\s*\([^)]*\)", "", str(value or "")).strip()
    text = ud.normalize("NFKD", text)
    text = "".join(ch for ch in text if not ud.combining(ch))
    text = text.casefold()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    return re.sub(r"\s+", " ", text).strip()


def _is_same_plan_name(left: Any, right: Any) -> bool:
    left_key = _normalize_match_text(left)
    right_key = _normalize_match_text(right)
    if not left_key or not right_key:
        return False
    return left_key == right_key or left_key in right_key or right_key in left_key


def _normalize_service_name_for_match(value: Any) -> str:
    normalized = ud.normalize("NFKD", str(value or "").lower())
    without_accents = "".join(ch for ch in normalized if not ud.combining(ch))
    return re.sub(r"[^a-z0-9]+", "", without_accents)


def _is_strategic_partner_service(value: Any) -> bool:
    normalized = _normalize_service_name_for_match(value)
    if not normalized:
        return False
    for alias in _STRATEGIC_SERVICE_ALIASES:
        normalized_alias = _normalize_service_name_for_match(alias)
        if normalized_alias and normalized_alias in normalized:
            return True
    return False


def _is_vas_section_name(section_name: str) -> bool:
    normalized = _normalize_match_text(section_name)
    return (
        "vas" in normalized
        or "valor adicionado" in normalized
        or "servicos de valor adicionado" in normalized
        or "servicos valor adicionado" in normalized
        or "sva detalhe total" in normalized
        or "servicos contratados de parceiros" in normalized
        or "servico contratado de parceiro" in normalized
    )


def _extract_invoice_total_geral(payload: Any) -> Decimal | None:
    if isinstance(payload, dict):
        desc = _normalize_match_text(payload.get("desc", ""))
        if desc == "total geral":
            total = _decimal_from_any(
                payload.get("value")
                if "value" in payload
                else payload.get("valor")
            )
            if total is not None:
                return total
        for value in payload.values():
            if isinstance(value, (dict, list, tuple)):
                result = _extract_invoice_total_geral(value)
                if result is not None:
                    return result
    elif isinstance(payload, (list, tuple)):
        for entry in payload:
            if isinstance(entry, (dict, list, tuple)):
                result = _extract_invoice_total_geral(entry)
                if result is not None:
                    return result
    return None


def _extract_contestation_invoice_items(
    payload: Any,
    *,
    section_name: str = "",
) -> list[dict[str, Any]]:
    found: list[dict[str, Any]] = []
    if isinstance(payload, dict):
        candidate_name = str(
            payload.get("desc")
            or payload.get("name")
            or payload.get("service_name")
            or payload.get("item_name")
            or payload.get("itemName")
            or payload.get("servico")
            or ""
        ).strip()
        candidate_amount = _first_decimal_from_mapping(
            payload,
            "valor_final",
            "valor",
            "price",
            "amount",
            "value",
            "valor_bruto",
            "claimedAmount",
            "validatedAmount",
        )
        if candidate_name and candidate_amount is not None and candidate_amount > 0:
            payload_type = str(payload.get("type") or payload.get("tipo") or "").strip()
            payload_desc = str(payload.get("desc") or "").strip()
            classe = str(payload.get("classe", "")).strip().lower()
            is_vas = (
                _is_vas_section_name(section_name)
                or _is_vas_section_name(payload_type)
                or classe in {"avulso", "estrategico"}
            )
            found.append(
                {
                    "name": candidate_name,
                    "amount": _money(candidate_amount),
                    "is_vas": is_vas,
                    "section": section_name,
                    "source_type": payload_type,
                    "source_desc": payload_desc,
                    "classe": classe,
                    "estrategico": bool(payload.get("estrategico")),
                    "verb": str(payload.get("verb", "")).strip().lower(),
                }
            )
        for key, value in payload.items():
            next_section = section_name
            if isinstance(key, str) and _is_vas_section_name(key):
                next_section = key
            if isinstance(value, (dict, list, tuple)):
                found.extend(
                    _extract_contestation_invoice_items(
                        value,
                        section_name=next_section,
                    )
                )
        return found
    if isinstance(payload, (list, tuple)):
        for item in payload:
            if isinstance(item, (dict, list, tuple)):
                found.extend(
                    _extract_contestation_invoice_items(
                        item,
                        section_name=section_name,
                    )
                )
    return found


def _has_langfuse_credentials() -> bool:
    return bool(
        os.getenv("LANGFUSE_PUBLIC_KEY", "").strip()
        and os.getenv("LANGFUSE_SECRET_KEY", "").strip()
    )


def _start_guardrail_observation(
    *,
    name: str,
    input: dict[str, Any] | None = None,
    metadata: dict[str, Any] | None = None,
) -> Any:
    if not _has_langfuse_credentials():
        return nullcontext(None)
    try:
        from langfuse import get_client

        return get_client().start_as_current_observation(
            name=name,
            as_type="span",
            input=input,
            metadata=metadata,
        )
    except Exception:
        logger.debug(
            "langfuse.contestation_guardrail_start_failed name=%s",
            name,
            exc_info=True,
        )
        return nullcontext(None)


def _summarize_requested_items(items: list[dict[str, Any]]) -> list[dict[str, str]]:
    summary: list[dict[str, str]] = []
    for item in items:
        summary.append(
            {
                "item_name": str(item.get("item_name", "") or "").strip(),
                "claimed_amount": _normalize_number_text(
                    item.get("claimed_amount", "0")
                ),
                "validated_amount": _normalize_number_text(
                    item.get("validated_amount", "0")
                ),
            }
        )
    return summary


def _validation_reason(validation_log: list[dict[str, Any]]) -> str:
    for entry in validation_log:
        reason = entry.get("erro")
        if reason:
            return str(reason).strip()
    return ""


def _emit_contestation_validation_block_span(
    *,
    items: list[dict[str, Any]],
    candidates: list[dict[str, Any]],
    validation_log: list[dict[str, Any]],
    validation_error: str,
) -> None:
    reason = _validation_reason(validation_log)
    approved_count = sum(
        1 for entry in validation_log if entry.get("status") == "aprovado"
    )
    rejected_count = sum(
        1 for entry in validation_log if entry.get("status") == "reprovado"
    )
    try:
        with _start_guardrail_observation(
            name=f"guardrail.{_GUARDRAIL_CODE}.blocked",
            input={
                "items_count": len(items),
                "items": _summarize_requested_items(items),
                "invoice_candidates_count": len(candidates),
            },
            metadata={
                "mechanism": "guardrail_action_validation",
                "code": _GUARDRAIL_CODE,
                "action": _GUARDRAIL_ACTION,
                "reason": reason,
            },
        ) as obs:
            if obs is None:
                return
            obs.update(
                level="WARNING",
                output={
                    "blocked": True,
                    "error": validation_error,
                    "items_validated_count": len(validation_log),
                    "items_approved_count": approved_count,
                    "items_rejected_count": rejected_count,
                    "validation_log": validation_log,
                    "code": _GUARDRAIL_CODE,
                },
            )
    except Exception:
        logger.debug(
            "langfuse.contestation_guardrail_update_failed code=%s",
            _GUARDRAIL_CODE,
            exc_info=True,
        )


def validate_contestation_items(
    items: list[dict[str, Any]],
    invoice_payload: dict[str, Any],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], str | None]:
    candidates = _extract_contestation_invoice_items(invoice_payload)
    validation_log: list[dict[str, Any]] = []

    with _start_guardrail_observation(
        name=f"guardrail.{_GUARDRAIL_CODE}.evaluated",
        input={
            "items_count": len(items),
            "items": _summarize_requested_items(items),
            "invoice_candidates_count": len(candidates),
        },
        metadata={
            "mechanism": "guardrail_action_validation",
            "code": _GUARDRAIL_CODE,
            "action": _GUARDRAIL_ACTION,
        },
    ) as obs:

        def _safe_update(**kwargs: Any) -> None:
            if obs is None:
                return
            try:
                obs.update(**kwargs)
            except Exception:
                logger.debug(
                    "langfuse.contestation_guardrail_update_failed code=%s",
                    _GUARDRAIL_CODE,
                    exc_info=True,
                )

        first_error: str | None = None

        def _record_failure(
            item_log: dict[str, Any],
            erro: str,
            message: str,
        ) -> None:
            nonlocal first_error
            item_log["status"] = "reprovado"
            item_log["erro"] = erro
            validation_log.append(item_log)
            if first_error is None:
                first_error = message

        for item in items:
            claimed = Decimal(_normalize_number_text(item.get("claimed_amount", "0")))
            validated = Decimal(
                _normalize_number_text(item.get("validated_amount", "0"))
            )
            item_name = str(item.get("item_name", "")).strip()
            if not item_name:
                continue
            item_log: dict[str, Any] = {
                "item_name": item_name,
                "item_na_fatura": False,
                "item_confirmado": False,
                "secao_vas": False,
                "valor_item_fatura": "",
                "valor_ajuste_solicitado": _normalize_number_text(
                    format(validated, "f")
                ),
                "valor_ajuste_valido": False,
                "vas_estrategico": False,
                "status": "em_validacao",
            }
            matching_candidates = [
                candidate
                for candidate in candidates
                if _is_same_plan_name(candidate.get("name", ""), item_name)
            ]
            # A mesma cobrança pode aparecer em múltiplas visões da fatura.
            # Prefira a evidência que traz classificação explícita de VAS em vez
            # de aceitar a primeira ocorrência genérica e concluir incorretamente
            # que o item está fora da seção VAS.
            matching_candidates.sort(
                key=lambda candidate: (
                    0 if (
                        str(candidate.get("classe", "")).strip().lower() in {"avulso", "estrategico"}
                        or bool(candidate.get("is_vas"))
                    ) else 1,
                    0 if _normalize_match_text(candidate.get("name", "")) == _normalize_match_text(item_name) else 1,
                )
            )

            # When the same subject appears more than once on the invoice, the
            # requested amount is useful evidence for selecting the correct
            # occurrence. Prefer an exact amount match. For a partial adjustment
            # choose the smallest invoice occurrence that can cover the requested
            # amount. If none can, select the largest occurrence so the generic
            # ``validated > item_amount`` rule below rejects the request.
            matched_candidate = None
            if matching_candidates:
                requested_amount = validated if validated > 0 else claimed
                if requested_amount > 0 and len(matching_candidates) > 1:
                    monetary_candidates = [
                        candidate
                        for candidate in matching_candidates
                        if isinstance(candidate.get("amount"), Decimal)
                        and candidate.get("amount") > 0
                    ]
                    exact_matches = [
                        candidate
                        for candidate in monetary_candidates
                        if _money(candidate["amount"]) == _money(requested_amount)
                    ]
                    if exact_matches:
                        matched_candidate = exact_matches[0]
                    else:
                        sufficient = sorted(
                            (
                                candidate
                                for candidate in monetary_candidates
                                if candidate["amount"] >= requested_amount
                            ),
                            key=lambda candidate: candidate["amount"],
                        )
                        if sufficient:
                            matched_candidate = sufficient[0]
                        elif monetary_candidates:
                            matched_candidate = max(
                                monetary_candidates,
                                key=lambda candidate: candidate["amount"],
                            )
                if matched_candidate is None:
                    matched_candidate = matching_candidates[0]
            if matched_candidate is None:
                _record_failure(
                    item_log,
                    "item_nao_encontrado_na_fatura",
                    f"Item '{item_name}' nao encontrado no json da fatura.",
                )
                continue
            item_log["item_na_fatura"] = True
            item_log["item_confirmado"] = True
            item_log["item_fatura_resolvido"] = str(matched_candidate.get("name", "") or "")
            item_log["secao_fatura"] = str(matched_candidate.get("section", "") or "")
            item_log["tipo_fatura"] = str(matched_candidate.get("source_type", "") or "")

            classe = str(matched_candidate.get("classe", "")).strip().lower()
            is_strategic = (
                classe == "estrategico"
                or bool(matched_candidate.get("estrategico"))
                or _is_strategic_partner_service(item_name)
            )
            is_vas_avulso = classe == "avulso" or (
                not classe
                and not is_strategic
                and bool(matched_candidate.get("is_vas"))
            )
            if not (is_vas_avulso or is_strategic):
                _record_failure(
                    item_log,
                    "item_fora_secao_vas",
                    f"Item '{item_name}' nao e do tipo VAS no json da fatura.",
                )
                continue
            item_log["secao_vas"] = True

            item_amount = matched_candidate.get("amount")
            if not isinstance(item_amount, Decimal) or item_amount <= 0:
                _record_failure(
                    item_log,
                    "valor_item_invalido_na_fatura",
                    f"Nao foi possivel validar o valor do item '{item_name}' na fatura.",
                )
                continue
            item_log["valor_item_fatura"] = _normalize_number_text(
                format(item_amount, "f")
            )

            if is_strategic:
                item_log["vas_estrategico"] = True
                _record_failure(
                    item_log,
                    "vas_estrategico_nao_permitido",
                    f"Item '{item_name}' identificado como VAS estrategico e nao pode ser ajustado.",
                )
                continue

            if claimed <= 0:
                claimed = item_amount
            if validated <= 0:
                validated = claimed
            if validated > item_amount:
                _record_failure(
                    item_log,
                    "valor_ajuste_maior_que_item",
                    f"Valor de ajuste do item '{item_name}' excede o valor cobrado na fatura.",
                )
                continue
            item_log["valor_ajuste_solicitado"] = _normalize_number_text(
                format(validated, "f")
            )
            item_log["valor_ajuste_valido"] = True
            item_log["status"] = "aprovado"
            validation_log.append(item_log)
            item["claimed_amount"] = _normalize_number_text(format(claimed, "f"))
            item["validated_amount"] = _normalize_number_text(format(validated, "f"))

        invoice_total = _extract_invoice_total_geral(invoice_payload)
        if invoice_total is not None and invoice_total > 0:
            total_ajustes = sum(
                (
                    Decimal(
                        _normalize_number_text(entry.get("valor_ajuste_solicitado", "0"))
                    )
                    for entry in validation_log
                    if entry.get("status") == "aprovado"
                ),
                Decimal("0"),
            )
            if total_ajustes > invoice_total:
                total_log: dict[str, Any] = {
                    "item_name": "<total_ajustes>",
                    "status": "reprovado",
                    "erro": "total_ajustes_excede_fatura",
                    "valor_total_ajustes": _normalize_number_text(
                        format(_money(total_ajustes), "f")
                    ),
                    "valor_total_fatura": _normalize_number_text(
                        format(_money(invoice_total), "f")
                    ),
                }
                validation_log.append(total_log)
                if first_error is None:
                    first_error = (
                        "Valor total de ajustes ("
                        f"{total_log['valor_total_ajustes']}) excede o "
                        f"valor total da fatura ({total_log['valor_total_fatura']})."
                    )

        approved_count = sum(
            1 for entry in validation_log if entry.get("status") == "aprovado"
        )
        rejected_count = sum(
            1 for entry in validation_log if entry.get("status") == "reprovado"
        )

        if first_error is not None:
            _emit_contestation_validation_block_span(
                items=items,
                candidates=candidates,
                validation_log=validation_log,
                validation_error=first_error,
            )
            _safe_update(
                level="WARNING",
                output={
                    "approved": False,
                    "items_count": len(items),
                    "items_validated_count": len(validation_log),
                    "items_approved_count": approved_count,
                    "items_rejected_count": rejected_count,
                    "validation_log": validation_log,
                    "error": first_error,
                    "reason": _validation_reason(validation_log),
                },
            )
            return items, validation_log, first_error

        _safe_update(
            output={
                "approved": True,
                "items_count": len(items),
                "items_validated_count": len(validation_log),
                "items_approved_count": approved_count,
                "items_rejected_count": rejected_count,
                "validation_log": validation_log,
            },
        )
        return items, validation_log, None
