"""把商业门禁和追加式计量接到真实 Agent 工具调用生命周期。"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime
from decimal import Decimal

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.logging import get_logger
from app.models.agent_run import AgentRun, AgentToolCall
from app.models.commercial_runtime import CommercialRuntimeReservation
from app.services.commercial_access_policy import (
    CommercialConfigurationError,
    CommercialConflictError,
)
from app.services.commercial_runtime_metering import (
    CommercialRuntimeMeteringService,
    RuntimeMeteringResult,
    RuntimeQuotaPreflight,
    SecurityDecision,
)
from app.services.commercial_runtime_policy import (
    preflight_quantity,
    tenant_from_agent_route,
    tool_call_business_occurred,
    tool_call_metering_state,
)
from app.services.commercial_runtime_registry import (
    ConfiguredRuntimeMeter as _ConfiguredMeter,
)
from app.services.commercial_runtime_registry import matching_runtime_meters
from app.services.commercial_runtime_reservations import (
    CommercialRuntimeReservationService,
)

logger = get_logger("app.services.commercial_runtime_bridge")


@dataclass(frozen=True)
class CommercialRuntimeGate:
    enforced: bool
    allowed: bool
    reason_code: str
    reason: str
    tenant_id: str | None
    preflight: RuntimeQuotaPreflight | None = None


@dataclass(frozen=True)
class CommercialRuntimePermit:
    gate: CommercialRuntimeGate
    tool_call_id: str
    reservation_id: str | None = None


class CommercialRuntimeBridge:
    """未配置时兼容放行；显式配置后执行严格门禁并追加真实事实。"""

    def __init__(self, db: Session) -> None:
        self.db = db

    def preflight(
        self,
        run_id: str,
        *,
        tool_type: str,
        tool_name: str,
        requested_quantity: Decimal | int | None = None,
        security_decision: SecurityDecision = "allow",
    ) -> CommercialRuntimeGate:
        run = self.db.scalar(select(AgentRun).where(AgentRun.run_id == str(run_id or "").strip()))
        if run is None:
            return CommercialRuntimeGate(
                enforced=True,
                allowed=False,
                reason_code="run_not_found",
                reason="Agent 运行记录不存在，不能执行商业权益检查。",
                tenant_id=None,
            )
        tenant_id = tenant_from_agent_route(run.route_json)
        if tenant_id is None:
            return CommercialRuntimeGate(
                enforced=False,
                allowed=True,
                reason_code="legacy_run_without_tenant",
                reason="运行记录没有可信租户归属，按未启用商业管控的兼容路径执行。",
                tenant_id=None,
            )
        meters = self._matching_meters(tenant_id, tool_type, tool_name)
        if not meters:
            return CommercialRuntimeGate(
                enforced=False,
                allowed=True,
                reason_code="runtime_meter_not_configured",
                reason="该工具尚未显式启用商业运行计量，兼容执行且不生成商业事实。",
                tenant_id=tenant_id,
            )
        try:
            reserved_quantity = (
                requested_quantity
                if requested_quantity is not None
                else preflight_quantity([item.config for item in meters])
            )
            result = CommercialRuntimeMeteringService(self.db).preflight_run_tool(
                run.run_id,
                tool_type=tool_type,
                tool_name=tool_name,
                requested_quantity=reserved_quantity,
                security_decision=security_decision,
            )
        except CommercialConfigurationError as error:
            return CommercialRuntimeGate(
                enforced=True,
                allowed=False,
                reason_code="configuration_error",
                reason=str(error),
                tenant_id=tenant_id,
            )
        return CommercialRuntimeGate(
            enforced=True,
            allowed=result.allowed,
            reason_code=result.reason_code,
            reason=result.reason,
            tenant_id=tenant_id,
            preflight=result,
        )

    def reserve_tool(
        self,
        run_id: str,
        *,
        tool_call_id: str,
        tool_type: str,
        tool_name: str,
        requested_quantity: Decimal | int | None = None,
        hard_max_confirmed: bool = False,
        security_decision: SecurityDecision = "allow",
    ) -> CommercialRuntimePermit:
        existing = CommercialRuntimeReservationService(self.db).by_tool_call(tool_call_id)
        if existing is not None:
            return self._replay_reservation(
                existing,
                run_id=run_id,
                tool_call_id=tool_call_id,
                tool_type=tool_type,
                tool_name=tool_name,
                requested_quantity=requested_quantity,
                hard_max_confirmed=hard_max_confirmed,
                security_decision=security_decision,
            )
        gate = self.preflight(
            run_id,
            tool_type=tool_type,
            tool_name=tool_name,
            requested_quantity=requested_quantity,
            security_decision=security_decision,
        )
        if not gate.enforced or not gate.allowed:
            self.db.rollback()
            return CommercialRuntimePermit(gate=gate, tool_call_id=tool_call_id)
        preflight = gate.preflight
        if preflight is None or preflight.entitlement_id is None:
            self.db.rollback()
            return CommercialRuntimePermit(
                gate=self._denied_gate(gate, "configuration_error", "商业预检缺少权益身份。"),
                tool_call_id=tool_call_id,
            )
        if preflight.quantity_basis != "call" and not hard_max_confirmed:
            self.db.rollback()
            return CommercialRuntimePermit(
                gate=self._denied_gate(
                    gate,
                    "hard_max_required",
                    "变量用量工具必须提供执行器可强制的最大数量后才能预占。",
                ),
                tool_call_id=tool_call_id,
            )
        meter = next(
            (
                item
                for item in self._matching_meters(gate.tenant_id or "", tool_type, tool_name)
                if item.entitlement.id == preflight.entitlement_id
            ),
            None,
        )
        if meter is None:
            self.db.rollback()
            return CommercialRuntimePermit(
                gate=self._denied_gate(
                    gate,
                    "configuration_changed",
                    "商业计量配置在预检后发生变化，请重试。",
                ),
                tool_call_id=tool_call_id,
            )
        try:
            reservation, _ = CommercialRuntimeReservationService(self.db).reserve(
                tenant_id=gate.tenant_id or "",
                entitlement_id=preflight.entitlement_id,
                run_id=run_id,
                tool_call_id=tool_call_id,
                tool_type=tool_type,
                tool_name=tool_name,
                quantity_basis=preflight.quantity_basis or "",
                reserved_quantity=preflight.requested_quantity or Decimal("0"),
                meter_config=meter.config,
            )
            self.db.commit()
        except Exception as error:
            self.db.rollback()
            reason = (
                str(error)
                if isinstance(
                    error,
                    (
                        CommercialConfigurationError,
                        CommercialConflictError,
                        LookupError,
                        ValueError,
                    ),
                )
                else "额度预占失败。"
            )
            return CommercialRuntimePermit(
                gate=self._denied_gate(gate, "reservation_denied", reason),
                tool_call_id=tool_call_id,
            )
        return CommercialRuntimePermit(
            gate=gate,
            tool_call_id=tool_call_id,
            reservation_id=reservation.id,
        )

    def _replay_reservation(
        self,
        existing: CommercialRuntimeReservation,
        *,
        run_id: str,
        tool_call_id: str,
        tool_type: str,
        tool_name: str,
        requested_quantity: Decimal | int | None,
        hard_max_confirmed: bool,
        security_decision: SecurityDecision,
    ) -> CommercialRuntimePermit:
        run = self.db.scalar(select(AgentRun).where(AgentRun.run_id == str(run_id).strip()))
        tenant_id = tenant_from_agent_route(run.route_json) if run is not None else None
        gate = CommercialRuntimeGate(
            enforced=True,
            allowed=True,
            reason_code="reservation_replayed",
            reason="相同工具调用已完成额度预占，本次复用原子占位。",
            tenant_id=tenant_id,
            preflight=RuntimeQuotaPreflight(
                allowed=True,
                reason_code="reservation_replayed",
                reason="相同工具调用已完成额度预占。",
                run_id=str(run_id),
                tenant_id=tenant_id,
                entitlement_id=existing.entitlement_id,
                quantity_basis=existing.quantity_basis,
                requested_quantity=Decimal(existing.reserved_quantity),
                commercially_allowed=True,
                security_decision=security_decision,
            ),
        )
        identity_matches = (
            run is not None
            and tenant_id == existing.tenant_id
            and str(run_id).strip() == existing.run_id
            and str(tool_call_id).strip() == existing.tool_call_id
            and str(tool_type).strip() == existing.tool_type
            and str(tool_name).strip() == existing.tool_name
        )
        if not identity_matches:
            self.db.rollback()
            return CommercialRuntimePermit(
                gate=self._denied_gate(
                    gate,
                    "reservation_identity_mismatch",
                    "工具调用编号已属于不同运行身份，拒绝复用额度预占。",
                ),
                tool_call_id=tool_call_id,
            )
        expires_at = existing.expires_at
        expires_at = expires_at.replace(tzinfo=UTC) if expires_at.tzinfo is None else expires_at
        if existing.status != "reserved" or expires_at <= datetime.now(UTC):
            self.db.rollback()
            return CommercialRuntimePermit(
                gate=self._denied_gate(
                    gate,
                    "reservation_not_reservable",
                    "原额度预占已终结或过期，不能再次授权真实工具执行。",
                ),
                tool_call_id=tool_call_id,
            )
        if security_decision != "allow":
            self.db.rollback()
            return CommercialRuntimePermit(
                gate=self._denied_gate(
                    gate,
                    "security_denied",
                    "当前安全门禁不允许复用原额度预占。",
                ),
                tool_call_id=tool_call_id,
            )
        if existing.quantity_basis != "call" and not hard_max_confirmed:
            self.db.rollback()
            return CommercialRuntimePermit(
                gate=self._denied_gate(
                    gate,
                    "hard_max_required",
                    "变量用量工具重试时仍须确认执行器可强制的最大数量。",
                ),
                tool_call_id=tool_call_id,
            )
        quantity = (
            requested_quantity
            if requested_quantity is not None
            else preflight_quantity([existing.meter_config_json])
        )
        try:
            replayed, _ = CommercialRuntimeReservationService(self.db).reserve(
                tenant_id=existing.tenant_id,
                entitlement_id=existing.entitlement_id,
                run_id=run_id,
                tool_call_id=tool_call_id,
                tool_type=tool_type,
                tool_name=tool_name,
                quantity_basis=existing.quantity_basis,
                reserved_quantity=Decimal(str(quantity)),
                meter_config=existing.meter_config_json,
            )
            self.db.commit()
        except (
            CommercialConfigurationError,
            CommercialConflictError,
            LookupError,
            ValueError,
        ) as error:
            self.db.rollback()
            return CommercialRuntimePermit(
                gate=self._denied_gate(gate, "reservation_replay_conflict", str(error)),
                tool_call_id=tool_call_id,
            )
        return CommercialRuntimePermit(
            gate=gate,
            tool_call_id=tool_call_id,
            reservation_id=replayed.id,
        )

    def sync_tool_call(self, tool_call_id: str) -> RuntimeMeteringResult:
        pair = None
        run_id = ""
        tenant_id = None
        try:
            pair = self.db.execute(
                select(AgentToolCall, AgentRun)
                .join(AgentRun, AgentRun.run_id == AgentToolCall.run_id)
                .where(AgentToolCall.id == str(tool_call_id or "").strip())
            ).one_or_none()
            if pair is not None:
                tool_call, run = pair
                tenant_id = tenant_from_agent_route(run.route_json)
                run_id = run.run_id
                reservation = CommercialRuntimeReservationService(self.db).by_tool_call(
                    tool_call.id
                )
                if reservation is not None:
                    result = self._sync_reserved(tool_call, run, reservation)
                    if result.status in {"created", "replayed"} or result.usage_event_id:
                        self.db.commit()
                    elif reservation.status in {"released", "expired"}:
                        self.db.commit()
                    else:
                        self.db.rollback()
                    self._log_result(result)
                    return result
                meters = self._matching_meters(
                    tenant_id or "",
                    tool_call.tool_type,
                    tool_call.tool_name,
                )
                if not meters:
                    result = RuntimeMeteringResult(
                        status="skipped",
                        reason_code=(
                            "legacy_run_without_tenant"
                            if tenant_id is None
                            else "runtime_meter_not_configured"
                        ),
                        reason=(
                            "运行记录没有可信租户归属，按兼容路径跳过商业事实。"
                            if tenant_id is None
                            else "该工具尚未显式启用商业运行计量。"
                        ),
                        tool_call_id=tool_call.id,
                        run_id=run.run_id,
                        tenant_id=tenant_id,
                        business_call_occurred=tool_call_business_occurred(tool_call.status),
                        requires_reconciliation=False,
                    )
                    self.db.rollback()
                    return result
                if tool_call_metering_state(tool_call.status) != "billable":
                    self.db.rollback()
                    return self._non_billable_result(tool_call, run, tenant_id)
                result = self._record_missing_reservation_backlog(
                    tool_call,
                    run,
                    tenant_id,
                    meters,
                )
                self.db.commit()
                self._log_result(result)
                return result
            result = CommercialRuntimeMeteringService(self.db).sync_tool_call(tool_call_id)
            self.db.rollback()
        except Exception:  # 工具调用已提交，计量失败必须转成可补偿结果。
            self.db.rollback()
            result = RuntimeMeteringResult(
                status="error",
                reason_code="metering_bridge_failed_after_business_call",
                reason="运行时计量桥接失败，真实工具调用已保留并等待幂等补偿。",
                tool_call_id=str(tool_call_id or ""),
                run_id=run_id,
                tenant_id=tenant_id,
                business_call_occurred=True,
                requires_reconciliation=True,
            )
            logger.exception(
                "Commercial runtime metering bridge failed tool_call_id=%s",
                tool_call_id,
            )
        self._log_result(result)
        return result

    def _sync_reserved(
        self,
        tool_call: AgentToolCall,
        run: AgentRun,
        reservation: CommercialRuntimeReservation,
    ) -> RuntimeMeteringResult:
        state = tool_call_metering_state(tool_call.status)
        reservations = CommercialRuntimeReservationService(self.db)
        tenant_id = tenant_from_agent_route(run.route_json)
        if state == "collecting":
            return RuntimeMeteringResult(
                status="skipped",
                reason_code="tool_call_not_terminal",
                reason="工具调用尚未完成，额度预占继续保留。",
                tool_call_id=tool_call.id,
                run_id=run.run_id,
                tenant_id=tenant_id,
                entitlement_id=reservation.entitlement_id,
                quantity_basis=reservation.quantity_basis,
                collecting=True,
                business_call_occurred=False,
                requires_reconciliation=True,
            )
        if state != "billable":
            if reservation.status == "reserved":
                reservations.release(
                    reservation.id,
                    reason_code=f"tool_{str(tool_call.status or 'unknown').lower()}",
                )
            return self._non_billable_result(tool_call, run, tenant_id)
        if reservation.status in {"released", "expired", "reconciliation_required"}:
            return RuntimeMeteringResult(
                status="error",
                reason_code="reservation_not_settleable",
                reason=f"额度预占状态 {reservation.status} 不能自动结算。",
                tool_call_id=tool_call.id,
                run_id=run.run_id,
                tenant_id=tenant_id,
                entitlement_id=reservation.entitlement_id,
                quantity_basis=reservation.quantity_basis,
                business_call_occurred=True,
                requires_reconciliation=True,
            )
        if reservation.status == "reserved":
            actual = CommercialRuntimeMeteringService.measured_quantity(
                tool_call,
                reservation.quantity_basis,  # type: ignore[arg-type]
            )
            reservation = reservations.mark_committed(reservation.id, actual)
        result = CommercialRuntimeMeteringService(self.db).sync_reserved_tool_call(
            tool_call.id,
            reservation,
        )
        if result.requires_reconciliation and result.usage_event_id:
            reservations.require_committed_reconciliation(
                reservation.id,
                reason_code=result.reason_code,
            )
        elif (
            not result.requires_reconciliation
            and reservation.status == "committed_reconciliation_required"
        ):
            reservations.mark_reconciled(reservation.id)
        return result

    def _record_missing_reservation_backlog(
        self,
        tool_call: AgentToolCall,
        run: AgentRun,
        tenant_id: str | None,
        meters: list[_ConfiguredMeter],
    ) -> RuntimeMeteringResult:
        gate = self.preflight(
            run.run_id,
            tool_type=tool_call.tool_type,
            tool_name=tool_call.tool_name,
        )
        preflight = gate.preflight
        if tenant_id is None:
            raise CommercialConflictError("真实工具调用缺少可持久化的商业补偿归属。")
        meter = (
            next(
                (
                    item
                    for item in meters
                    if preflight is not None
                    and item.entitlement.id == preflight.entitlement_id
                ),
                None,
            )
            if preflight is not None and preflight.entitlement_id is not None
            else (meters[0] if len(meters) == 1 else None)
        )
        if meter is None:
            raise CommercialConflictError("真实工具调用对应的商业计量配置缺失或存在歧义。")
        quantity_basis = (
            preflight.quantity_basis
            if preflight is not None and preflight.quantity_basis
            else str(meter.config.get("quantity_basis") or "").strip()
        )
        try:
            actual = CommercialRuntimeMeteringService.measured_quantity(
                tool_call,
                quantity_basis,  # type: ignore[arg-type]
            )
        except Exception:
            actual = None
        requested = preflight.requested_quantity if preflight is not None else None
        reserved = actual or requested or preflight_quantity([meter.config])
        backlog, _ = CommercialRuntimeReservationService(
            self.db
        ).record_reconciliation_backlog(
            tenant_id=tenant_id,
            entitlement_id=meter.entitlement.id,
            run_id=run.run_id,
            tool_call_id=tool_call.id,
            tool_type=tool_call.tool_type,
            tool_name=tool_call.tool_name,
            quantity_basis=quantity_basis,
            reserved_quantity=reserved,
            actual_quantity=actual,
            meter_config=meter.config,
            resolution_code="missing_pre_execution_reservation",
            as_of=tool_call.created_at,
        )
        return RuntimeMeteringResult(
            status="error",
            reason_code="missing_pre_execution_reservation",
            reason="真实工具调用没有执行前预占，已持久化补偿并冻结对应额度。",
            tool_call_id=tool_call.id,
            run_id=run.run_id,
            tenant_id=tenant_id,
            entitlement_id=backlog.entitlement_id,
            quantity_basis=backlog.quantity_basis,
            quantity=actual,
            business_call_occurred=True,
            requires_reconciliation=True,
        )

    def _matching_meters(
        self,
        tenant_id: str,
        tool_type: str,
        tool_name: str,
    ) -> list[_ConfiguredMeter]:
        return matching_runtime_meters(
            self.db,
            tenant_id=tenant_id,
            tool_type=tool_type,
            tool_name=tool_name,
        )

    @staticmethod
    def _denied_gate(
        gate: CommercialRuntimeGate,
        reason_code: str,
        reason: str,
    ) -> CommercialRuntimeGate:
        return CommercialRuntimeGate(
            enforced=True,
            allowed=False,
            reason_code=reason_code,
            reason=reason,
            tenant_id=gate.tenant_id,
            preflight=gate.preflight,
        )

    @staticmethod
    def _non_billable_result(
        tool_call: AgentToolCall,
        run: AgentRun,
        tenant_id: str | None,
    ) -> RuntimeMeteringResult:
        return RuntimeMeteringResult(
            status="skipped",
            reason_code="tool_call_not_billable",
            reason="只有成功完成的真实工具调用才进入商业用量与成本账本。",
            tool_call_id=tool_call.id,
            run_id=run.run_id,
            tenant_id=tenant_id,
            business_call_occurred=tool_call_business_occurred(tool_call.status),
            requires_reconciliation=False,
        )

    @staticmethod
    def _log_result(result: RuntimeMeteringResult) -> None:
        if result.requires_reconciliation:
            logger.warning(
                "Commercial runtime metering requires reconciliation "
                "tool_call_id=%s reason_code=%s retry=sync_tool_call",
                result.tool_call_id,
                result.reason_code,
            )
        elif result.status in {"created", "replayed"}:
            logger.info(
                "Commercial runtime metering persisted tool_call_id=%s status=%s",
                result.tool_call_id,
                result.status,
            )
