from __future__ import annotations

import hashlib
import uuid
from dataclasses import dataclass
from datetime import UTC, datetime
from decimal import Decimal, InvalidOperation
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.financial_connector import (
    FinancialConnectorEvent,
    PaymentReconciliationCase,
    PaymentReconciliationEvent,
)
from app.schemas.financial_connector import FinancialEventEnvelope
from app.services.expense_cases import ExpenseCaseService
from app.services.financial_connector_actions import FinancialConnectorActionService
from app.services.financial_connector_auth import VerifiedConnectorRequest


@dataclass(slots=True)
class ReconciliationResult:
    case: PaymentReconciliationCase | None
    audit_event: PaymentReconciliationEvent | None
    processing_status: str
    error_code: str | None
    claim_id: str | None
    expense_case_id: str | None
    claim_status: str | None


class PaymentReconciliationService:
    """把已认证事件精确匹配到现有付款状态机；不负责 commit。"""

    def __init__(self, db: Session) -> None:
        self.db = db
        self.actions = FinancialConnectorActionService(db)
        self.expense_cases = ExpenseCaseService(db)

    def process(
        self,
        envelope: FinancialEventEnvelope,
        verified: VerifiedConnectorRequest,
        *,
        connector_event_id: str,
    ) -> ReconciliationResult:
        if envelope.event_type == "payment_settled":
            return self._payment_settled(envelope, verified, connector_event_id)
        if envelope.event_type == "payment_failed":
            return self._payment_failed(envelope, verified, connector_event_id)
        if envelope.event_type in {"erp_posted", "erp_posting_failed"}:
            return self._erp_event(envelope, verified, connector_event_id)
        return self._payment_reversal(envelope, verified, connector_event_id)

    def _payment_settled(
        self,
        envelope: FinancialEventEnvelope,
        verified: VerifiedConnectorRequest,
        connector_event_id: str,
    ) -> ReconciliationResult:
        tenant_id = envelope.tenant_id
        provider = verified.config.provider
        claim = self.actions.lock_claim(
            tenant_id=tenant_id,
            claim_id=str(envelope.payload.get("claim_id") or ""),
        )
        if claim is None:
            return self._without_case("claim_not_found")

        existing = self._lock_case(tenant_id, provider, claim.id)
        mismatch = self.actions.settlement_mismatch(claim, envelope.payload)
        amount = _amount(envelope.payload.get("amount"))
        if amount is None or amount < 0:
            mismatch = "amount_invalid"
            amount = Decimal("0.00")
        expense_case = self.expense_cases.ensure_case_for_claim(claim, tenant_id=tenant_id)
        status = "exception" if mismatch else "matched"
        before = _case_state(existing)
        if mismatch is None:
            self.actions.settle_claim(
                claim,
                tenant_id=tenant_id,
                provider=provider,
                connector_event_id=connector_event_id,
                external_event_id=envelope.external_event_id,
                content_hash=verified.content_hash,
                verification_level=verified.verification_level,
                evidence_classification=verified.evidence_classification,
                external_reference_tail=_tail(envelope.payload.get("external_payment_reference")),
                correlation_id=envelope.correlation_id,
            )
        case = self._build_or_update_case(
            existing,
            tenant_id=tenant_id,
            provider=provider,
            claim=claim,
            expense_case_id=expense_case.id,
            actual_amount=amount,
            actual_currency=str(envelope.payload.get("currency") or "").upper(),
            external_reference=_tail(envelope.payload.get("external_payment_reference")),
            status=status,
            exception_code=mismatch,
            connector_event_id=connector_event_id,
            reset_erp=True,
        )
        action = "exception_created" if mismatch else "auto_matched"
        audit = self._audit_event(
            case,
            connector_event_id=connector_event_id,
            action=action,
            fingerprint=verified.request_fingerprint,
            before=before,
            reason=mismatch,
            correlation_id=envelope.correlation_id,
        )
        return ReconciliationResult(
            case=case,
            audit_event=audit,
            processing_status="exception" if mismatch else "processed",
            error_code=mismatch,
            claim_id=claim.id,
            expense_case_id=expense_case.id,
            claim_status=str(claim.status or ""),
        )

    def _payment_failed(
        self,
        envelope: FinancialEventEnvelope,
        verified: VerifiedConnectorRequest,
        connector_event_id: str,
    ) -> ReconciliationResult:
        tenant_id = envelope.tenant_id
        provider = verified.config.provider
        claim = self.actions.lock_claim(
            tenant_id=tenant_id,
            claim_id=str(envelope.payload.get("claim_id") or ""),
        )
        if claim is None:
            return self._without_case("claim_not_found")
        expense_case = self.expense_cases.ensure_case_for_claim(claim, tenant_id=tenant_id)
        existing = self._lock_case(tenant_id, provider, claim.id)
        before = _case_state(existing)
        amount = _amount(envelope.payload.get("amount")) or Decimal("0.00")
        association_error = self.actions.settlement_mismatch(claim, envelope.payload)
        error_code = association_error or "external_payment_failed"
        case = self._build_or_update_case(
            existing,
            tenant_id=tenant_id,
            provider=provider,
            claim=claim,
            expense_case_id=expense_case.id,
            actual_amount=max(amount, Decimal("0.00")),
            actual_currency=str(envelope.payload.get("currency") or "").upper(),
            external_reference=_tail(envelope.payload.get("external_payment_reference")),
            status="exception",
            exception_code=error_code,
            connector_event_id=connector_event_id,
        )
        audit = self._audit_event(
            case,
            connector_event_id=connector_event_id,
            action="exception_created",
            fingerprint=verified.request_fingerprint,
            before=before,
            reason=error_code,
            correlation_id=envelope.correlation_id,
        )
        return ReconciliationResult(
            case=case,
            audit_event=audit,
            processing_status="exception",
            error_code=error_code,
            claim_id=claim.id,
            expense_case_id=expense_case.id,
            claim_status=str(claim.status or ""),
        )

    def _erp_event(
        self,
        envelope: FinancialEventEnvelope,
        verified: VerifiedConnectorRequest,
        connector_event_id: str,
    ) -> ReconciliationResult:
        origin, claim, case = self._origin_graph(envelope, verified)
        if origin is None or claim is None or case is None:
            return self._without_case("origin_settlement_not_found")
        if origin.event_type != "payment_settled" or origin.processing_status != "processed":
            return self._without_case("origin_settlement_not_processed")
        if str(envelope.payload.get("claim_id") or "") != str(origin.claim_id or ""):
            return self._without_case("origin_claim_mismatch")

        # ERP 回执通过 origin_external_event_id 关联已认证的支付事实，协议本身
        # 不要求重复携带支付参考号，因此这里只校验申请、金额与币种。
        mismatch = self.actions.payment_payload_mismatch(
            claim,
            envelope.payload,
            require_external_reference=False,
        )
        if str(claim.status or "").lower() != "paid":
            mismatch = "claim_not_paid"
        if mismatch:
            return self._existing_case_exception(
                case,
                claim_status=str(claim.status or ""),
                connector_event_id=connector_event_id,
                fingerprint=verified.request_fingerprint,
                correlation_id=envelope.correlation_id,
                error_code=f"erp_{mismatch}",
            )

        before = _case_state(case)
        posted = envelope.event_type == "erp_posted"
        self.actions.record_erp_event(
            claim,
            tenant_id=envelope.tenant_id,
            provider=verified.config.provider,
            connector_event_id=connector_event_id,
            correlation_id=envelope.correlation_id,
            event_type=envelope.event_type,
            payload=envelope.payload,
        )
        case.erp_status = "posted" if posted else "posting_failed"
        case.last_connector_event_id = connector_event_id
        if posted and str(case.exception_code or "").startswith("erp_"):
            case.status = "matched"
        case.exception_code = None if posted else "erp_posting_failed"
        case.version += 1
        case.updated_at = datetime.now(UTC)
        if posted:
            document_number = str(envelope.payload.get("erp_document_number") or "").strip()
            case.erp_document_tail = _tail(document_number)
            case.erp_document_hash = (
                f"sha256:{hashlib.sha256(document_number.encode()).hexdigest()}"
            )
        action = "erp_posted" if posted else "erp_posting_failed"
        audit = self._audit_event(
            case,
            connector_event_id=connector_event_id,
            action=action,
            fingerprint=verified.request_fingerprint,
            before=before,
            reason=None if posted else "erp_posting_failed",
            correlation_id=envelope.correlation_id,
        )
        return ReconciliationResult(
            case=case,
            audit_event=audit,
            processing_status="processed" if posted else "exception",
            error_code=None if posted else "erp_posting_failed",
            claim_id=claim.id,
            expense_case_id=case.expense_case_id,
            claim_status=str(claim.status or ""),
        )

    def _payment_reversal(
        self,
        envelope: FinancialEventEnvelope,
        verified: VerifiedConnectorRequest,
        connector_event_id: str,
    ) -> ReconciliationResult:
        origin, claim, case = self._origin_graph(envelope, verified)
        if origin is None or claim is None or case is None:
            return self._without_case("origin_settlement_not_found")
        mismatch = self.actions.payment_payload_mismatch(claim, envelope.payload)
        if str(claim.status or "").lower() != "paid":
            mismatch = "claim_not_paid"
        if str(envelope.payload.get("claim_id") or "") != str(origin.claim_id or ""):
            mismatch = "origin_claim_mismatch"
        if origin.event_type != "payment_settled" or origin.processing_status != "processed":
            mismatch = "origin_settlement_not_processed"
        amount = _amount(envelope.payload.get("amount"))
        if amount is None or amount != Decimal(claim.amount).quantize(Decimal("0.01")):
            mismatch = "reversal_amount_mismatch"
        if mismatch:
            return self._existing_case_exception(
                case,
                claim_status=str(claim.status or ""),
                connector_event_id=connector_event_id,
                fingerprint=verified.request_fingerprint,
                correlation_id=envelope.correlation_id,
                error_code=mismatch,
            )

        before = _case_state(case)
        self.actions.reopen_claim(
            claim,
            tenant_id=envelope.tenant_id,
            provider=verified.config.provider,
            connector_event_id=connector_event_id,
            external_event_id=envelope.external_event_id,
            origin_connector_event_id=origin.id,
            content_hash=verified.content_hash,
            verification_level=verified.verification_level,
            evidence_classification=verified.evidence_classification,
            correlation_id=envelope.correlation_id,
        )
        case.status = "reopened"
        case.exception_code = "payment_reversed"
        case.last_connector_event_id = connector_event_id
        case.version += 1
        case.updated_at = datetime.now(UTC)
        audit = self._audit_event(
            case,
            connector_event_id=connector_event_id,
            action="reopened",
            fingerprint=verified.request_fingerprint,
            before=before,
            reason="payment_reversed",
            correlation_id=envelope.correlation_id,
        )
        return ReconciliationResult(
            case=case,
            audit_event=audit,
            processing_status="processed",
            error_code=None,
            claim_id=claim.id,
            expense_case_id=case.expense_case_id,
            claim_status=str(claim.status or ""),
        )

    def _origin_graph(
        self,
        envelope: FinancialEventEnvelope,
        verified: VerifiedConnectorRequest,
    ) -> tuple[
        FinancialConnectorEvent | None,
        Any | None,
        PaymentReconciliationCase | None,
    ]:
        origin = self.db.scalar(
            select(FinancialConnectorEvent).where(
                FinancialConnectorEvent.tenant_id == envelope.tenant_id,
                FinancialConnectorEvent.provider == verified.config.provider,
                FinancialConnectorEvent.environment == "production",
                FinancialConnectorEvent.verification_level == "production_verified",
                FinancialConnectorEvent.external_event_id
                == str(envelope.payload.get("origin_external_event_id") or ""),
            )
        )
        if origin is None or not origin.claim_id:
            return None, None, None
        claim = self.actions.lock_claim(
            tenant_id=envelope.tenant_id,
            claim_id=origin.claim_id,
        )
        case = self._lock_case(envelope.tenant_id, verified.config.provider, origin.claim_id)
        return origin, claim, case

    def _lock_case(
        self,
        tenant_id: str,
        provider: str,
        claim_id: str,
    ) -> PaymentReconciliationCase | None:
        statement = select(PaymentReconciliationCase).where(
            PaymentReconciliationCase.tenant_id == tenant_id,
            PaymentReconciliationCase.provider == provider,
            PaymentReconciliationCase.claim_id == claim_id,
        )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            statement = statement.with_for_update()
        return self.db.scalar(statement.execution_options(populate_existing=True))

    @staticmethod
    def _build_or_update_case(
        existing: PaymentReconciliationCase | None,
        *,
        tenant_id: str,
        provider: str,
        claim: Any,
        expense_case_id: str,
        actual_amount: Decimal,
        actual_currency: str,
        external_reference: str,
        status: str,
        exception_code: str | None,
        connector_event_id: str,
        reset_erp: bool = False,
    ) -> PaymentReconciliationCase:
        expected = Decimal(claim.amount or Decimal("0.00")).quantize(Decimal("0.01"))
        now = datetime.now(UTC)
        row = existing or PaymentReconciliationCase(
            id=str(uuid.uuid4()),
            tenant_id=tenant_id,
            provider=provider,
            claim_id=claim.id,
            expected_reference=str(claim.claim_no or ""),
            erp_status="pending_posting",
            version=0,
            created_at=now,
        )
        row.expense_case_id = expense_case_id
        row.expected_amount = expected
        row.actual_amount = actual_amount
        row.amount_difference = actual_amount - expected
        row.expected_currency = str(claim.currency or "CNY").upper()
        row.actual_currency = (actual_currency or row.expected_currency).upper()
        row.external_reference_tail = external_reference or None
        row.status = status
        row.exception_code = exception_code
        row.last_connector_event_id = connector_event_id
        if reset_erp:
            row.erp_status = "pending_posting"
            row.erp_document_tail = None
            row.erp_document_hash = None
        row.version += 1
        row.updated_at = now
        return row

    @staticmethod
    def _audit_event(
        case: PaymentReconciliationCase,
        *,
        connector_event_id: str,
        action: str,
        fingerprint: str,
        before: dict[str, Any],
        reason: str | None,
        correlation_id: str,
    ) -> PaymentReconciliationEvent:
        return PaymentReconciliationEvent(
            id=str(uuid.uuid4()),
            tenant_id=case.tenant_id,
            reconciliation_case_id=case.id,
            connector_event_id=connector_event_id,
            action=action,
            actor_type="connector",
            actor_id=f"financial-connector:{case.provider}",
            request_fingerprint=fingerprint,
            before_json=before,
            after_json=_case_state(case),
            response_json={"status": case.status, "version": case.version},
            reason=reason,
            correlation_id=correlation_id,
            occurred_at=datetime.now(UTC),
        )

    def _existing_case_exception(
        self,
        case: PaymentReconciliationCase,
        *,
        claim_status: str,
        connector_event_id: str,
        fingerprint: str,
        correlation_id: str,
        error_code: str,
    ) -> ReconciliationResult:
        before = _case_state(case)
        case.status = "exception"
        case.exception_code = error_code
        case.last_connector_event_id = connector_event_id
        case.version += 1
        case.updated_at = datetime.now(UTC)
        return ReconciliationResult(
            case=case,
            audit_event=self._audit_event(
                case,
                connector_event_id=connector_event_id,
                action="exception_created",
                fingerprint=fingerprint,
                before=before,
                reason=error_code,
                correlation_id=correlation_id,
            ),
            processing_status="exception",
            error_code=error_code,
            claim_id=case.claim_id,
            expense_case_id=case.expense_case_id,
            claim_status=claim_status,
        )

    @staticmethod
    def _without_case(error_code: str) -> ReconciliationResult:
        return ReconciliationResult(
            case=None,
            audit_event=None,
            processing_status="exception",
            error_code=error_code,
            claim_id=None,
            expense_case_id=None,
            claim_status=None,
        )


def _amount(value: Any) -> Decimal | None:
    try:
        return Decimal(str(value)).quantize(Decimal("0.01"))
    except (InvalidOperation, TypeError, ValueError):
        return None


def _tail(value: Any) -> str:
    return str(value or "").strip()[-8:]


def _case_state(case: PaymentReconciliationCase | None) -> dict[str, Any]:
    if case is None:
        return {}
    return {
        "status": str(case.status or ""),
        "exception_code": case.exception_code,
        "erp_status": str(case.erp_status or ""),
        "version": int(case.version or 0),
        "last_connector_event_id": str(case.last_connector_event_id or ""),
    }
