from __future__ import annotations

import asyncio
import json
import os
import time
from dataclasses import dataclass
from typing import Any

from agent_framework.cache.cache import Cache

from .client import TimApiClient


@dataclass(slots=True)
class InvoiceContext:
    msisdn: str
    invoice_id: str = ""
    complete_invoices: dict[str, Any] | None = None
    billing_analysis: dict[str, Any] | None = None
    invoice_detail: Any = None
    invoice_amount: str = ""
    invoice_amount_open: str = ""
    invoice_period: str = ""
    invoice_emissao: str = ""
    customer_id: str = ""
    error: str | None = None
    cache_hit: bool = False
    business_events: list[dict[str, Any]] | None = None
    errors: dict[str, str] | None = None
    metadata: dict[str, Any] | None = None

    def as_dict(self) -> dict[str, Any]:
        return {
            "msisdn": self.msisdn,
            "invoice_id": self.invoice_id,
            "complete_invoices_payload": self.complete_invoices,
            "billing_analysis": self.billing_analysis,
            "invoice_detail": self.invoice_detail,
            "invoice_amount": self.invoice_amount,
            "invoice_amount_open": self.invoice_amount_open,
            "invoice_period": self.invoice_period,
            "invoice_emissao": self.invoice_emissao,
            "customer_id": self.customer_id,
            "invoice_context_error": self.error,
            "invoice_context_cache_hit": self.cache_hit,
            "invoice_context_business_events": list(self.business_events or []),
            "invoice_context_errors": dict(self.errors or {}),
            "invoice_context_metadata": dict(self.metadata or {}),
        }


def extract_invoice_summary_context(detail: Any) -> dict[str, str]:
    """Extrai o resumo semântico da fatura detalhada.

    Mantém a paridade com o prefetch do Contas original: ``bill_pdf`` devolve
    um envelope com ``parsed_content`` e o parser coloca o total em
    ``total_geral``. Workflows e agentes não precisam conhecer a estrutura do
    PDF para consumir ``invoice_amount``/``invoice_amount_open``.
    """
    parsed = detail
    if isinstance(detail, dict) and isinstance(detail.get("parsed_content"), dict):
        parsed = detail["parsed_content"]
    if not isinstance(parsed, dict):
        return {}

    resumo = parsed.get("Fatura Resumo")
    if not isinstance(resumo, list):
        resumo = []

    invoice_period = ""
    invoice_emissao = ""
    invoice_amount = ""
    parsed_total = parsed.get("total_geral")
    if parsed_total is not None and str(parsed_total).strip():
        invoice_amount = str(parsed_total).strip()

    for item in resumo:
        if not isinstance(item, dict):
            continue
        desc = str(item.get("desc", "") or "").strip().casefold()
        if desc in {"período", "periodo"}:
            invoice_period = str(item.get("period", "") or "").strip()
        elif desc in {"emissão", "emissao"}:
            invoice_emissao = str(item.get("emissao", "") or "").strip()
        if not invoice_amount and bool(item.get("is_total")) and str(item.get("value", "")).strip():
            invoice_amount = str(item.get("value", "")).strip()
        if not invoice_amount and "total" in desc and str(item.get("value", "")).strip():
            invoice_amount = str(item.get("value", "")).strip()

    context: dict[str, str] = {}
    if invoice_period:
        context["invoice_period"] = invoice_period
    if invoice_emissao:
        context["invoice_emissao"] = invoice_emissao
    if invoice_amount:
        context["invoice_amount"] = invoice_amount
        context["invoice_amount_open"] = invoice_amount
    return context


class InvoiceContextService:
    """Session-scoped invoice prefetch backed by the framework cache.

    This replaces the legacy InvoiceContextProvider without owning session,
    thread, LangGraph or LLM infrastructure. The cache implementation and TTL
    belong to ``agent_framework``; this class only knows which TIM evidence is
    useful to the Contas domain.
    """

    def __init__(self, client: TimApiClient, cache: Cache, *, ttl_seconds: int | None = None) -> None:
        self.client = client
        self.cache = cache
        self.ttl_seconds = ttl_seconds or int(os.getenv("TIM_INVOICE_CONTEXT_TTL_SECONDS", "1800"))
        self._inflight: dict[str, asyncio.Task[InvoiceContext]] = {}
        self._inflight_lock = asyncio.Lock()

    @staticmethod
    def _key(*, session_id: str, msisdn: str, invoice_id: str) -> str | None:
        # No global cache without a session: prevents cross-customer leakage.
        sid = str(session_id or "").strip()
        if not sid:
            return None
        return f"contas:invoice-context:{sid}:{msisdn}:{invoice_id or 'latest'}"

    @staticmethod
    def _extract_identity(complete: Any, requested_invoice_id: str = "") -> tuple[str, str]:
        if not isinstance(complete, dict):
            return "", requested_invoice_id
        billing = complete.get("billingProfile") or complete.get("billing_profile") or {}
        customer = billing.get("customer") if isinstance(billing, dict) else {}
        customer_id = str((customer or {}).get("customerId") or (customer or {}).get("id") or "") if isinstance(customer, dict) else ""
        items = complete.get("paymentItems") or complete.get("payment_items") or []
        invoice_id = requested_invoice_id
        if not invoice_id and isinstance(items, list):
            first = next((x for x in items if isinstance(x, dict)), {})
            invoice_id = str(first.get("invoiceId") or first.get("invoiceNumber") or "") if isinstance(first, dict) else ""
        return customer_id, invoice_id

    @staticmethod
    def _event(code: str, *, msisdn: str, invoice_id: str, session_id: str, message_id: str = "", api_status_code: int = 0, error: str = "", channel_id: str = "URA", ura_call_id: str = "") -> dict[str, Any]:
        payload = {
            "tag": code,
            "agentId": os.getenv("TIM_AGENT_ID", "contas"),
            "gsm": msisdn,
            "sessionId": session_id,
            "messageId": message_id,
            "channelId": channel_id or "URA",
            "uraCallId": ura_call_id or "",
            "agentSpecificData": json.dumps({"billingId": invoice_id}, ensure_ascii=False) if invoice_id else "",
            "billingId": invoice_id or "",
            "apiStatusCode": int(api_status_code or 0),
        }
        if error:
            payload["error"] = error
        return {"code": code, "payload": payload, "component": "invoice_context_prefetch"}

    async def _result_event_once(self, code: str, *, session_id: str, msisdn: str, invoice_id: str, message_id: str, api_status_code: int = 0, error: str = "") -> list[dict[str, Any]]:
        if not session_id:
            return [self._event(code, msisdn=msisdn, invoice_id=invoice_id, session_id=session_id, message_id=message_id, api_status_code=api_status_code, error=error)]
        marker = f"contas:invoice-context:event-result:{code}:{session_id}:{msisdn}:{invoice_id or 'latest'}"
        if await self.cache.get(marker):
            return []
        await self.cache.set(marker, True, ttl_seconds=self.ttl_seconds)
        return [self._event(code, msisdn=msisdn, invoice_id=invoice_id, session_id=session_id, message_id=message_id, api_status_code=api_status_code, error=error)]

    async def _started_event_once(self, *, session_id: str, msisdn: str, invoice_id: str, message_id: str) -> list[dict[str, Any]]:
        if not session_id:
            return [self._event("CVN.002", msisdn=msisdn, invoice_id=invoice_id, session_id=session_id, message_id=message_id)]
        marker = f"contas:invoice-context:event-start:{session_id}:{msisdn}:{invoice_id or 'latest'}"
        if await self.cache.get(marker):
            return []
        await self.cache.set(marker, True, ttl_seconds=self.ttl_seconds)
        return [self._event("CVN.002", msisdn=msisdn, invoice_id=invoice_id, session_id=session_id, message_id=message_id)]

    @staticmethod
    async def _timed_to_thread(name: str, fn: Any, *args: Any, **kwargs: Any) -> tuple[Any, str, float]:
        started = time.perf_counter()
        try:
            value = await asyncio.to_thread(fn, *args, **kwargs)
            return value, "", round((time.perf_counter() - started) * 1000, 3)
        except Exception as exc:
            return None, f"{type(exc).__name__}: {exc}", round((time.perf_counter() - started) * 1000, 3)

    async def _fetch(
        self, *, session_id: str, msisdn: str, invoice_id: str, include_detail: bool, message_id: str
    ) -> InvoiceContext:
        events = await self._started_event_once(
            session_id=session_id, msisdn=msisdn, invoice_id=invoice_id, message_id=message_id
        )
        fetch_started = time.perf_counter()
        complete_result, billing_result = await asyncio.gather(
            self._timed_to_thread("complete_invoices", self.client.consultar_faturas, msisdn),
            self._timed_to_thread("billing_analysis", self.client.billing_analysis, msisdn, invoice_id=invoice_id),
        )
        complete, complete_error, complete_ms = complete_result
        billing, billing_error, billing_ms = billing_result
        errors: dict[str, str] = {}
        if complete_error:
            errors["complete_invoices"] = complete_error
        if billing_error:
            errors["billing_analysis"] = billing_error
        error_parts = [f"{k}: {v}" for k, v in errors.items()]
        billing_status = 0

        customer_id, resolved_invoice_id = self._extract_identity(complete, invoice_id)
        event_invoice_id = resolved_invoice_id or invoice_id
        if billing is None:
            events.extend(await self._result_event_once("CVN.007", msisdn=msisdn, invoice_id=event_invoice_id, session_id=session_id, message_id=message_id, api_status_code=billing_status, error=error_parts[-1] if error_parts else "billing_analysis failed"))
        else:
            events.extend(await self._result_event_once("CVN.006", msisdn=msisdn, invoice_id=event_invoice_id, session_id=session_id, message_id=message_id))

        detail = None
        detail_ms = 0.0
        summary: dict[str, str] = {}
        if include_detail and resolved_invoice_id:
            detail, detail_error, detail_ms = await self._timed_to_thread(
                "invoice_detail", self.client.bill_pdf, msisdn, resolved_invoice_id, customer_id,
                include_danfe=True, output="json"
            )
            if detail_error:
                errors["invoice_detail"] = detail_error
                error_parts.append(f"invoice_detail: {detail_error}")
            else:
                summary = extract_invoice_summary_context(detail)

        metadata = {
            "cache_hit": False,
            "fetch_elapsed_ms": round((time.perf_counter() - fetch_started) * 1000, 3),
            "task_timings": {
                "complete_invoices": complete_ms,
                "billing_analysis": billing_ms,
                **({"invoice_detail": detail_ms} if include_detail else {}),
            },
        }
        return InvoiceContext(
            msisdn=msisdn, invoice_id=resolved_invoice_id,
            complete_invoices=complete if isinstance(complete, dict) else None,
            billing_analysis=billing if isinstance(billing, dict) else None,
            invoice_detail=detail,
            invoice_amount=summary.get("invoice_amount", ""),
            invoice_amount_open=summary.get("invoice_amount_open", ""),
            invoice_period=summary.get("invoice_period", ""),
            invoice_emissao=summary.get("invoice_emissao", ""),
            customer_id=customer_id,
            error="; ".join(error_parts) or None, cache_hit=False, business_events=events, errors=errors, metadata=metadata,
        )

    async def get(
        self, *, session_id: str, msisdn: str, invoice_id: str = "",
        use_cache: bool = True, include_detail: bool = False, message_id: str = "",
    ) -> InvoiceContext:
        key = self._key(session_id=session_id, msisdn=msisdn, invoice_id=invoice_id)
        if use_cache and key:
            cached = await self.cache.get(key)
            if isinstance(cached, dict) and (not include_detail or cached.get("invoice_detail") is not None):
                cached = dict(cached)
                cached["cache_hit"] = True
                cached["business_events"] = []  # cache hit must not duplicate business effects
                metadata = dict(cached.get("metadata") or {})
                metadata["cache_hit"] = True
                fetched_at = float(cached.get("_fetched_at") or time.time())
                metadata["cache_age_ms"] = max(0.0, round((time.time() - fetched_at) * 1000, 3))
                cached["metadata"] = metadata
                return InvoiceContext(**{k: cached.get(k) for k in InvoiceContext.__dataclass_fields__})

        task_key = (f"{key}:detail={int(include_detail)}" if key else f"nocache:{session_id}:{msisdn}:{invoice_id}:{include_detail}")
        if use_cache and key:
            async with self._inflight_lock:
                task = self._inflight.get(task_key)
                if task is None:
                    task = asyncio.create_task(self._fetch(session_id=session_id, msisdn=msisdn, invoice_id=invoice_id, include_detail=include_detail, message_id=message_id))
                    self._inflight[task_key] = task
            try:
                ctx = await task
            finally:
                async with self._inflight_lock:
                    if self._inflight.get(task_key) is task and task.done():
                        self._inflight.pop(task_key, None)
        else:
            ctx = await self._fetch(session_id=session_id, msisdn=msisdn, invoice_id=invoice_id, include_detail=include_detail, message_id=message_id)

        if key:
            await self.cache.set(key, {
                "msisdn": ctx.msisdn, "invoice_id": ctx.invoice_id,
                "complete_invoices": ctx.complete_invoices, "billing_analysis": ctx.billing_analysis,
                "invoice_detail": ctx.invoice_detail,
                "invoice_amount": ctx.invoice_amount, "invoice_amount_open": ctx.invoice_amount_open,
                "invoice_period": ctx.invoice_period, "invoice_emissao": ctx.invoice_emissao,
                "customer_id": ctx.customer_id,
                "error": ctx.error, "cache_hit": False,
                "errors": dict(ctx.errors or {}), "metadata": dict(ctx.metadata or {}),
                "_fetched_at": time.time(),
            }, ttl_seconds=self.ttl_seconds)
        return ctx

