from __future__ import annotations

from datetime import datetime
from decimal import Decimal, InvalidOperation
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

FinancialEventType = Literal[
    "payment_settled",
    "payment_failed",
    "erp_posted",
    "erp_posting_failed",
    "payment_refunded",
    "payment_reversed",
]

FinancialConnectorSimulationScenario = Literal[
    "success",
    "failure",
    "out_of_order",
    "duplicate",
    "conflict",
    "refund",
    "erp_receipt",
]


class FinancialConnectorConfigCreate(BaseModel):
    provider: str = Field(min_length=1, max_length=80)
    environment: Literal["test", "mock", "staging", "production"]
    key_version: str = Field(min_length=1, max_length=40)
    secret_ref: str = Field(min_length=1, max_length=180)
    allowed_event_types: list[FinancialEventType] = Field(min_length=1)
    clock_skew_seconds: int = Field(default=300, ge=30, le=900)
    status: Literal["disabled"] = "disabled"
    request_id: str = Field(min_length=8, max_length=120)
    reason: str = Field(min_length=4, max_length=1000)

    @field_validator(
        "provider",
        "key_version",
        "secret_ref",
        "request_id",
        "reason",
        mode="before",
    )
    @classmethod
    def normalize_text(cls, value: Any) -> str:
        return str(value or "").strip()


class FinancialConnectorConfigRead(BaseModel):
    """刻意不包含 secret_ref 或任何 HMAC 材料。"""

    model_config = ConfigDict(from_attributes=True)

    id: str
    tenant_id: str
    provider: str
    environment: Literal["test", "mock", "staging", "production"]
    key_version: str
    allowed_event_types_json: list[str] = Field(default_factory=list)
    clock_skew_seconds: int
    status: Literal["active", "disabled", "rotating"]
    version: int
    last_success_at: datetime | None = None
    last_error_at: datetime | None = None
    last_error_code: str | None = None
    created_by: str
    created_at: datetime
    updated_at: datetime


class FinancialConnectorConfigLifecycleAction(BaseModel):
    expected_version: int = Field(ge=1)
    request_id: str = Field(min_length=8, max_length=120)
    reason: str = Field(min_length=4, max_length=1000)

    @field_validator("request_id", "reason", mode="before")
    @classmethod
    def normalize_lifecycle_text(cls, value: Any) -> str:
        return str(value or "").strip()


class FinancialConnectorConfigRotateAction(FinancialConnectorConfigLifecycleAction):
    new_key_version: str = Field(min_length=1, max_length=40)
    new_secret_ref: str = Field(min_length=1, max_length=180)

    @field_validator("new_key_version", "new_secret_ref", mode="before")
    @classmethod
    def normalize_rotation_text(cls, value: Any) -> str:
        return str(value or "").strip()


class FinancialConnectorConfigRotationRead(BaseModel):
    previous: FinancialConnectorConfigRead
    replacement: FinancialConnectorConfigRead


class FinancialConnectorConfigEventRead(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: str
    tenant_id: str
    config_id: str
    action: Literal[
        "created",
        "activated",
        "disabled",
        "rotation_started",
        "rotation_replacement_created",
    ]
    actor_id: str
    request_id: str
    reason: str
    expected_version: int | None = None
    before_json: dict[str, Any] = Field(default_factory=dict)
    after_json: dict[str, Any] = Field(default_factory=dict)
    occurred_at: datetime


class FinancialEventEnvelope(BaseModel):
    model_config = ConfigDict(extra="forbid")

    tenant_id: str = Field(min_length=1, max_length=64)
    external_event_id: str = Field(min_length=1, max_length=160)
    event_type: FinancialEventType
    occurred_at: datetime
    correlation_id: str = Field(min_length=1, max_length=64)
    payload: dict[str, Any]

    @field_validator("tenant_id", "external_event_id", "correlation_id", mode="before")
    @classmethod
    def normalize_text(cls, value: Any) -> str:
        return str(value or "").strip()

    @model_validator(mode="after")
    def validate_payload_shape(self) -> FinancialEventEnvelope:
        allowed = {
            "claim_id",
            "claim_reference",
            "amount",
            "currency",
            "external_payment_reference",
            "origin_external_event_id",
            "erp_document_number",
            "accounting_period",
            "failure_code",
        }
        unknown = sorted(str(key) for key in self.payload if key not in allowed)
        if unknown:
            raise ValueError(f"财务事件 payload 包含未允许字段：{', '.join(unknown)}")
        required = {"claim_id", "claim_reference", "amount", "currency"}
        if self.event_type in {"payment_settled", "payment_failed"}:
            required.add("external_payment_reference")
        if self.event_type in {"payment_refunded", "payment_reversed"}:
            required.update({"external_payment_reference", "origin_external_event_id"})
        if self.event_type in {"erp_posted", "erp_posting_failed"}:
            required.add("origin_external_event_id")
        if self.event_type == "erp_posted":
            required.add("erp_document_number")
        missing = sorted(key for key in required if not str(self.payload.get(key) or "").strip())
        if missing:
            raise ValueError(f"财务事件 payload 缺少字段：{', '.join(missing)}")
        currency = str(self.payload.get("currency") or "").strip().upper()
        if len(currency) != 3 or not currency.isalpha():
            raise ValueError("财务事件币种必须是三位字母代码。")
        try:
            amount = Decimal(str(self.payload.get("amount")))
        except (InvalidOperation, TypeError, ValueError) as error:
            raise ValueError("财务事件金额无效。") from error
        if not amount.is_finite() or amount < 0 or amount.as_tuple().exponent < -4:
            raise ValueError("财务事件金额必须是非负且最多四位小数。")
        if self.occurred_at.tzinfo is None:
            raise ValueError("财务事件发生时间必须包含时区。")
        return self


class FinancialEventIngestionRead(BaseModel):
    accepted: bool = True
    replayed: bool = False
    event_id: str
    external_event_id: str
    processing_status: Literal["processed", "exception", "pending"]
    reconciliation_case_id: str | None = None
    reconciliation_status: str | None = None
    claim_status: str | None = None
    verification_level: Literal["simulated", "staging_verified", "production_verified"]
    evidence_classification: Literal["simulated_connector", "staging_connector", "external_cash"]
    projection_scope: Literal[
        "simulation_only",
        "canonical",
        "legacy_nonproduction_effect_unknown",
    ]
    error_code: str | None = None


class FinancialConnectorSimulationCreate(BaseModel):
    claim_id: str = Field(min_length=1, max_length=36)
    scenario: FinancialConnectorSimulationScenario
    request_id: str = Field(min_length=8, max_length=120)

    @field_validator("claim_id", "request_id", mode="before")
    @classmethod
    def normalize_simulation_text(cls, value: Any) -> str:
        return str(value or "").strip()


class FinancialConnectorSimulationStepRead(BaseModel):
    name: str
    event_type: FinancialEventType
    outcome: Literal["accepted", "replayed", "expected_exception", "conflict"]
    event_id: str | None = None
    processing_status: Literal["processed", "exception", "pending"] | None = None
    error_code: str | None = None


class FinancialConnectorSimulationRead(BaseModel):
    tenant_id: str
    config_id: str
    provider: str
    environment: Literal["test", "mock", "staging"]
    scenario: FinancialConnectorSimulationScenario
    request_fingerprint: str
    evidence_classification: Literal["simulated_connector", "staging_connector"]
    projection_scope: Literal["simulation_only"] = "simulation_only"
    core_side_effects_allowed: Literal[False] = False
    steps: list[FinancialConnectorSimulationStepRead] = Field(default_factory=list)


class FinancialConnectorMetricAvailabilityRead(BaseModel):
    status: Literal["available", "unavailable"]
    source: str
    reason: str | None = None


class FinancialConnectorObservabilityItemRead(BaseModel):
    config_id: str
    provider: str
    environment: Literal["test", "mock", "staging", "production"]
    key_version: str
    status: Literal["active", "disabled", "rotating"]
    evidence_classification: Literal[
        "simulated_connector",
        "staging_connector",
        "external_cash",
    ]
    evidence_label: str
    last_success_at: datetime | None = None
    last_error_at: datetime | None = None
    last_error_code: str | None = None
    event_count: int = 0
    processed_event_count: int = 0
    failed_event_count: int = 0
    failure_rate: float = Field(default=0.0, ge=0.0, le=1.0)
    backlog_count: int = 0
    reconciliation_anomaly_count: int = 0
    retry_count: int | None = 0
    auth_failure_count: int = 0
    signature_failure_count: int | None = 0
    payload_conflict_count: int = 0
    latest_replay_at: datetime | None = None
    latest_auth_failure_at: datetime | None = None
    latest_signature_failure_at: datetime | None = None
    latest_payload_conflict_at: datetime | None = None


class FinancialConnectorObservabilitySummaryRead(BaseModel):
    connector_count: int = 0
    active_connector_count: int = 0
    event_count: int = 0
    processed_event_count: int = 0
    failed_event_count: int = 0
    failure_rate: float = Field(default=0.0, ge=0.0, le=1.0)
    backlog_count: int = 0
    reconciliation_anomaly_count: int = 0
    retry_count: int | None = 0
    auth_failure_count: int = 0
    signature_failure_count: int | None = 0
    payload_conflict_count: int = 0
    latest_replay_at: datetime | None = None
    latest_auth_failure_at: datetime | None = None
    latest_signature_failure_at: datetime | None = None
    latest_payload_conflict_at: datetime | None = None


class FinancialConnectorObservabilityRead(BaseModel):
    tenant_id: str
    window_hours: int
    window_started_at: datetime
    as_of: datetime
    generated_at: datetime
    source_revision: str
    summary: FinancialConnectorObservabilitySummaryRead
    retry_metric: FinancialConnectorMetricAvailabilityRead
    auth_failure_metric: FinancialConnectorMetricAvailabilityRead
    signature_failure_metric: FinancialConnectorMetricAvailabilityRead
    payload_conflict_metric: FinancialConnectorMetricAvailabilityRead
    items: list[FinancialConnectorObservabilityItemRead] = Field(default_factory=list)


class FinancialPaymentEvidenceRead(BaseModel):
    claim_id: str
    claim_status: str
    payment_state: Literal["not_paid", "paid"]
    evidence_classification: Literal[
        "none",
        "internal_manual_payment",
        "external_cash",
        "simulated_connector",
        "staging_connector",
    ]
    evidence_label: str
    trust_level: Literal["none", "low", "high"]
    source_type: Literal["none", "manual_confirmation", "external_connector"]
    provider: str | None = None
    verification_level: str | None = None
    external_reference_tail: str | None = None
    recorded_at: datetime | None = None
    disclaimer: str


class PaymentReconciliationEventRead(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: str
    connector_event_id: str
    action: str
    actor_type: str
    actor_id: str
    reason: str | None = None
    correlation_id: str
    occurred_at: datetime


class PaymentReconciliationCaseRead(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: str
    tenant_id: str
    provider: str
    claim_id: str
    expense_case_id: str | None = None
    expected_amount: Decimal
    actual_amount: Decimal
    amount_difference: Decimal
    expected_currency: str
    actual_currency: str
    expected_reference: str
    external_reference_tail: str | None = None
    status: str
    exception_code: str | None = None
    erp_status: str
    erp_document_tail: str | None = None
    assigned_to: str | None = None
    last_connector_event_id: str
    version: int
    created_at: datetime
    updated_at: datetime


class PaymentReconciliationCaseDetailRead(PaymentReconciliationCaseRead):
    timeline: list[PaymentReconciliationEventRead] = Field(default_factory=list)


class PaymentReconciliationListRead(BaseModel):
    items: list[PaymentReconciliationCaseRead] = Field(default_factory=list)
    total: int
    page: int
    page_size: int


class PaymentReconciliationActionCreate(BaseModel):
    expected_version: int = Field(ge=1)
    reason: str = Field(min_length=4, max_length=1000)

    @field_validator("reason", mode="before")
    @classmethod
    def normalize_reason(cls, value: Any) -> str:
        return str(value or "").strip()
