from __future__ import annotations

from collections import defaultdict
from decimal import Decimal

from app.schemas.savings_insights import (
    SavingsAnalysisQualityIssue,
    SavingsInsightCandidateRead,
    SavingsInsightEvidenceRead,
)
from app.services.savings_fact_scope import stable_digest


class SavingsAnomalyAttributionAnalyzer:
    """把异常集中度变成描述性归因与政策模拟准备项，不声称因果或节省。"""

    DRIVER_FIELDS = (
        ("department", "department_id", "department_name"),
        ("expense_type", "expense_type", "expense_type"),
        ("city", "city", "city"),
        ("project", "project_code", "project_code"),
    )

    def analyze(
        self,
        price_deviations: list[SavingsInsightCandidateRead],
        *,
        minimum_group_size: int,
    ) -> list[SavingsInsightCandidateRead]:
        attributions = self._attributions(
            price_deviations,
            minimum_group_size=minimum_group_size,
        )
        simulations = [
            self._policy_simulation_candidate(candidate)
            for candidate in attributions
            if candidate.dimension_json.get("driver_type") in {"expense_type", "city"}
        ]
        return [*attributions, *simulations]

    def _attributions(
        self,
        candidates: list[SavingsInsightCandidateRead],
        *,
        minimum_group_size: int,
    ) -> list[SavingsInsightCandidateRead]:
        grouped: dict[
            tuple[str, str, str, str],
            list[SavingsInsightCandidateRead],
        ] = defaultdict(list)
        for candidate in candidates:
            dimensions = candidate.dimension_json
            currency = str(candidate.currency or "").strip().upper()
            claim_id = str(dimensions.get("claim_id") or "").strip()
            if not currency or not claim_id:
                continue
            for driver_type, identity_key, label_key in self.DRIVER_FIELDS:
                identity = str(dimensions.get(identity_key) or "").strip()
                label = str(dimensions.get(label_key) or identity).strip()
                if not identity and not label:
                    continue
                if not identity:
                    identity = f"name:{label.casefold()}"
                grouped[(driver_type, identity, label or identity, currency)].append(
                    candidate
                )

        results: list[SavingsInsightCandidateRead] = []
        for (driver_type, driver_id, driver_label, currency), group in sorted(
            grouped.items(),
            key=lambda item: item[0],
        ):
            claim_ids = sorted(
                {
                    str(candidate.dimension_json.get("claim_id") or "").strip()
                    for candidate in group
                    if str(candidate.dimension_json.get("claim_id") or "").strip()
                }
            )
            if len(claim_ids) < minimum_group_size:
                continue
            source_keys = sorted(candidate.candidate_key for candidate in group)
            evidence = self._deduplicated_evidence(group)
            exposure = sum(
                (
                    Decimal(candidate.exposure_amount)
                    for candidate in group
                    if candidate.exposure_amount is not None
                ),
                Decimal("0.0000"),
            ).quantize(Decimal("0.0001"))
            quality_issues = [
                SavingsAnalysisQualityIssue(
                    code="descriptive_attribution_not_causal",
                    message=(
                        "该归因只证明异常在此维度集中，不证明该维度导致支出，也不能直接作为节省金额。"
                    ),
                    dimension_type=driver_type,
                    dimension_id=driver_id,
                    metadata={"distinct_claim_count": len(claim_ids)},
                )
            ]
            if len(evidence) == 100 and sum(len(item.evidence) for item in group) > 100:
                quality_issues.append(
                    SavingsAnalysisQualityIssue(
                        code="candidate_evidence_truncated",
                        message="归因候选只返回前 100 条去重证据，完整来源保留在稳定指纹中。",
                        metadata={"source_candidate_count": len(group)},
                    )
                )
            material = {
                "driver_type": driver_type,
                "driver_id": driver_id,
                "currency": currency,
                "source_candidate_keys": source_keys,
            }
            results.append(
                SavingsInsightCandidateRead(
                    candidate_key=f"anomaly-attribution:{stable_digest(material)}",
                    insight_type="anomaly_driver_attribution",
                    title=f"{driver_label} 的费用异常集中度需要复核",
                    description=(
                        f"{len(claim_ids)} 张不同单据在 {driver_type}={driver_label} 维度出现"
                        f"高于历史中位数的金额，合计偏差暴露 {exposure} {currency}。"
                        "这是描述性集中信号，不是因果结论或可确认节省。"
                    ),
                    dimension_json={
                        "driver_type": driver_type,
                        "driver_id": driver_id,
                        "driver_label": driver_label,
                        "distinct_claim_count": len(claim_ids),
                        "claim_ids": claim_ids[:100],
                        "source_candidate_count": len(source_keys),
                        "source_candidate_keys": source_keys[:100],
                        "source_candidate_keys_truncated": len(source_keys) > 100,
                        "attribution_kind": "descriptive_concentration_not_causal",
                        "recommended_action": (
                            "由费用治理负责人核验业务必要性、同质性和适用政策，再决定是否运行政策模拟。"
                        ),
                    },
                    evidence=evidence,
                    evidence_sufficient_for_signal=True,
                    data_quality_status="partial",
                    quality_issues=quality_issues,
                    exposure_amount=exposure,
                    exposure_meaning="aggregated_historical_deviation_not_savings",
                    currency=currency,
                    estimated_savings=None,
                    monetization_status="withheld_no_counterfactual",
                )
            )
        return results

    @staticmethod
    def _deduplicated_evidence(
        candidates: list[SavingsInsightCandidateRead],
    ) -> list[SavingsInsightEvidenceRead]:
        unique: dict[tuple[str, str, str, str], SavingsInsightEvidenceRead] = {}
        for candidate in candidates:
            for evidence in candidate.evidence:
                key = (
                    evidence.evidence_role,
                    evidence.resource_type,
                    evidence.resource_id,
                    evidence.content_hash,
                )
                unique.setdefault(key, evidence)
        return [unique[key] for key in sorted(unique)[:100]]

    @staticmethod
    def _policy_simulation_candidate(
        attribution: SavingsInsightCandidateRead,
    ) -> SavingsInsightCandidateRead:
        dimensions = dict(attribution.dimension_json)
        driver_type = str(dimensions.get("driver_type") or "")
        driver_id = str(dimensions.get("driver_id") or "")
        driver_label = str(dimensions.get("driver_label") or driver_id)
        material = {
            "attribution_candidate_key": attribution.candidate_key,
            "policy_scope": {"dimension_type": driver_type, "dimension_id": driver_id},
            "required_counterfactual": "versioned_policy_limit",
        }
        return SavingsInsightCandidateRead(
            candidate_key=f"policy-simulation:{stable_digest(material)}",
            insight_type="policy_simulation_candidate",
            title=f"为 {driver_label} 准备版本化政策模拟",
            description=(
                "已有异常集中证据，可先创建带生效期、适用范围和审批人的政策草案并运行只读模拟；"
                "当前没有核验政策反事实，因此不估算节省、不自动创建机会。"
            ),
            dimension_json={
                "driver_type": driver_type,
                "driver_id": driver_id,
                "driver_label": driver_label,
                "source_attribution_candidate_key": attribution.candidate_key,
                "simulation_action": "run_versioned_policy_counterfactual",
                "required_inputs": [
                    "published_policy_version",
                    "effective_period",
                    "eligible_expense_scope",
                    "approved_limit_or_rate",
                    "exception_and_appeal_rules",
                ],
                "write_mode": "read_only_no_opportunity_creation",
            },
            evidence=list(attribution.evidence),
            evidence_sufficient_for_signal=True,
            data_quality_status="partial",
            quality_issues=[
                SavingsAnalysisQualityIssue(
                    code="versioned_policy_counterfactual_required",
                    message=(
                        "历史中位数不是政策反事实；只有正式版本化政策、适用期和目标口径齐全后"
                        "才能计算模拟结果。"
                    ),
                    dimension_type=driver_type,
                    dimension_id=driver_id,
                ),
                SavingsAnalysisQualityIssue(
                    code="policy_simulation_does_not_create_opportunity",
                    message="政策模拟候选保持只读，需业务审批和反事实核验后另行发现机会。",
                    severity="info",
                ),
            ],
            exposure_amount=attribution.exposure_amount,
            exposure_meaning="observed_anomaly_exposure_for_simulation_not_savings",
            currency=attribution.currency,
            estimated_savings=None,
            monetization_status="withheld_no_counterfactual",
        )
