from __future__ import annotations

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

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.commercial import (
    CommercialCostEvent,
    CommercialEntitlement,
    TenantCommercialPlan,
    TenantSubscription,
    UsageMeterEvent,
)
from app.models.commercial_billing import CommercialBillingPeriod
from app.models.savings import SavingsOpportunity, SavingsRealization
from app.schemas.commercial import (
    CommercialAnalyticsRead,
    CommercialMetricRead,
    CommercialMoneyRead,
    CommercialRatioRead,
)

MoneyMap = dict[str, Decimal]
FOUR_PLACES = Decimal("0.0001")
SIX_PLACES = Decimal("0.000001")


class CommercialAnalyticsService:
    """平台商业账与客户价值账分离聚合，不做隐式汇率换算。"""

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

    def build(
        self,
        tenant_id: str,
        *,
        start: datetime,
        end: datetime,
        as_of: datetime,
    ) -> CommercialAnalyticsRead:
        start, end, as_of = self._normalize_window(start, end, as_of)
        effective_end = min(end, as_of)
        subscriptions = list(
            self.db.scalars(
                select(TenantSubscription).where(
                    TenantSubscription.tenant_id == tenant_id,
                )
            ).all()
        )
        billing_periods = list(
            self.db.scalars(
                select(CommercialBillingPeriod).where(
                    CommercialBillingPeriod.tenant_id == tenant_id,
                    CommercialBillingPeriod.period_start < effective_end,
                    CommercialBillingPeriod.period_end > start,
                )
            ).all()
        )
        billing_period_by_id = {item.id: item for item in billing_periods}
        plans = list(
            self.db.scalars(
                select(TenantCommercialPlan).where(
                    TenantCommercialPlan.tenant_id == tenant_id,
                )
            ).all()
        )
        entitlements = list(
            self.db.scalars(
                select(CommercialEntitlement).where(
                    CommercialEntitlement.tenant_id == tenant_id,
                )
            ).all()
        )
        entitlement_by_id = {item.id: item for item in entitlements}

        charge_values, charge_basis_count, charge_issues = self._customer_charges(
            tenant_id,
            start=start,
            end=effective_end,
            billing_periods=billing_periods,
            billing_period_by_id=billing_period_by_id,
            entitlement_by_id=entitlement_by_id,
        )
        cost_values = self._internal_costs(
            tenant_id,
            start=start,
            end=effective_end,
        )
        savings_values = self._verified_savings(
            tenant_id,
            start=start,
            end=effective_end,
            as_of=as_of,
        )
        labor_values, labor_issues = self._labor_value(plans, start=start, end=effective_end)

        customer_charges = self._charge_metric(
            charge_values,
            charge_basis_count=charge_basis_count,
            issues=charge_issues,
        )
        internal_costs = self._cost_metric(cost_values)
        verified_cash = self._savings_metric(savings_values)
        contribution = self._contribution_metric(
            charge_values,
            cost_values,
            charge_available=charge_basis_count > 0,
            charge_issues=charge_issues,
        )
        customer_roi = self._roi_metric(
            savings_values,
            charge_values,
            charge_available=charge_basis_count > 0,
            labor_available=bool(labor_values),
        )
        labor = self._labor_metric(labor_values, labor_issues)

        issues = list(dict.fromkeys(charge_issues + labor_issues))
        if not cost_values:
            issues.append("查询窗口内缺少平台内部成本事件。")
        if not savings_values:
            issues.append("查询窗口内缺少 canonical 且财务确认的现金节省事实。")
        if not subscriptions:
            issues.append("租户尚未配置商业订阅。")
        has_any = bool(charge_basis_count or cost_values or savings_values or labor_values)
        data_quality = "partial" if has_any else "unavailable"
        return CommercialAnalyticsRead(
            tenant_id=tenant_id,
            start=start,
            end=end,
            as_of=as_of,
            generated_at=datetime.now(UTC),
            customer_charges=customer_charges,
            internal_costs=internal_costs,
            contribution_margin=contribution,
            verified_cash_savings=verified_cash,
            customer_roi=customer_roi,
            customer_labor_value=labor,
            data_quality_status=data_quality,
            data_quality_issues=list(dict.fromkeys(issues)),
        )

    def _customer_charges(
        self,
        tenant_id: str,
        *,
        start: datetime,
        end: datetime,
        billing_periods: list[CommercialBillingPeriod],
        billing_period_by_id: dict[str, CommercialBillingPeriod],
        entitlement_by_id: dict[str, CommercialEntitlement],
    ) -> tuple[MoneyMap, int, list[str]]:
        values: dict[str, Decimal] = defaultdict(Decimal)
        basis_count = 0
        issues = ["缺少发票、贷项通知单和收款事实；客户收费仅为合同计费代理值。"]
        for period in billing_periods:
            period_start = _utc(period.period_start)
            if start <= period_start < end:
                values[period.currency] += Decimal(period.base_fee_snapshot)
                basis_count += 1

        usage_events = list(
            self.db.scalars(
                select(UsageMeterEvent).where(
                    UsageMeterEvent.tenant_id == tenant_id,
                    UsageMeterEvent.occurred_at < end,
                )
            ).all()
        )
        grouped: dict[tuple[str, str, str], list[UsageMeterEvent]] = defaultdict(list)
        quota_groups: dict[tuple[str, str], list[UsageMeterEvent]] = defaultdict(list)
        for event in usage_events:
            grouped[(event.entitlement_id, event.billing_period_id, event.quota_period_key)].append(
                event
            )
            quota_groups[(event.entitlement_id, event.quota_period_key)].append(event)
        for (entitlement_id, billing_period_id, quota_key), events in grouped.items():
            entitlement = entitlement_by_id.get(entitlement_id)
            if entitlement is None:
                issues.append("存在无法关联权益配置的用量事件，未计入合同收费代理值。")
                continue
            period = billing_period_by_id.get(billing_period_id)
            if period is None:
                issues.append("存在无法关联不可变账期的用量事件，未计入合同收费代理值。")
                continue
            if period.pricing_model_snapshot not in {"usage", "hybrid", "custom"}:
                continue
            segment_start = max(start, _utc(period.period_start))
            segment_end = min(end, _utc(period.period_end))
            window_events = [
                event for event in events if segment_start <= _utc(event.occurred_at) < segment_end
            ]
            if not window_events:
                continue
            config = entitlement.config_json or {}
            unit_price = _decimal_or_none(config.get("billable_unit_price"))
            billing_currency = str(config.get("billing_currency") or "").strip().upper()
            billing_mode = str(config.get("billing_mode") or "").strip().casefold()
            if unit_price is None or unit_price < 0 or billing_currency != period.currency:
                issues.append(
                    f"权益 {entitlement.entitlement_key} 缺少可信同币种计费单价，"
                    "对应使用量未计入客户收费。"
                )
                continue
            if billing_mode not in {"all_usage", "overage"}:
                issues.append(
                    f"权益 {entitlement.entitlement_key} 未配置 billing_mode，"
                    "对应使用量未计入客户收费。"
                )
                continue
            if billing_mode == "all_usage":
                billable_quantity = sum(
                    (Decimal(event.quantity) for event in window_events),
                    Decimal("0"),
                )
            else:
                included = Decimal(entitlement.included_quantity or 0)
                quota_events = quota_groups[(entitlement_id, quota_key)]
                before = sum(
                    (
                        Decimal(event.quantity)
                        for event in quota_events
                        if _utc(event.occurred_at) < segment_start
                    ),
                    Decimal("0"),
                )
                through_end = sum(
                    (
                        Decimal(event.quantity)
                        for event in quota_events
                        if _utc(event.occurred_at) < segment_end
                    ),
                    Decimal("0"),
                )
                billable_quantity = max(Decimal("0"), through_end - included) - max(
                    Decimal("0"), before - included
                )
            values[billing_currency] += (billable_quantity * unit_price).quantize(
                FOUR_PLACES,
                rounding=ROUND_HALF_UP,
            )
            basis_count += 1
        return dict(values), basis_count, list(dict.fromkeys(issues))

    def _internal_costs(self, tenant_id: str, *, start: datetime, end: datetime) -> MoneyMap:
        values: dict[str, Decimal] = defaultdict(Decimal)
        rows = self.db.scalars(
            select(CommercialCostEvent).where(
                CommercialCostEvent.tenant_id == tenant_id,
                CommercialCostEvent.occurred_at >= start,
                CommercialCostEvent.occurred_at < end,
            )
        ).all()
        for row in rows:
            values[row.reporting_currency] += Decimal(row.reporting_amount)
        return dict(values)

    def _verified_savings(
        self,
        tenant_id: str,
        *,
        start: datetime,
        end: datetime,
        as_of: datetime,
    ) -> MoneyMap:
        values: dict[str, Decimal] = defaultdict(Decimal)
        rows = self.db.execute(
            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.status == "finance_confirmed",
                SavingsRealization.dedupe_status == "canonical",
                SavingsRealization.confirmed_at.is_not(None),
                SavingsRealization.confirmed_at <= as_of,
                SavingsOpportunity.value_kind == "cash",
            )
        ).all()
        for realization, _ in rows:
            values[realization.reporting_currency] += Decimal(realization.reporting_amount)
        return dict(values)

    @staticmethod
    def _labor_value(
        plans: list[TenantCommercialPlan],
        *,
        start: datetime,
        end: datetime,
    ) -> tuple[MoneyMap, list[str]]:
        values: dict[str, Decimal] = defaultdict(Decimal)
        issues: list[str] = []
        relevant = [
            plan
            for plan in plans
            if plan.status == "active"
            and _utc(plan.effective_from) < end
            and (plan.effective_to is None or _utc(plan.effective_to) >= start)
        ]
        for plan in relevant:
            baseline = (plan.contract_terms_json or {}).get("customer_labor_baseline")
            if not isinstance(baseline, dict):
                continue
            currency = str(baseline.get("currency") or "").strip().upper()
            released_hours = _decimal_or_none(baseline.get("released_hours"))
            hourly_cost = _decimal_or_none(baseline.get("fully_loaded_hourly_cost"))
            approved_by = str(baseline.get("approved_by") or "").strip()
            verified_at = str(baseline.get("verified_at") or "").strip()
            if (
                len(currency) != 3
                or released_hours is None
                or released_hours < 0
                or hourly_cost is None
                or hourly_cost < 0
                or not approved_by
                or not verified_at
            ):
                issues.append("客户工时基线字段不完整，未计入工时价值。")
                continue
            values[currency] += (released_hours * hourly_cost).quantize(
                FOUR_PLACES,
                rounding=ROUND_HALF_UP,
            )
        if not values:
            issues.append("缺少客户批准的工时基线、完全成本和可释放工时。")
        return dict(values), issues

    @staticmethod
    def _charge_metric(
        values: MoneyMap,
        *,
        charge_basis_count: int,
        issues: list[str],
    ) -> CommercialMetricRead:
        if not charge_basis_count:
            return _metric(
                "customer_charges",
                "客户合同收费",
                "unavailable",
                {},
                "查询窗口内没有可归属的合同周期收费或可信用量计价配置。",
                required=["账单/发票事实", "用量计价规则", "贷项通知单"],
                notes=issues,
                basis="contract_charge_proxy",
            )
        return _metric(
            "customer_charges",
            "客户合同收费",
            "partial",
            values,
            "金额来自订阅快照和管理员维护的用量计价规则，不代表已开票或已收款收入。",
            required=["账单/发票事实", "收款回执"],
            notes=issues,
            basis="contract_charge_proxy",
        )

    @staticmethod
    def _cost_metric(values: MoneyMap) -> CommercialMetricRead:
        if not values:
            return _metric(
                "internal_costs",
                "平台内部成本",
                "unavailable",
                {},
                "查询窗口内没有内部成本事件，不能用 0 代替未知成本。",
                required=["AI/OCR/基础设施/支持等成本事件"],
                basis="internal_cost_ledger",
            )
        return _metric(
            "internal_costs",
            "平台内部成本",
            "available",
            values,
            "金额来自独立的内部成本事件台账。",
            basis="internal_cost_ledger",
        )

    @staticmethod
    def _savings_metric(values: MoneyMap) -> CommercialMetricRead:
        if not values:
            return _metric(
                "verified_cash_savings",
                "客户财务确认现金节省",
                "unavailable",
                {},
                "查询窗口内没有 canonical 且财务确认的现金节省事实。",
                required=["Savings Ledger 财务确认实际结果"],
                basis="verified_savings_ledger",
            )
        return _metric(
            "verified_cash_savings",
            "客户财务确认现金节省",
            "available",
            values,
            "仅包含 Savings Ledger 的 canonical、finance_confirmed 现金结果。",
            basis="verified_savings_ledger",
        )

    @staticmethod
    def _contribution_metric(
        charges: MoneyMap,
        costs: MoneyMap,
        *,
        charge_available: bool,
        charge_issues: list[str],
    ) -> CommercialMetricRead:
        common = sorted(set(charges) & set(costs))
        if not charge_available or not costs or not common:
            return _metric(
                "contribution_margin",
                "贡献毛利",
                "unavailable",
                {},
                "收费与成本缺失或币种不匹配，不能跨币种相减。",
                required=["同币种收入事实", "同币种内部成本事实"],
                notes=charge_issues,
                basis="contract_contribution_proxy",
            )
        values = {currency: charges[currency] - costs[currency] for currency in common}
        unmatched = sorted(set(charges) ^ set(costs))
        notes = list(charge_issues)
        if unmatched:
            notes.append("部分币种只有收费或成本，未计算对应贡献毛利。")
        return _metric(
            "contribution_margin",
            "贡献毛利",
            "partial",
            values,
            "以合同收费代理值减内部成本；缺少收入确认事实，不能视为会计毛利。",
            required=["收入确认事实"],
            notes=notes,
            basis="contract_contribution_proxy",
        )

    @staticmethod
    def _roi_metric(
        savings: MoneyMap,
        charges: MoneyMap,
        *,
        charge_available: bool,
        labor_available: bool,
    ) -> CommercialMetricRead:
        common = [
            currency for currency in sorted(set(savings) & set(charges)) if charges[currency] > 0
        ]
        if not savings or not charge_available or not common:
            return CommercialMetricRead(
                key="customer_roi",
                label="客户现金 ROI",
                status="unavailable",
                reason="缺少同币种正向客户收费或财务确认现金节省，不能计算 ROI。",
                required_inputs=["同币种客户收费", "财务确认现金节省"],
                notes=["不同币种不会被合并或换算。"],
            )
        ratios = [
            CommercialRatioRead(
                currency=currency,
                ratio=((savings[currency] - charges[currency]) / charges[currency]).quantize(
                    SIX_PLACES,
                    rounding=ROUND_HALF_UP,
                ),
                numerator=(savings[currency] - charges[currency]).quantize(FOUR_PLACES),
                denominator=charges[currency].quantize(FOUR_PLACES),
            )
            for currency in common
        ]
        notes = ["ROI 按币种分别计算，未做隐式汇率换算。"]
        if not labor_available:
            notes.append("客户工时价值缺失，因此现金 ROI 未叠加任何推测工时收益。")
        return CommercialMetricRead(
            key="customer_roi",
            label="客户现金 ROI",
            status="partial",
            ratios=ratios,
            reason=(
                "按（财务确认现金节省－客户合同收费代理值）/ 客户合同收费代理值计算；"
                "缺少已开票/收款事实。"
            ),
            required_inputs=["账单/收款事实"],
            notes=notes,
        )

    @staticmethod
    def _labor_metric(values: MoneyMap, issues: list[str]) -> CommercialMetricRead:
        if not values:
            return _metric(
                "customer_labor_value",
                "客户认可工时价值",
                "unavailable",
                {},
                "缺少客户批准的工时基线，不能将假设工时折算为价值。",
                required=["基线工时", "完全成本", "可释放工时", "客户批准人"],
                notes=issues,
                basis="customer_approved_labor_baseline",
            )
        return _metric(
            "customer_labor_value",
            "客户认可工时价值",
            "partial",
            values,
            "来自版本化合同条款中的客户批准工时基线，尚非逐期工时事实台账。",
            required=["逐期活跃工时事实"],
            notes=issues,
            basis="customer_approved_labor_baseline",
        )

    @staticmethod
    def _normalize_window(
        start: datetime,
        end: datetime,
        as_of: datetime,
    ) -> tuple[datetime, datetime, datetime]:
        for item in (start, end, as_of):
            if item.tzinfo is None or item.utcoffset() is None:
                raise ValueError("商业分析时间必须显式包含时区。")
        normalized = tuple(_utc(item) for item in (start, end, as_of))
        normalized_start, normalized_end, normalized_as_of = normalized
        if normalized_start >= normalized_end:
            raise ValueError("商业分析开始时间必须早于结束时间。")
        if normalized_as_of < normalized_start:
            raise ValueError("商业分析截止时间不能早于开始时间。")
        if (normalized_end - normalized_start).days > 731:
            raise ValueError("单次商业分析时间范围不能超过 731 天。")
        return normalized_start, normalized_end, normalized_as_of


def _metric(
    key: str,
    label: str,
    status: str,
    values: MoneyMap,
    reason: str,
    *,
    required: list[str] | None = None,
    notes: list[str] | None = None,
    basis: str,
) -> CommercialMetricRead:
    return CommercialMetricRead(
        key=key,
        label=label,
        status=status,  # type: ignore[arg-type]
        values=[
            CommercialMoneyRead(
                currency=currency,
                amount=amount.quantize(FOUR_PLACES, rounding=ROUND_HALF_UP),
                basis=basis,
            )
            for currency, amount in sorted(values.items())
        ],
        reason=reason,
        required_inputs=list(required or []),
        notes=list(notes or []),
    )


def _decimal_or_none(value: Any) -> Decimal | None:
    if value is None or isinstance(value, bool):
        return None
    try:
        return Decimal(str(value))
    except (InvalidOperation, ValueError):
        return None


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