from __future__ import annotations

from collections import defaultdict
from datetime import UTC, datetime
from decimal import Decimal
from typing import Any

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

from app.api.deps import CurrentUserContext
from app.models.savings import SavingsOpportunity, SavingsRealization
from app.schemas.cfo_value import (
    CfoValueBreakdownGroupRead,
    CfoValueBreakdownItemRead,
    CfoValueCashKpiRead,
    CfoValueDashboardRead,
    CfoValueDataQualityRead,
    CfoValueFiltersRead,
    CfoValueFunnelRead,
    CfoValueFunnelStageRead,
    CfoValueGuardrailRead,
    CfoValueKpisRead,
    CfoValueMoneyRead,
    CfoValueSourceRead,
    CfoValueTrendPointRead,
    CfoValueUnavailableKpiRead,
    CfoValueWindowRead,
)
from app.services.savings_access_policy import SavingsAccessPolicy

MoneyMap = dict[str, Decimal]


class CfoValueAnalyticsService:
    """只从 Savings Ledger 聚合可追溯 CFO 价值，不推导演示收益。"""

    def __init__(self, db: Session) -> None:
        self.db = db

    def build_dashboard(
        self,
        current_user: CurrentUserContext,
        *,
        start: datetime,
        end: datetime,
        as_of: datetime,
        filters: CfoValueFiltersRead,
    ) -> CfoValueDashboardRead:
        SavingsAccessPolicy.require_tenant_value_read(current_user)
        start, end, as_of = self._normalize_window(start, end, as_of)
        tenant_id = str(current_user.tenant_id or "").strip()
        if not tenant_id:
            raise PermissionError("当前登录上下文缺少租户，不能聚合经营价值。")

        opportunity_statement = select(SavingsOpportunity).where(
            SavingsOpportunity.tenant_id == tenant_id,
            SavingsOpportunity.created_at >= start,
            SavingsOpportunity.created_at <= end,
            SavingsOpportunity.created_at <= as_of,
        )
        opportunity_statement = SavingsAccessPolicy.apply_opportunity_read_scope(
            opportunity_statement,
            current_user,
            include_owner=False,
        )
        opportunity_statement = self._apply_filters(opportunity_statement, filters)
        opportunities = list(self.db.scalars(opportunity_statement).all())

        realization_statement = (
            select(SavingsRealization, SavingsOpportunity)
            .join(
                SavingsOpportunity,
                (SavingsOpportunity.tenant_id == SavingsRealization.tenant_id)
                & (SavingsOpportunity.id == SavingsRealization.opportunity_id),
            )
            .where(
                SavingsRealization.tenant_id == tenant_id,
                SavingsRealization.realized_at >= start,
                SavingsRealization.realized_at <= end,
                SavingsRealization.realized_at <= as_of,
            )
        )
        realization_statement = SavingsAccessPolicy.apply_opportunity_read_scope(
            realization_statement,
            current_user,
            include_owner=False,
        )
        realization_statement = self._apply_filters(realization_statement, filters)
        realization_rows = list(self.db.execute(realization_statement).all())
        opportunity_by_id = {item.id: item for item in opportunities}
        for _, opportunity in realization_rows:
            opportunity_by_id.setdefault(opportunity.id, opportunity)
        relevant_opportunities = list(opportunity_by_id.values())

        verified_rows = [
            (realization, opportunity)
            for realization, opportunity in realization_rows
            if self._is_verified(realization, opportunity, as_of=as_of)
        ]
        pending_rows = [
            (realization, opportunity)
            for realization, opportunity in realization_rows
            if realization.status == "pending_confirmation"
        ]
        verified_cash = self._sum_realizations(verified_rows)
        pending_cash = self._sum_realizations(pending_rows)
        reversal_rows = [row for row in verified_rows if row[0].realization_type == "reversal"]
        reversal_money = self._sum_realizations(reversal_rows)
        estimated = self._sum_opportunities(opportunities)
        in_progress = self._sum_opportunities(
            [item for item in opportunities if item.status == "in_progress"]
        )
        rejected_or_expired = self._sum_opportunities(
            [item for item in opportunities if item.status in {"rejected", "expired"}]
        )
        mature = [
            item
            for item in opportunities
            if item.status in {"realized", "verified", "reversed", "rejected", "expired"}
            or (
                item.status == "in_progress"
                and item.due_at is not None
                and self._utc(item.due_at) <= as_of
            )
        ]
        mature_estimated = self._sum_opportunities(mature)

        data_quality = self._data_quality(realization_rows)
        freshness_at = self._freshness(opportunities, realization_rows)
        source_status = (
            "empty"
            if not opportunities and not realization_rows
            else (
                "partial"
                if data_quality.missing_evidence_count or data_quality.missing_fx_count
                else "complete"
            )
        )
        return CfoValueDashboardRead(
            window=CfoValueWindowRead(start=start, end=end, as_of=as_of),
            source=CfoValueSourceRead(
                generated_at=datetime.now(UTC),
                freshness_at=freshness_at,
                data_status=source_status,
                opportunity_count=len(relevant_opportunities),
                realization_count=len(realization_rows),
                coverage_notes=[
                    "现金价值仅包含 canonical 且已独立财务确认的实际结果。",
                    "当前付款证据为平台业务状态，尚未接入银行或 ERP 回执。",
                ],
            ),
            filters=filters,
            kpis=CfoValueKpisRead(
                verified_cash=CfoValueCashKpiRead(
                    status="available" if verified_cash else "empty",
                    values=self._money_read(verified_cash),
                    confirmed_realization_count=len(verified_rows),
                    definition=(
                        "按实际发生期间汇总 canonical、finance_confirmed 的现金结果，"
                        "包含只追加负向冲回并扣除执行成本。"
                    ),
                ),
                releasable_labor=CfoValueUnavailableKpiRead(
                    key="verified_releasable_labor_value",
                    label="财务确认可释放工时价值",
                    status="collecting",
                    reason="尚未形成客户认可的活跃工时、角色成本与可释放比例基线。",
                    required_inputs=[
                        "上线前后每单人工活跃分钟",
                        "生效期内角色完全成本",
                        "客户确认的可释放比例",
                    ],
                ),
                safe_straight_through=CfoValueUnavailableKpiRead(
                    key="safe_straight_through_rate",
                    label="安全智能直通率",
                    status="collecting",
                    reason="尚未冻结 eligibility 队列并完成事后审计结果回填。",
                    required_inputs=[
                        "创建时 eligibility 快照",
                        "必要审批完成事实",
                        "事后审计重大问题结果",
                    ],
                ),
            ),
            funnel=self._funnel(
                opportunities,
                pending_rows=pending_rows,
                verified_rows=verified_rows,
                reversal_rows=reversal_rows,
                estimated=estimated,
                in_progress=in_progress,
                pending_cash=pending_cash,
                verified_cash=verified_cash,
                reversal_money=reversal_money,
                rejected_or_expired=rejected_or_expired,
                mature_estimated=mature_estimated,
            ),
            trend=self._trend(realization_rows, as_of=as_of),
            breakdowns=self._breakdowns(relevant_opportunities, verified_rows),
            guardrails=self._guardrails(
                realization_rows,
                verified_rows=verified_rows,
                reversal_rows=reversal_rows,
                pending_cash=pending_cash,
                data_quality=data_quality,
            ),
            data_quality=data_quality,
        )

    @staticmethod
    def _apply_filters(
        statement: Select[Any],
        filters: CfoValueFiltersRead,
    ) -> Select[Any]:
        scalar_filters = (
            (SavingsOpportunity.owner_id, filters.owner_id),
            (SavingsOpportunity.source_type, filters.source_type),
            (SavingsOpportunity.value_kind, filters.value_kind),
        )
        for column, value in scalar_filters:
            normalized = str(value or "").strip()
            if normalized:
                statement = statement.where(column == normalized)
        for key, value in (
            ("department_id", filters.department_id),
            ("project_code", filters.project_code),
            ("expense_type", filters.expense_type),
            ("supplier_id", filters.supplier_id),
            ("city", filters.city),
        ):
            normalized = str(value or "").strip()
            if normalized:
                statement = statement.where(
                    SavingsOpportunity.dimension_json[key].as_string() == normalized
                )
        return statement

    def _funnel(
        self,
        opportunities: list[SavingsOpportunity],
        *,
        pending_rows: list[tuple[SavingsRealization, SavingsOpportunity]],
        verified_rows: list[tuple[SavingsRealization, SavingsOpportunity]],
        reversal_rows: list[tuple[SavingsRealization, SavingsOpportunity]],
        estimated: MoneyMap,
        in_progress: MoneyMap,
        pending_cash: MoneyMap,
        verified_cash: MoneyMap,
        reversal_money: MoneyMap,
        rejected_or_expired: MoneyMap,
        mature_estimated: MoneyMap,
    ) -> CfoValueFunnelRead:
        rejected_count = sum(1 for item in opportunities if item.status in {"rejected", "expired"})
        rates: dict[str, Decimal | None] = {}
        for currency in sorted(set(mature_estimated) | set(verified_cash)):
            denominator = mature_estimated.get(currency, Decimal("0"))
            rates[currency] = (
                (verified_cash.get(currency, Decimal("0")) / denominator).quantize(
                    Decimal("0.0001")
                )
                if denominator > 0
                else None
            )
        return CfoValueFunnelRead(
            stages=[
                CfoValueFunnelStageRead(
                    key="estimated",
                    label="已识别预计机会",
                    count=len(opportunities),
                    values=self._money_read(estimated),
                ),
                CfoValueFunnelStageRead(
                    key="in_progress",
                    label="执行中",
                    count=sum(1 for item in opportunities if item.status == "in_progress"),
                    values=self._money_read(in_progress),
                ),
                CfoValueFunnelStageRead(
                    key="actual_pending",
                    label="实际待确认",
                    count=len(pending_rows),
                    values=self._money_read(pending_cash),
                ),
                CfoValueFunnelStageRead(
                    key="verified",
                    label="财务已确认",
                    count=sum(
                        1
                        for realization, _ in verified_rows
                        if realization.realization_type == "actual"
                    ),
                    values=self._money_read(verified_cash),
                ),
                CfoValueFunnelStageRead(
                    key="reversed",
                    label="已冲回",
                    count=len(reversal_rows),
                    values=self._money_read(reversal_money),
                ),
                CfoValueFunnelStageRead(
                    key="rejected_or_expired",
                    label="拒绝或到期",
                    count=rejected_count,
                    values=self._money_read(rejected_or_expired),
                ),
            ],
            mature_estimated_values=self._money_read(mature_estimated),
            verified_values=self._money_read(verified_cash),
            realization_rate_by_currency=rates,
        )

    def _trend(
        self,
        rows: list[tuple[SavingsRealization, SavingsOpportunity]],
        *,
        as_of: datetime,
    ) -> list[CfoValueTrendPointRead]:
        buckets: dict[tuple[str, str], dict[str, Decimal]] = defaultdict(
            lambda: {
                "verified_net": Decimal("0"),
                "actual_pending": Decimal("0"),
                "reversal": Decimal("0"),
            }
        )
        for realization, opportunity in rows:
            period = realization.realized_at.strftime("%Y-%m")
            bucket = buckets[(period, realization.reporting_currency)]
            if self._is_verified(realization, opportunity, as_of=as_of):
                bucket["verified_net"] += Decimal(realization.reporting_amount)
                if realization.realization_type == "reversal":
                    bucket["reversal"] += Decimal(realization.reporting_amount)
            elif realization.status == "pending_confirmation":
                bucket["actual_pending"] += Decimal(realization.reporting_amount)
        return [
            CfoValueTrendPointRead(
                period=period,
                currency=currency,
                **values,
            )
            for (period, currency), values in sorted(buckets.items())
        ]

    def _breakdowns(
        self,
        opportunities: list[SavingsOpportunity],
        verified_rows: list[tuple[SavingsRealization, SavingsOpportunity]],
    ) -> list[CfoValueBreakdownGroupRead]:
        verified_by_opportunity: dict[str, MoneyMap] = defaultdict(lambda: defaultdict(Decimal))
        for realization, opportunity in verified_rows:
            verified_by_opportunity[opportunity.id][realization.reporting_currency] += Decimal(
                realization.reporting_amount
            )
        dimensions = (
            ("department", "department_id", "department_name"),
            ("project", "project_code", "project_code"),
            ("expense_type", "expense_type", "expense_type"),
            ("supplier", "supplier_id", "supplier_name"),
            ("city", "city", "city"),
            ("owner", None, None),
            ("source", None, None),
        )
        groups: list[CfoValueBreakdownGroupRead] = []
        for dimension, id_key, name_key in dimensions:
            aggregate: dict[str, dict[str, Any]] = {}
            for opportunity in opportunities:
                dimension_id, dimension_name = self._dimension_value(
                    opportunity,
                    dimension=dimension,
                    id_key=id_key,
                    name_key=name_key,
                )
                if not dimension_id:
                    continue
                item = aggregate.setdefault(
                    dimension_id,
                    {
                        "name": dimension_name or dimension_id,
                        "count": 0,
                        "estimated": defaultdict(Decimal),
                        "verified": defaultdict(Decimal),
                    },
                )
                item["count"] += 1
                item["estimated"][opportunity.reporting_currency] += Decimal(
                    opportunity.estimated_net
                )
                for currency, amount in verified_by_opportunity.get(opportunity.id, {}).items():
                    item["verified"][currency] += amount
            items = [
                CfoValueBreakdownItemRead(
                    dimension=dimension,
                    dimension_id=dimension_id,
                    dimension_name=data["name"],
                    opportunity_count=data["count"],
                    verified_values=self._money_read(data["verified"]),
                    estimated_values=self._money_read(data["estimated"]),
                )
                for dimension_id, data in aggregate.items()
            ]
            items.sort(
                key=lambda item: sum(value.amount for value in item.verified_values),
                reverse=True,
            )
            groups.append(CfoValueBreakdownGroupRead(dimension=dimension, items=items[:20]))
        return groups

    def _guardrails(
        self,
        rows: list[tuple[SavingsRealization, SavingsOpportunity]],
        *,
        verified_rows: list[tuple[SavingsRealization, SavingsOpportunity]],
        reversal_rows: list[tuple[SavingsRealization, SavingsOpportunity]],
        pending_cash: MoneyMap,
        data_quality: CfoValueDataQualityRead,
    ) -> list[CfoValueGuardrailRead]:
        confirmed_actual_count = sum(
            1 for realization, _ in verified_rows if realization.realization_type == "actual"
        )
        reversal_rate = (
            (Decimal(len(reversal_rows)) / Decimal(confirmed_actual_count)).quantize(
                Decimal("0.0001")
            )
            if confirmed_actual_count
            else None
        )
        return [
            CfoValueGuardrailRead(
                key="pending_confirmation",
                label="实际结果待财务确认",
                status="attention" if data_quality.pending_confirmation_count else "ok",
                count=data_quality.pending_confirmation_count,
                values=self._money_read(pending_cash),
            ),
            CfoValueGuardrailRead(
                key="post_confirmation_reversal_rate",
                label="确认后冲回率",
                status="attention" if reversal_rows else "ok",
                count=len(reversal_rows),
                rate=reversal_rate,
            ),
            CfoValueGuardrailRead(
                key="actual_over_estimate",
                label="实际超过预计异常",
                status="attention" if data_quality.actual_over_estimate_count else "ok",
                count=data_quality.actual_over_estimate_count,
            ),
            CfoValueGuardrailRead(
                key="evidence_or_dedupe_gap",
                label="证据或去重待补齐",
                status=(
                    "attention"
                    if data_quality.missing_evidence_count or data_quality.pending_dedupe_count
                    else "ok"
                ),
                count=(data_quality.missing_evidence_count + data_quality.pending_dedupe_count),
            ),
            CfoValueGuardrailRead(
                key="confirmed_high_risk_exposure",
                label="开放且已确认的高危风险暴露",
                status="unavailable",
                reason="风险观察池尚未建立与经济义务一致的 canonical 去重键，不能汇总为金额。",
            ),
        ]

    @staticmethod
    def _data_quality(
        rows: list[tuple[SavingsRealization, SavingsOpportunity]],
    ) -> CfoValueDataQualityRead:
        pending = business_state = pending_dedupe = missing_fx = missing_evidence = over = 0
        for realization, opportunity in rows:
            pending += realization.status == "pending_confirmation"
            business_state += (
                str((realization.final_snapshot_json or {}).get("evidence_level") or "")
                == "business_state"
            )
            pending_dedupe += realization.dedupe_status == "pending_review"
            missing_fx += (
                not realization.fx_source
                or not realization.fx_rate
                or (
                    realization.original_currency != realization.reporting_currency
                    and realization.fx_source == "same_currency"
                )
            )
            missing_evidence += not list(realization.evidence_json or [])
            over += realization.realization_type == "actual" and Decimal(
                realization.actual_net
            ) > Decimal(opportunity.estimated_net)
        notes = []
        if business_state:
            notes.append("部分结果仅有平台付款业务状态，仍需独立财务确认或外部凭证。")
        if pending_dedupe:
            notes.append("待确认结果尚未完成 canonical 去重复核，不进入主 KPI。")
        return CfoValueDataQualityRead(
            pending_confirmation_count=pending,
            business_state_only_count=business_state,
            pending_dedupe_count=pending_dedupe,
            missing_fx_count=missing_fx,
            missing_evidence_count=missing_evidence,
            actual_over_estimate_count=over,
            notes=notes,
        )

    @staticmethod
    def _is_verified(
        realization: SavingsRealization,
        opportunity: SavingsOpportunity,
        *,
        as_of: datetime,
    ) -> bool:
        confirmed_at = (
            CfoValueAnalyticsService._utc(realization.confirmed_at)
            if realization.confirmed_at is not None
            else None
        )
        return bool(
            opportunity.value_kind == "cash"
            and realization.status == "finance_confirmed"
            and realization.dedupe_status == "canonical"
            and confirmed_at is not None
            and confirmed_at <= as_of
        )

    @staticmethod
    def _sum_realizations(
        rows: list[tuple[SavingsRealization, SavingsOpportunity]],
    ) -> MoneyMap:
        result: MoneyMap = defaultdict(Decimal)
        for realization, _ in rows:
            result[realization.reporting_currency] += Decimal(realization.reporting_amount)
        return dict(result)

    @staticmethod
    def _sum_opportunities(opportunities: list[SavingsOpportunity]) -> MoneyMap:
        result: MoneyMap = defaultdict(Decimal)
        for opportunity in opportunities:
            if opportunity.value_kind == "cash":
                result[opportunity.reporting_currency] += Decimal(opportunity.estimated_net)
        return dict(result)

    @staticmethod
    def _money_read(values: MoneyMap) -> list[CfoValueMoneyRead]:
        return [
            CfoValueMoneyRead(currency=currency, amount=amount.quantize(Decimal("0.0001")))
            for currency, amount in sorted(values.items())
        ]

    @staticmethod
    def _dimension_value(
        opportunity: SavingsOpportunity,
        *,
        dimension: str,
        id_key: str | None,
        name_key: str | None,
    ) -> tuple[str, str]:
        if dimension == "owner":
            return opportunity.owner_id, opportunity.owner_name
        if dimension == "source":
            return opportunity.source_type, opportunity.source_type
        dimensions = opportunity.dimension_json or {}
        dimension_id = str(dimensions.get(id_key or "") or "").strip()
        dimension_name = str(dimensions.get(name_key or "") or dimension_id).strip()
        return dimension_id, dimension_name

    @staticmethod
    def _freshness(
        opportunities: list[SavingsOpportunity],
        rows: list[tuple[SavingsRealization, SavingsOpportunity]],
    ) -> datetime | None:
        timestamps = [
            CfoValueAnalyticsService._utc(item.updated_at)
            for item in opportunities
            if item.updated_at
        ]
        timestamps.extend(
            CfoValueAnalyticsService._utc(realization.updated_at)
            for realization, _ in rows
            if realization.updated_at is not None
        )
        return max(timestamps) if timestamps else None

    @staticmethod
    def _normalize_window(
        start: datetime,
        end: datetime,
        as_of: datetime,
    ) -> tuple[datetime, datetime, datetime]:
        values = []
        for value in (start, end, as_of):
            normalized = (
                value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
            )
            values.append(normalized)
        normalized_start, normalized_end, normalized_as_of = values
        if normalized_start > normalized_end:
            raise ValueError("开始时间不能晚于结束时间。")
        if (normalized_end - normalized_start).days > 731:
            raise ValueError("单次价值看板查询时间范围不能超过 731 天。")
        return normalized_start, normalized_end, normalized_as_of

    @staticmethod
    def _utc(value: datetime) -> datetime:
        return value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
