from __future__ import annotations

import hashlib
import json
from datetime import UTC, datetime
from decimal import Decimal

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.financial_connector import FinancialConnectorConfig, FinancialConnectorEvent
from app.models.financial_record import ExpenseClaim
from app.schemas.financial_connector import (
    FinancialConnectorSimulationCreate,
    FinancialConnectorSimulationRead,
    FinancialConnectorSimulationStepRead,
    FinancialEventEnvelope,
)
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin
from app.services.financial_connector_auth import (
    FinancialConnectorSecretResolver,
    sign_financial_event,
)
from app.services.financial_connector_ingestion import (
    FinancialConnectorConflictError,
    FinancialConnectorIngestionService,
)


class FinancialConnectorMockAdapterError(ValueError):
    pass


class FinancialConnectorMockAdapter:
    """通过正式认证入口生成确定性的非生产连接器场景。"""

    def __init__(
        self,
        db: Session,
        *,
        secrets: FinancialConnectorSecretResolver | None = None,
        now: datetime | None = None,
    ) -> None:
        self.db = db
        self.secrets = secrets or FinancialConnectorSecretResolver.from_settings()
        self.now = now or datetime.now(UTC)

    def run(
        self,
        *,
        tenant_id: str,
        config_id: str,
        payload: FinancialConnectorSimulationCreate,
    ) -> FinancialConnectorSimulationRead:
        tenant = str(tenant_id or "").strip()
        config = self.db.scalar(
            select(FinancialConnectorConfig).where(
                FinancialConnectorConfig.tenant_id == tenant,
                FinancialConnectorConfig.id == str(config_id or "").strip(),
            )
        )
        if config is None:
            raise LookupError("连接器配置不存在。")
        if config.status != "active":
            raise FinancialConnectorMockAdapterError("只有已激活的连接器配置可以运行模拟。")
        if config.environment == "production":
            raise FinancialConnectorMockAdapterError("生产连接器禁止运行模拟场景。")

        claim = self.db.scalar(
            select(ExpenseClaim).where(
                ExpenseClaim.id == payload.claim_id,
                ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant),
            )
        )
        if claim is None:
            raise LookupError("报销单不存在。")

        required_types = _SCENARIO_EVENT_TYPES[payload.scenario]
        allowed_types = {str(item).strip() for item in config.allowed_event_types_json or []}
        missing = sorted(required_types - allowed_types)
        if missing:
            raise FinancialConnectorMockAdapterError(
                f"连接器未允许模拟所需事件类型：{', '.join(missing)}。"
            )

        request_fingerprint, digest = _simulation_fingerprint(
            tenant_id=tenant,
            config_id=config.id,
            claim_id=claim.id,
            scenario=payload.scenario,
            request_id=payload.request_id,
        )
        timestamp = int(self.now.timestamp())
        ingestion = FinancialConnectorIngestionService(
            self.db,
            secrets=self.secrets,
            now_epoch=timestamp,
        )
        steps = self._run_scenario(
            ingestion=ingestion,
            config=config,
            claim=claim,
            scenario=payload.scenario,
            digest=digest,
            timestamp=timestamp,
        )
        return FinancialConnectorSimulationRead(
            tenant_id=tenant,
            config_id=config.id,
            provider=config.provider,
            environment=config.environment,
            scenario=payload.scenario,
            request_fingerprint=request_fingerprint,
            evidence_classification=(
                "staging_connector" if config.environment == "staging" else "simulated_connector"
            ),
            steps=steps,
        )

    def _run_scenario(
        self,
        *,
        ingestion: FinancialConnectorIngestionService,
        config: FinancialConnectorConfig,
        claim: ExpenseClaim,
        scenario: str,
        digest: str,
        timestamp: int,
    ) -> list[FinancialConnectorSimulationStepRead]:
        if scenario == "failure":
            return [
                self._ingest_step(
                    ingestion,
                    config,
                    self._envelope(config, claim, scenario, digest, "failure", "payment_failed"),
                    timestamp,
                )
            ]
        if scenario == "out_of_order":
            return [
                self._ingest_step(
                    ingestion,
                    config,
                    self._envelope(
                        config,
                        claim,
                        scenario,
                        digest,
                        "refund",
                        "payment_refunded",
                        origin_external_event_id=f"sim-missing-{digest[:24]}",
                    ),
                    timestamp,
                )
            ]

        settlement = self._envelope(
            config,
            claim,
            scenario,
            digest,
            "settlement",
            "payment_settled",
        )
        first = self._ingest_step(ingestion, config, settlement, timestamp)
        if scenario == "success":
            return [first]
        if scenario == "duplicate":
            return [first, self._ingest_step(ingestion, config, settlement, timestamp)]
        if scenario == "conflict":
            conflicting = self._envelope(
                config,
                claim,
                scenario,
                digest,
                "settlement",
                "payment_settled",
                amount=Decimal(claim.amount) + Decimal("0.01"),
            )
            try:
                second = self._ingest_step(ingestion, config, conflicting, timestamp)
            except FinancialConnectorConflictError:
                second = FinancialConnectorSimulationStepRead(
                    name="settlement",
                    event_type="payment_settled",
                    outcome="conflict",
                    error_code="external_event_payload_conflict",
                )
            return [first, second]

        follow_up_type = "payment_refunded" if scenario == "refund" else "erp_posted"
        follow_up_name = "refund" if scenario == "refund" else "erp_receipt"
        follow_up = self._envelope(
            config,
            claim,
            scenario,
            digest,
            follow_up_name,
            follow_up_type,
            origin_external_event_id=settlement.external_event_id,
        )
        return [first, self._ingest_step(ingestion, config, follow_up, timestamp)]

    def _envelope(
        self,
        config: FinancialConnectorConfig,
        claim: ExpenseClaim,
        scenario: str,
        digest: str,
        step: str,
        event_type: str,
        *,
        origin_external_event_id: str | None = None,
        amount: Decimal | None = None,
    ) -> FinancialEventEnvelope:
        external_event_id = f"sim-{scenario}-{digest[:24]}-{step}"[:160]
        existing_time = self.db.scalar(
            select(FinancialConnectorEvent.occurred_at).where(
                FinancialConnectorEvent.tenant_id == config.tenant_id,
                FinancialConnectorEvent.provider == config.provider,
                FinancialConnectorEvent.external_event_id == external_event_id,
            )
        )
        event_payload: dict[str, str] = {
            "claim_id": claim.id,
            "claim_reference": claim.claim_no,
            "amount": str(amount if amount is not None else claim.amount),
            "currency": str(claim.currency or "CNY").strip().upper(),
        }
        if event_type in {"payment_settled", "payment_failed", "payment_refunded"}:
            event_payload["external_payment_reference"] = f"SIM-PAY-{digest[-16:]}"
        if origin_external_event_id:
            event_payload["origin_external_event_id"] = origin_external_event_id
        if event_type == "payment_failed":
            event_payload["failure_code"] = "simulated_provider_failure"
        if event_type == "erp_posted":
            event_payload["erp_document_number"] = f"SIM-ERP-{digest[-16:]}"
            event_payload["accounting_period"] = self.now.strftime("%Y-%m")
        return FinancialEventEnvelope(
            tenant_id=config.tenant_id,
            external_event_id=external_event_id,
            event_type=event_type,
            occurred_at=_aware(existing_time or self.now),
            correlation_id=f"sim-{digest[:56]}",
            payload=event_payload,
        )

    def _ingest_step(
        self,
        ingestion: FinancialConnectorIngestionService,
        config: FinancialConnectorConfig,
        envelope: FinancialEventEnvelope,
        timestamp: int,
    ) -> FinancialConnectorSimulationStepRead:
        secret = self.secrets.resolve(config.secret_ref).decode("utf-8")
        signature = sign_financial_event(
            envelope,
            timestamp=timestamp,
            secret=secret,
            tenant_id=config.tenant_id,
            provider=config.provider,
            key_version=config.key_version,
        )
        result = ingestion.ingest(
            envelope,
            tenant_header=config.tenant_id,
            provider_header=config.provider,
            key_version_header=config.key_version,
            timestamp_header=str(timestamp),
            signature_header=signature,
        )
        if result.replayed:
            outcome = "replayed"
        elif result.processing_status == "exception":
            outcome = "expected_exception"
        else:
            outcome = "accepted"
        return FinancialConnectorSimulationStepRead(
            name=envelope.external_event_id.rsplit("-", 1)[-1],
            event_type=envelope.event_type,
            outcome=outcome,
            event_id=result.event_id,
            processing_status=result.processing_status,
            error_code=result.error_code,
        )


_SCENARIO_EVENT_TYPES: dict[str, set[str]] = {
    "success": {"payment_settled"},
    "failure": {"payment_failed"},
    "out_of_order": {"payment_refunded"},
    "duplicate": {"payment_settled"},
    "conflict": {"payment_settled"},
    "refund": {"payment_settled", "payment_refunded"},
    "erp_receipt": {"payment_settled", "erp_posted"},
}


def _simulation_fingerprint(
    *,
    tenant_id: str,
    config_id: str,
    claim_id: str,
    scenario: str,
    request_id: str,
) -> tuple[str, str]:
    raw = json.dumps(
        {
            "tenant_id": tenant_id,
            "config_id": config_id,
            "claim_id": claim_id,
            "scenario": scenario,
            "request_id": request_id,
        },
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    digest = hashlib.sha256(raw).hexdigest()
    return f"sha256:{digest}", digest


def _aware(value: datetime) -> datetime:
    if value.tzinfo is None:
        return value.replace(tzinfo=UTC)
    return value
