from __future__ import annotations

import hashlib
import uuid
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import select, text
from sqlalchemy.orm import Session

from app.models.financial_connector import FinancialConnectorEvent
from app.schemas.financial_connector import (
    FinancialEventEnvelope,
    FinancialEventIngestionRead,
)
from app.services.financial_connector_auth import (
    FinancialConnectorAuthenticator,
    FinancialConnectorAuthError,
    FinancialConnectorSecretResolver,
    VerifiedConnectorRequest,
)
from app.services.financial_connector_commercial import (
    FinancialConnectorCommercialObserver,
    build_financial_connector_commercial_observer,
)
from app.services.financial_connector_operational_events import (
    FinancialConnectorOperationalEventCandidate,
    FinancialConnectorOperationalEventService,
    operational_event_candidate,
)
from app.services.financial_connector_simulation import FinancialConnectorSimulationService
from app.services.payment_reconciliation import PaymentReconciliationService


class FinancialConnectorConflictError(ValueError):
    def __init__(
        self,
        message: str,
        *,
        operational_event: FinancialConnectorOperationalEventCandidate,
    ) -> None:
        super().__init__(message)
        self.operational_event = operational_event


class FinancialConnectorIngestionService:
    def __init__(
        self,
        db: Session,
        *,
        secrets: FinancialConnectorSecretResolver | None = None,
        now_epoch: int | None = None,
        commercial_observer: FinancialConnectorCommercialObserver | None = None,
    ) -> None:
        self.db = db
        self.received_at = (
            datetime.fromtimestamp(now_epoch, UTC) if now_epoch is not None else datetime.now(UTC)
        )
        self.authenticator = FinancialConnectorAuthenticator(
            db,
            secrets=secrets,
            now_epoch=now_epoch,
        )
        self.reconciliation = PaymentReconciliationService(db)
        self.simulation = FinancialConnectorSimulationService(db)
        self.operational_events = FinancialConnectorOperationalEventService(db)
        self.commercial_observer = (
            commercial_observer or build_financial_connector_commercial_observer(db)
        )

    def ingest(
        self,
        envelope: FinancialEventEnvelope,
        *,
        tenant_header: str,
        provider_header: str,
        key_version_header: str,
        timestamp_header: str,
        signature_header: str,
    ) -> FinancialEventIngestionRead:
        try:
            verified = self.authenticator.verify(
                envelope,
                tenant_header=tenant_header,
                provider_header=provider_header,
                key_version_header=key_version_header,
                timestamp_header=timestamp_header,
                signature_header=signature_header,
            )
        except FinancialConnectorAuthError as error:
            if error.operational_context is not None:
                candidate = operational_event_candidate(
                    error.operational_context,
                    event_type="auth_failure",
                    reason_code=error.code,
                    occurred_at=self.received_at,
                )
                self.operational_events.record(candidate)
                error.operational_event = candidate
            raise
        provider = verified.config.provider
        self._serialize_external_event(
            tenant_id=envelope.tenant_id,
            provider=provider,
            external_event_id=envelope.external_event_id,
        )
        existing = self.db.scalar(
            select(FinancialConnectorEvent).where(
                FinancialConnectorEvent.tenant_id == envelope.tenant_id,
                FinancialConnectorEvent.provider == provider,
                FinancialConnectorEvent.external_event_id == envelope.external_event_id,
            )
        )
        if existing is not None:
            if existing.request_fingerprint != verified.request_fingerprint:
                candidate = operational_event_candidate(
                    verified.operational_context,
                    event_type="payload_conflict",
                    reason_code="external_event_payload_conflict",
                    occurred_at=self.received_at,
                )
                self.operational_events.record(candidate)
                raise FinancialConnectorConflictError(
                    "相同 external_event_id 已对应不同 payload，首次事实不会被覆盖。",
                    operational_event=candidate,
                )
            self.operational_events.record(
                operational_event_candidate(
                    verified.operational_context,
                    event_type="replay",
                    reason_code="duplicate_external_event",
                    occurred_at=self.received_at,
                )
            )
            replay = FinancialEventIngestionRead.model_validate(existing.response_json)
            return replay.model_copy(update={"replayed": True})

        commercial_attempt = self.commercial_observer.permit(
            tenant_id=verified.config.tenant_id,
            config_id=verified.config.id,
            provider=provider,
            request_fingerprint=verified.request_fingerprint,
            received_at=self.received_at,
        )
        connector_event_id = str(uuid.uuid4())
        try:
            with self.db.no_autoflush:
                if verified.verification_level == "production_verified":
                    result = self.reconciliation.process(
                        envelope,
                        verified,
                        connector_event_id=connector_event_id,
                    )
                else:
                    result = self.simulation.project(envelope, verified)
                # 对账过程会更新现有投影并让它引用本次事实。原始事件查询必须继续
                # 禁止自动 flush，确保新事实先入库，再更新带外键的投影。
                origin_event_id = self._origin_event_id(
                    envelope,
                    provider=provider,
                    verified=verified,
                )
            response = FinancialEventIngestionRead(
                accepted=True,
                replayed=False,
                event_id=connector_event_id,
                external_event_id=envelope.external_event_id,
                processing_status=result.processing_status,
                reconciliation_case_id=result.case.id if result.case is not None else None,
                reconciliation_status=result.case.status if result.case is not None else None,
                claim_status=result.claim_status,
                verification_level=verified.verification_level,
                evidence_classification=verified.evidence_classification,
                projection_scope=(
                    "canonical"
                    if verified.verification_level == "production_verified"
                    else "simulation_only"
                ),
                error_code=result.error_code,
            )
            event = FinancialConnectorEvent(
                id=connector_event_id,
                tenant_id=envelope.tenant_id,
                config_id=verified.config.id,
                provider=provider,
                environment=verified.config.environment,
                direction="inbound",
                external_event_id=envelope.external_event_id,
                event_type=envelope.event_type,
                occurred_at=envelope.occurred_at,
                received_at=self.received_at,
                key_version=verified.config.key_version,
                verification_level=verified.verification_level,
                request_fingerprint=verified.request_fingerprint,
                content_hash=verified.content_hash,
                processing_status=result.processing_status,
                error_code=result.error_code,
                claim_id=result.claim_id,
                expense_case_id=result.expense_case_id,
                origin_event_id=origin_event_id,
                correlation_id=envelope.correlation_id,
                external_reference_tail=_tail(envelope.payload.get("external_payment_reference"))
                or None,
                normalized_payload_json=_sanitized_payload(envelope, result.claim_id),
                response_json=response.model_dump(mode="json"),
            )
            self.db.add(event)
            self.db.flush([event])
            if result.case is not None:
                self.db.add(result.case)
                self.db.flush([result.case])
            if result.audit_event is not None:
                self.db.add(result.audit_event)
                self.db.flush([result.audit_event])

            now = self.received_at
            if result.processing_status == "processed":
                verified.config.last_success_at = now
                verified.config.last_error_code = None
            else:
                verified.config.last_error_at = now
                verified.config.last_error_code = result.error_code
            self.commercial_observer.bind_to_transaction(self.db, commercial_attempt)
            return response
        except Exception:
            self.commercial_observer.release(commercial_attempt)
            raise

    def _origin_event_id(
        self,
        envelope: FinancialEventEnvelope,
        *,
        provider: str,
        verified: VerifiedConnectorRequest,
    ) -> str | None:
        if envelope.event_type not in {
            "payment_refunded",
            "payment_reversed",
            "erp_posted",
            "erp_posting_failed",
        }:
            return None
        return self.db.scalar(
            select(FinancialConnectorEvent.id).where(
                FinancialConnectorEvent.tenant_id == envelope.tenant_id,
                FinancialConnectorEvent.provider == provider,
                FinancialConnectorEvent.environment == verified.config.environment,
                FinancialConnectorEvent.verification_level == verified.verification_level,
                FinancialConnectorEvent.external_event_id
                == str(envelope.payload.get("origin_external_event_id") or ""),
            )
        )

    def _serialize_external_event(
        self,
        *,
        tenant_id: str,
        provider: str,
        external_event_id: str,
    ) -> None:
        bind = self.db.get_bind()
        if bind is None or bind.dialect.name != "postgresql":
            return
        lock_key = f"financial-event:{tenant_id}:{provider}:{external_event_id}"
        self.db.execute(
            text("SELECT pg_advisory_xact_lock(hashtextextended(:lock_key, 0))"),
            {"lock_key": lock_key},
        )


def _sanitized_payload(
    envelope: FinancialEventEnvelope,
    trusted_claim_id: str | None,
) -> dict[str, Any]:
    payload = envelope.payload
    normalized: dict[str, Any] = {
        "claim_id": trusted_claim_id,
        "amount": str(payload.get("amount") or "")[:40],
        "currency": str(payload.get("currency") or "").upper()[:3],
        "external_reference_tail": _tail(payload.get("external_payment_reference")),
        "origin_external_event_id": str(payload.get("origin_external_event_id") or "")[:160]
        or None,
        "failure_code": str(payload.get("failure_code") or "")[:80] or None,
        "accounting_period": str(payload.get("accounting_period") or "")[:24] or None,
    }
    document = str(payload.get("erp_document_number") or "").strip()
    if document:
        normalized["erp_document_tail"] = _tail(document)
        normalized["erp_document_hash"] = (
            f"sha256:{hashlib.sha256(document.encode('utf-8')).hexdigest()}"
        )
    return normalized


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