from __future__ import annotations

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

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.savings import ProfileBaselineSnapshot
from app.schemas.savings_insights import (
    SavingsAnalysisQualityIssue,
    SavingsInsightAnalysisRead,
    SavingsInsightAnalyzeRequest,
    SavingsInsightCandidateRead,
    SavingsInsightEvidenceRead,
)
from app.services.savings_access_policy import SavingsAccessPolicy
from app.services.savings_fact_scope import (
    ExpenseSavingsFact,
    SavingsFactScopeReader,
    stable_digest,
)
from app.services.savings_insight_attribution import (
    SavingsAnomalyAttributionAnalyzer,
)
from app.services.savings_insight_budget import SavingsBudgetForecastAnalyzer
from app.services.savings_protocol import (
    SavingsRequestProtocol,
    savings_actor,
    savings_payload_fingerprint,
)


class SavingsInsightAnalysisService:
    """发现可行动费用信号；无可信反事实时明确拒绝货币化。"""

    MAX_CANDIDATES = 200

    def __init__(self, db: Session) -> None:
        self.db = db
        self.protocol = SavingsRequestProtocol(db)
        self.fact_reader = SavingsFactScopeReader(db)
        self.budget_analyzer = SavingsBudgetForecastAnalyzer(db)
        self.attribution_analyzer = SavingsAnomalyAttributionAnalyzer()

    def analyze(
        self,
        payload: SavingsInsightAnalyzeRequest,
        current_user: CurrentUserContext,
    ) -> SavingsInsightAnalysisRead:
        tenant_id = str(current_user.tenant_id or "default").strip() or "default"
        actor_id, _ = savings_actor(current_user)
        request_fingerprint = savings_payload_fingerprint(
            action="analyze_savings_insights",
            aggregate_type="insight_batch",
            aggregate_id=payload.request_id,
            actor_id=actor_id,
            payload=payload.model_dump(mode="json", exclude={"request_id"}),
        )
        now = datetime.now(UTC)
        with self.protocol.serialize(
            tenant_id=tenant_id,
            actor_id=actor_id,
            request_id=payload.request_id,
        ):
            fact_set = self.fact_reader.load_archived_facts(
                window_start=payload.window_start,
                window_end=payload.window_end,
                as_of=payload.as_of,
                current_user=current_user,
            )
            budget_candidates, budget_issues = self.budget_analyzer.analyze(
                window_start=payload.window_start,
                window_end=payload.window_end,
                as_of=payload.as_of,
                current_user=current_user,
            )
            repeated_candidates = self._repeated_small_expenses(
                fact_set.facts,
                payload,
            )
            price_candidates, baseline_issues = self._historical_price_deviations(
                tenant_id=tenant_id,
                facts=fact_set.facts,
                payload=payload,
                current_user=current_user,
            )
            attribution_candidates = self.attribution_analyzer.analyze(
                price_candidates,
                minimum_group_size=payload.minimum_repeat_count,
            )

        candidates = [
            *budget_candidates,
            *repeated_candidates,
            *price_candidates,
            *attribution_candidates,
        ]
        candidates.sort(key=lambda item: (item.insight_type, item.candidate_key))
        issues = [
            *fact_set.quality_issues,
            *budget_issues,
            *baseline_issues,
            self._supplier_coverage_issue(),
        ]
        if len(candidates) > self.MAX_CANDIDATES:
            issues.append(
                SavingsAnalysisQualityIssue(
                    code="candidate_result_truncated",
                    message="候选结果超过单次返回上限，已按稳定键截断；建议缩小分析窗口。",
                    metadata={
                        "candidate_count": len(candidates),
                        "returned_count": self.MAX_CANDIDATES,
                    },
                )
            )
            candidates = candidates[: self.MAX_CANDIDATES]
        return SavingsInsightAnalysisRead(
            request_id=payload.request_id,
            request_fingerprint=request_fingerprint,
            tenant_id=tenant_id,
            data_scope=fact_set.data_scope,
            candidates=candidates,
            quality_issues=issues,
            source_claim_count=fact_set.claim_count,
            source_item_count=fact_set.item_count,
            created_opportunity_ids=[],
            monetized_opportunity_count=0,
            generated_at=now,
        )

    def _repeated_small_expenses(
        self,
        facts: list[ExpenseSavingsFact],
        payload: SavingsInsightAnalyzeRequest,
    ) -> list[SavingsInsightCandidateRead]:
        grouped: dict[tuple[str, str, str, str, Decimal, str], list[ExpenseSavingsFact]] = (
            defaultdict(list)
        )
        for fact in facts:
            if fact.amount > payload.small_amount_threshold:
                continue
            employee = fact.dimension("employee")
            if employee is None or not fact.expense_type:
                continue
            employee_id, employee_label = employee
            grouped[
                (
                    employee_id,
                    employee_label,
                    fact.expense_type,
                    fact.city,
                    fact.amount.quantize(Decimal("0.01")),
                    fact.currency,
                )
            ].append(fact)

        candidates: list[SavingsInsightCandidateRead] = []
        for key, group in sorted(grouped.items(), key=lambda row: str(row[0])):
            distinct_claim_ids = sorted({fact.claim_id for fact in group})
            if len(distinct_claim_ids) < payload.minimum_repeat_count:
                continue
            employee_id, employee_label, expense_type, city, amount, currency = key
            material = {
                "employee_id": employee_id,
                "expense_type": expense_type,
                "city": city,
                "amount": str(amount),
                "currency": currency,
                "source_hashes": sorted(fact.content_hash for fact in group),
            }
            candidate_key = f"repeated-small:{stable_digest(material)}"
            observed_spend = sum((fact.amount for fact in group), Decimal("0.0000"))
            evidence = [self._fact_evidence(fact, "repeated_expense_fact") for fact in group[:100]]
            quality_issues = [
                SavingsAnalysisQualityIssue(
                    code="procurement_counterfactual_missing",
                    message=(
                        "重复发生只能证明支出模式，不能证明浪费；缺少协议价、合并采购报价或政策上限，"
                        "因此不生成节省金额。"
                    ),
                    metadata={"distinct_claim_count": len(distinct_claim_ids)},
                )
            ]
            if len(group) > 100:
                quality_issues.append(
                    SavingsAnalysisQualityIssue(
                        code="candidate_evidence_truncated",
                        message="候选证据列表只返回前 100 条，完整来源仍包含在稳定指纹中。",
                        metadata={"source_count": len(group)},
                    )
                )
            candidates.append(
                SavingsInsightCandidateRead(
                    candidate_key=candidate_key,
                    insight_type="repeated_small_expense_pattern",
                    title=f"{employee_label} 的重复小额 {expense_type} 支出",
                    description=(
                        f"窗口内有 {len(distinct_claim_ids)} 张不同单据出现相同金额 {amount} "
                        f"{currency} 的 {expense_type} 支出，建议核查是否可合并采购或使用协议渠道。"
                    ),
                    dimension_json={
                        "employee_id": employee_id,
                        "employee_name": employee_label,
                        "expense_type": expense_type,
                        "city": city,
                        "amount": str(amount),
                        "distinct_claim_count": len(distinct_claim_ids),
                    },
                    evidence=evidence,
                    evidence_sufficient_for_signal=True,
                    data_quality_status=(
                        "complete"
                        if all(fact.source_level == "claim_item" for fact in group)
                        else "partial"
                    ),
                    quality_issues=quality_issues,
                    exposure_amount=observed_spend.quantize(Decimal("0.0001")),
                    exposure_meaning="observed_repeated_spend_not_savings",
                    currency=currency,
                    estimated_savings=None,
                    monetization_status="withheld_no_counterfactual",
                )
            )
        return candidates

    def _historical_price_deviations(
        self,
        *,
        tenant_id: str,
        facts: list[ExpenseSavingsFact],
        payload: SavingsInsightAnalyzeRequest,
        current_user: CurrentUserContext,
    ) -> tuple[list[SavingsInsightCandidateRead], list[SavingsAnalysisQualityIssue]]:
        if not SavingsAccessPolicy.can_read_tenant_value(current_user):
            return [], [
                SavingsAnalysisQualityIssue(
                    code="historical_baseline_scope_unverifiable",
                    message=(
                        "历史费用类型/城市基线没有记录部门范围，不能向仅部门权限用户安全披露。"
                    ),
                    severity="error",
                )
            ]
        baselines = list(
            self.db.scalars(
                select(ProfileBaselineSnapshot)
                .where(
                    ProfileBaselineSnapshot.tenant_id == tenant_id,
                    ProfileBaselineSnapshot.baseline_type == "historical_cohort",
                    ProfileBaselineSnapshot.metric_key == "median_expense_fact_amount",
                    ProfileBaselineSnapshot.method
                    == "median_archived_expense_facts_tenant_scope",
                    ProfileBaselineSnapshot.dimension_type.in_(("expense_type", "city")),
                    ProfileBaselineSnapshot.data_quality_status.in_(("complete", "partial")),
                    ProfileBaselineSnapshot.window_end < payload.window_start,
                    ProfileBaselineSnapshot.frozen_at <= payload.as_of,
                )
                .order_by(
                    ProfileBaselineSnapshot.window_end.desc(),
                    ProfileBaselineSnapshot.frozen_at.desc(),
                    ProfileBaselineSnapshot.id.desc(),
                )
            ).all()
        )
        baselines = [
            baseline
            for baseline in baselines
            if not any(
                isinstance(issue, dict)
                and issue.get("code") == "baseline_contains_claim_fallback"
                for issue in list(baseline.quality_issues_json or [])
            )
        ]
        if not baselines:
            return [], [
                SavingsAnalysisQualityIssue(
                    code="historical_baseline_unavailable",
                    message="观察窗口之前没有质量合格的费用类型或城市历史基线。",
                )
            ]
        by_dimension: dict[tuple[str, str, str], ProfileBaselineSnapshot] = {}
        for baseline in baselines:
            key = (
                str(baseline.dimension_type),
                str(baseline.dimension_id),
                str(baseline.original_currency or ""),
            )
            by_dimension.setdefault(key, baseline)

        candidates: list[SavingsInsightCandidateRead] = []
        for fact in facts:
            matches: list[ProfileBaselineSnapshot] = []
            for dimension_type in ("expense_type", "city"):
                dimension = fact.dimension(dimension_type)
                if dimension is None:
                    continue
                baseline = by_dimension.get(
                    (dimension_type, dimension[0], fact.currency)
                )
                if baseline is not None:
                    matches.append(baseline)
            if not matches:
                continue
            baseline = max(matches, key=lambda row: int(row.sample_count or 0))
            baseline_value = Decimal(baseline.baseline_value or 0)
            if baseline_value <= 0:
                continue
            ratio = fact.amount / baseline_value
            if ratio < payload.price_deviation_ratio:
                continue
            delta = (fact.amount - baseline_value).quantize(Decimal("0.0001"))
            material = {
                "baseline_id": baseline.id,
                "fact_hash": fact.content_hash,
                "ratio": str(payload.price_deviation_ratio),
            }
            candidates.append(
                SavingsInsightCandidateRead(
                    candidate_key=f"historical-price:{stable_digest(material)}",
                    insight_type="historical_price_deviation",
                    title=f"单据 {fact.claim_no} 的费用金额高于历史中位数",
                    description=(
                        f"当前金额 {fact.amount} {fact.currency}，历史中位数为 "
                        f"{baseline_value} {fact.currency}。差额是偏差信号，"
                        "不是已证明的可节省金额。"
                    ),
                    dimension_json={
                        "claim_id": fact.claim_id,
                        "claim_item_id": fact.item_id or "",
                        "expense_type": fact.expense_type,
                        "city": fact.city,
                        "department_id": fact.department_id,
                        "department_name": fact.department_name,
                        "project_code": fact.project_code,
                        "baseline_dimension_type": baseline.dimension_type,
                        "baseline_dimension_id": baseline.dimension_id,
                        "deviation_ratio": str(ratio.quantize(Decimal("0.0001"))),
                    },
                    evidence=[
                        self._fact_evidence(fact, "observed_expense_fact"),
                        SavingsInsightEvidenceRead(
                            evidence_role="historical_baseline",
                            resource_type="profile_baseline_snapshot",
                            resource_id=baseline.id,
                            content_hash=baseline.query_fingerprint,
                            occurred_at=baseline.frozen_at,
                            metadata={
                                "sample_count": baseline.sample_count,
                                "quality_status": baseline.data_quality_status,
                            },
                        ),
                    ],
                    evidence_sufficient_for_signal=True,
                    data_quality_status="partial",
                    quality_issues=[
                        SavingsAnalysisQualityIssue(
                            code="unit_supplier_comparability_missing",
                            message=(
                                "当前模型缺少数量、单位和核验供应商，不能证明与历史样本同质，"
                                "因此不生成节省金额。"
                            ),
                            dimension_type=str(baseline.dimension_type),
                            dimension_id=str(baseline.dimension_id),
                        )
                    ],
                    exposure_amount=delta,
                    exposure_meaning="observed_amount_above_historical_median",
                    currency=fact.currency,
                    estimated_savings=None,
                    monetization_status="withheld_no_counterfactual",
                    baseline_snapshot_id=baseline.id,
                )
            )
        return candidates, []

    @staticmethod
    def _fact_evidence(
        fact: ExpenseSavingsFact,
        role: str,
    ) -> SavingsInsightEvidenceRead:
        return SavingsInsightEvidenceRead(
            evidence_role=role,
            resource_type=("expense_claim_item" if fact.item_id else "expense_claim"),
            resource_id=fact.source_id,
            content_hash=fact.content_hash,
            occurred_at=fact.occurred_at,
            metadata={
                "claim_id": fact.claim_id,
                "claim_no": fact.claim_no,
                "amount": str(fact.amount),
                "currency": fact.currency,
            },
        )

    @staticmethod
    def _supplier_coverage_issue() -> SavingsAnalysisQualityIssue:
        return SavingsAnalysisQualityIssue(
            code="supplier_price_drift_unavailable",
            message=(
                "费用事实没有核验供应商、商品数量和单位价格，供应商价格漂移分析保持不可用，"
                "不会推算节省金额。"
            ),
            severity="error",
            dimension_type="supplier",
        )
