from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta

from sqlalchemy import case, func, select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.financial_connector import (
    FinancialConnectorConfig,
    FinancialConnectorEvent,
    FinancialConnectorOperationalEvent,
    PaymentReconciliationCase,
)
from app.schemas.financial_connector import (
    FinancialConnectorMetricAvailabilityRead,
    FinancialConnectorObservabilityItemRead,
    FinancialConnectorObservabilityRead,
    FinancialConnectorObservabilitySummaryRead,
)
from app.services.financial_connector_operational_events import (
    FINANCIAL_CONNECTOR_OPERATIONAL_SOURCE_REVISION,
)


class FinancialConnectorObservabilityPermissionError(PermissionError):
    pass


class FinancialConnectorObservabilityService:
    """只从租户最小化事实聚合指标，不读取或返回原始财务 payload。"""

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

    @staticmethod
    def require_finance_or_admin(current_user: CurrentUserContext) -> None:
        roles = {str(item).strip().lower() for item in current_user.role_codes}
        if not current_user.is_admin and not roles & {"finance", "executive"}:
            raise FinancialConnectorObservabilityPermissionError(
                "只有财务业务角色或平台管理员可以查看连接器运行指标。"
            )

    def read_for_current_user(
        self,
        current_user: CurrentUserContext,
        *,
        window_hours: int,
    ) -> FinancialConnectorObservabilityRead:
        self.require_finance_or_admin(current_user)
        return self.read_for_tenant(current_user.tenant_id, window_hours=window_hours)

    def read_for_tenant(
        self,
        tenant_id: str,
        *,
        window_hours: int,
    ) -> FinancialConnectorObservabilityRead:
        tenant = str(tenant_id or "").strip()
        started_at = self.now - timedelta(hours=window_hours)
        configs = list(
            self.db.scalars(
                select(FinancialConnectorConfig)
                .where(FinancialConnectorConfig.tenant_id == tenant)
                .order_by(
                    FinancialConnectorConfig.provider,
                    FinancialConnectorConfig.key_version,
                )
            ).all()
        )
        event_stats = self._event_stats(tenant, started_at, self.now)
        operational_stats = self._operational_stats(tenant, started_at, self.now)
        anomaly_stats = self._anomaly_stats(tenant, started_at, self.now)
        anomaly_owners = _anomaly_owner_config_ids(configs)
        items = [
            self._item(
                config,
                event_stats.get(config.id, _EMPTY_EVENT_STATS),
                operational_stats.get(config.id, _OperationalStats()),
                (
                    anomaly_stats.get(config.provider, 0)
                    if anomaly_owners.get(config.provider) == config.id
                    else 0
                ),
            )
            for config in configs
        ]
        event_count = sum(item.event_count for item in items)
        failed_count = sum(item.failed_event_count for item in items)
        retry_metric = _durable_metric("financial_connector_operational_events.replay")
        auth_failure_metric = _durable_metric(
            "financial_connector_operational_events.auth_failure"
        )
        signature_metric = _durable_metric(
            "financial_connector_operational_events.auth_failure:signature_invalid"
        )
        conflict_metric = _durable_metric(
            "financial_connector_operational_events.payload_conflict"
        )
        return FinancialConnectorObservabilityRead(
            tenant_id=tenant,
            window_hours=window_hours,
            window_started_at=started_at,
            as_of=self.now,
            generated_at=self.now,
            source_revision=FINANCIAL_CONNECTOR_OPERATIONAL_SOURCE_REVISION,
            summary=FinancialConnectorObservabilitySummaryRead(
                connector_count=len(items),
                active_connector_count=sum(item.status == "active" for item in items),
                event_count=event_count,
                processed_event_count=sum(item.processed_event_count for item in items),
                failed_event_count=failed_count,
                failure_rate=_rate(failed_count, event_count),
                backlog_count=sum(item.backlog_count for item in items),
                reconciliation_anomaly_count=sum(anomaly_stats.values()),
                retry_count=sum(int(item.retry_count or 0) for item in items),
                auth_failure_count=sum(item.auth_failure_count for item in items),
                signature_failure_count=sum(
                    int(item.signature_failure_count or 0) for item in items
                ),
                payload_conflict_count=sum(item.payload_conflict_count for item in items),
                latest_replay_at=_latest(item.latest_replay_at for item in items),
                latest_auth_failure_at=_latest(
                    item.latest_auth_failure_at for item in items
                ),
                latest_signature_failure_at=_latest(
                    item.latest_signature_failure_at for item in items
                ),
                latest_payload_conflict_at=_latest(
                    item.latest_payload_conflict_at for item in items
                ),
            ),
            retry_metric=retry_metric,
            auth_failure_metric=auth_failure_metric,
            signature_failure_metric=signature_metric,
            payload_conflict_metric=conflict_metric,
            items=items,
        )

    def _event_stats(
        self,
        tenant_id: str,
        started_at: datetime,
        as_of: datetime,
    ) -> dict[str, tuple[int, int, int, int]]:
        rows = self.db.execute(
            select(
                FinancialConnectorEvent.config_id,
                func.count(FinancialConnectorEvent.id),
                func.sum(
                    case((FinancialConnectorEvent.processing_status == "processed", 1), else_=0)
                ),
                func.sum(
                    case((FinancialConnectorEvent.processing_status == "exception", 1), else_=0)
                ),
                func.sum(
                    case((FinancialConnectorEvent.processing_status == "pending", 1), else_=0)
                ),
            )
            .where(
                FinancialConnectorEvent.tenant_id == tenant_id,
                FinancialConnectorEvent.received_at >= started_at,
                FinancialConnectorEvent.received_at <= as_of,
            )
            .group_by(FinancialConnectorEvent.config_id)
        ).all()
        return {
            str(config_id): (
                int(total or 0),
                int(processed or 0),
                int(failed or 0),
                int(pending or 0),
            )
            for config_id, total, processed, failed, pending in rows
        }

    def _operational_stats(
        self,
        tenant_id: str,
        started_at: datetime,
        as_of: datetime,
    ) -> dict[str, _OperationalStats]:
        rows = self.db.execute(
            select(
                FinancialConnectorOperationalEvent.config_id,
                FinancialConnectorOperationalEvent.event_type,
                FinancialConnectorOperationalEvent.reason_code,
                func.count(FinancialConnectorOperationalEvent.id),
                func.max(FinancialConnectorOperationalEvent.occurred_at),
            )
            .where(
                FinancialConnectorOperationalEvent.tenant_id == tenant_id,
                FinancialConnectorOperationalEvent.occurred_at >= started_at,
                FinancialConnectorOperationalEvent.occurred_at <= as_of,
            )
            .group_by(
                FinancialConnectorOperationalEvent.config_id,
                FinancialConnectorOperationalEvent.event_type,
                FinancialConnectorOperationalEvent.reason_code,
            )
        ).all()
        result: dict[str, _OperationalStats] = {}
        for config_id, event_type, reason_code, total, latest in rows:
            stats = result.setdefault(str(config_id), _OperationalStats())
            count = int(total or 0)
            if event_type == "replay":
                stats.replay_count += count
                stats.latest_replay_at = _latest((stats.latest_replay_at, latest))
            elif event_type == "auth_failure":
                stats.auth_failure_count += count
                stats.latest_auth_failure_at = _latest(
                    (stats.latest_auth_failure_at, latest)
                )
                if reason_code == "signature_invalid":
                    stats.signature_failure_count += count
                    stats.latest_signature_failure_at = _latest(
                        (stats.latest_signature_failure_at, latest)
                    )
            elif event_type == "payload_conflict":
                stats.payload_conflict_count += count
                stats.latest_payload_conflict_at = _latest(
                    (stats.latest_payload_conflict_at, latest)
                )
        return result

    def _anomaly_stats(
        self,
        tenant_id: str,
        started_at: datetime,
        as_of: datetime,
    ) -> dict[str, int]:
        rows = self.db.execute(
            select(PaymentReconciliationCase.provider, func.count(PaymentReconciliationCase.id))
            .where(
                PaymentReconciliationCase.tenant_id == tenant_id,
                PaymentReconciliationCase.updated_at >= started_at,
                PaymentReconciliationCase.updated_at <= as_of,
                PaymentReconciliationCase.status.in_({"exception", "reopened"}),
            )
            .group_by(PaymentReconciliationCase.provider)
        ).all()
        return {str(provider): int(total or 0) for provider, total in rows}

    @staticmethod
    def _item(
        config: FinancialConnectorConfig,
        stats: tuple[int, int, int, int],
        operational: _OperationalStats,
        anomaly_count: int,
    ) -> FinancialConnectorObservabilityItemRead:
        total, processed, failed, pending = stats
        classification, label = _evidence_for_environment(config.environment)
        return FinancialConnectorObservabilityItemRead(
            config_id=config.id,
            provider=config.provider,
            environment=config.environment,
            key_version=config.key_version,
            status=config.status,
            evidence_classification=classification,
            evidence_label=label,
            last_success_at=config.last_success_at,
            last_error_at=config.last_error_at,
            last_error_code=config.last_error_code,
            event_count=total,
            processed_event_count=processed,
            failed_event_count=failed,
            failure_rate=_rate(failed, total),
            backlog_count=pending,
            reconciliation_anomaly_count=anomaly_count,
            retry_count=operational.replay_count,
            auth_failure_count=operational.auth_failure_count,
            signature_failure_count=operational.signature_failure_count,
            payload_conflict_count=operational.payload_conflict_count,
            latest_replay_at=operational.latest_replay_at,
            latest_auth_failure_at=operational.latest_auth_failure_at,
            latest_signature_failure_at=operational.latest_signature_failure_at,
            latest_payload_conflict_at=operational.latest_payload_conflict_at,
        )


_EMPTY_EVENT_STATS = (0, 0, 0, 0)


@dataclass(slots=True)
class _OperationalStats:
    replay_count: int = 0
    auth_failure_count: int = 0
    signature_failure_count: int = 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


def _rate(numerator: int, denominator: int) -> float:
    if denominator <= 0:
        return 0.0
    return round(numerator / denominator, 4)


def _evidence_for_environment(environment: str) -> tuple[str, str]:
    if environment == "production":
        return "external_cash", "生产外部现金回执"
    if environment == "staging":
        return "staging_connector", "预发布验证回执（不入核心账）"
    return "simulated_connector", "模拟回执（不入核心账）"


def _durable_metric(source: str) -> FinancialConnectorMetricAvailabilityRead:
    return FinancialConnectorMetricAvailabilityRead(
        status="available",
        source=source,
        reason=None,
    )


def _latest(values: Iterable[datetime | None]) -> datetime | None:
    timestamps = [
        item.replace(tzinfo=UTC) if item.tzinfo is None else item.astimezone(UTC)
        for item in values
        if isinstance(item, datetime)
    ]
    return max(timestamps) if timestamps else None


def _anomaly_owner_config_ids(
    configs: list[FinancialConnectorConfig],
) -> dict[str, str]:
    """对账投影只有 provider 维度，异常只归到一个配置版本以避免轮换期重复计数。"""

    owners: dict[str, FinancialConnectorConfig] = {}
    priority = {"active": 3, "rotating": 2, "disabled": 1}
    for config in configs:
        current = owners.get(config.provider)
        if current is None or (
            priority.get(config.status, 0),
            int(config.version or 0),
            config.key_version,
        ) > (
            priority.get(current.status, 0),
            int(current.version or 0),
            current.key_version,
        ):
            owners[config.provider] = config
    return {provider: config.id for provider, config in owners.items()}
