from __future__ import annotations

import uuid
from collections import defaultdict
from datetime import UTC, datetime
from decimal import Decimal
from statistics import median

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.savings import (
    ProfileBaselineSnapshot,
    SavingsEvent,
    SavingsEvidenceLink,
)
from app.schemas.savings import ProfileBaselineSnapshotRead
from app.schemas.savings_insights import (
    SavingsAnalysisQualityIssue,
    SavingsBaselineGenerateRequest,
    SavingsBaselineGenerationRead,
)
from app.services.savings_fact_scope import (
    ExpenseSavingsFact,
    ExpenseWorkflowCycleFact,
    SavingsFactScopeReader,
    stable_digest,
)
from app.services.savings_protocol import (
    SavingsRequestProtocol,
    savings_actor,
    savings_payload_fingerprint,
)


class SavingsBaselineGenerationService:
    """从租户隔离的已归档费用事实冻结可重放历史基线。"""

    ALGORITHM_VERSION = "archived-expense-median-v1"
    WORKFLOW_ALGORITHM_VERSION = "completed-workflow-elapsed-median-v1"

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

    def generate(
        self,
        payload: SavingsBaselineGenerateRequest,
        current_user: CurrentUserContext,
    ) -> SavingsBaselineGenerationRead:
        tenant_id = str(current_user.tenant_id or "default").strip() or "default"
        actor_id, actor_name = savings_actor(current_user)
        request_fingerprint = self._request_fingerprint(payload, actor_id)
        now = datetime.now(UTC)
        try:
            with self.protocol.serialize(
                tenant_id=tenant_id,
                actor_id="baseline-generator",
                request_id=request_fingerprint,
            ):
                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,
                    include_workflow_cycles="workflow" in payload.dimensions,
                )
                snapshots, issues, existing_count = self._freeze_dimensions(
                    payload=payload,
                    tenant_id=tenant_id,
                    actor_id=actor_id,
                    actor_name=actor_name,
                    request_fingerprint=request_fingerprint,
                    facts=fact_set.facts,
                    workflow_facts=fact_set.workflow_cycle_facts,
                    data_scope=fact_set.data_scope,
                    now=now,
                )
                self.db.commit()
        except Exception:
            self.db.rollback()
            raise

        return SavingsBaselineGenerationRead(
            request_id=payload.request_id,
            request_fingerprint=request_fingerprint,
            tenant_id=tenant_id,
            data_scope=fact_set.data_scope,
            snapshots=[ProfileBaselineSnapshotRead.model_validate(row) for row in snapshots],
            quality_issues=[*fact_set.quality_issues, *issues],
            source_claim_count=fact_set.claim_count,
            source_item_count=fact_set.item_count,
            source_workflow_cycle_count=len(fact_set.workflow_cycle_facts),
            replayed=bool(snapshots) and existing_count == len(snapshots),
            generated_at=now,
        )

    def _freeze_dimensions(
        self,
        *,
        payload: SavingsBaselineGenerateRequest,
        tenant_id: str,
        actor_id: str,
        actor_name: str,
        request_fingerprint: str,
        facts: list[ExpenseSavingsFact],
        workflow_facts: list[ExpenseWorkflowCycleFact],
        data_scope: str,
        now: datetime,
    ) -> tuple[
        list[ProfileBaselineSnapshot],
        list[SavingsAnalysisQualityIssue],
        int,
    ]:
        snapshots: list[ProfileBaselineSnapshot] = []
        issues: list[SavingsAnalysisQualityIssue] = []
        existing_count = 0
        # PostgreSQL advisory transaction lock 持续到 commit，统一顺序避免反序死锁。
        for dimension_type in sorted(payload.dimensions):
            if dimension_type == "supplier":
                issues.append(self._supplier_coverage_issue())
                continue
            if dimension_type == "workflow":
                workflow_snapshots, workflow_existing = self._freeze_workflow_dimension(
                    payload=payload,
                    tenant_id=tenant_id,
                    actor_id=actor_id,
                    actor_name=actor_name,
                    request_fingerprint=request_fingerprint,
                    facts=workflow_facts,
                    data_scope=data_scope,
                    now=now,
                )
                snapshots.extend(workflow_snapshots)
                existing_count += workflow_existing
                continue
            grouped: dict[tuple[str, str, str], list[ExpenseSavingsFact]] = defaultdict(list)
            missing_count = 0
            for fact in facts:
                dimension = fact.dimension(dimension_type)
                if dimension is None:
                    missing_count += 1
                    continue
                dimension_id, dimension_label = dimension
                grouped[(dimension_id, dimension_label, fact.currency)].append(fact)
            if missing_count:
                issues.append(
                    SavingsAnalysisQualityIssue(
                        code="baseline_dimension_value_missing",
                        message=f"部分费用事实缺少 {dimension_type} 维度，未纳入该维度基线。",
                        dimension_type=dimension_type,
                        metadata={"fact_count": missing_count},
                    )
                )
            for (dimension_id, dimension_label, currency), group in sorted(grouped.items()):
                baseline, existed = self._freeze_group(
                    payload=payload,
                    tenant_id=tenant_id,
                    actor_id=actor_id,
                    actor_name=actor_name,
                    request_fingerprint=request_fingerprint,
                    dimension_type=dimension_type,
                    dimension_id=dimension_id,
                    dimension_label=dimension_label,
                    currency=currency,
                    facts=group,
                    data_scope=data_scope,
                    now=now,
                )
                snapshots.append(baseline)
                existing_count += int(existed)
        snapshots.sort(
            key=lambda row: (row.dimension_type, row.dimension_id, row.original_currency or "")
        )
        return snapshots, issues, existing_count

    def _freeze_workflow_dimension(
        self,
        *,
        payload: SavingsBaselineGenerateRequest,
        tenant_id: str,
        actor_id: str,
        actor_name: str,
        request_fingerprint: str,
        facts: list[ExpenseWorkflowCycleFact],
        data_scope: str,
        now: datetime,
    ) -> tuple[list[ProfileBaselineSnapshot], int]:
        grouped: dict[tuple[str, str], list[ExpenseWorkflowCycleFact]] = defaultdict(list)
        for fact in facts:
            grouped[(fact.workflow_key, fact.workflow_label)].append(fact)

        snapshots: list[ProfileBaselineSnapshot] = []
        existing_count = 0
        for (workflow_key, workflow_label), group in sorted(grouped.items()):
            baseline, existed = self._freeze_workflow_group(
                payload=payload,
                tenant_id=tenant_id,
                actor_id=actor_id,
                actor_name=actor_name,
                request_fingerprint=request_fingerprint,
                workflow_key=workflow_key,
                workflow_label=workflow_label,
                facts=group,
                data_scope=data_scope,
                now=now,
            )
            snapshots.append(baseline)
            existing_count += int(existed)
        return snapshots, existing_count

    def _freeze_workflow_group(
        self,
        *,
        payload: SavingsBaselineGenerateRequest,
        tenant_id: str,
        actor_id: str,
        actor_name: str,
        request_fingerprint: str,
        workflow_key: str,
        workflow_label: str,
        facts: list[ExpenseWorkflowCycleFact],
        data_scope: str,
        now: datetime,
    ) -> tuple[ProfileBaselineSnapshot, bool]:
        query_fingerprint = stable_digest(
            {
                "algorithm_version": self.WORKFLOW_ALGORITHM_VERSION,
                "tenant_id": tenant_id,
                "data_scope": data_scope,
                "dimension_type": "workflow",
                "dimension_id": workflow_key,
                "metric_key": "median_submission_to_payment_elapsed_minutes",
                "window_start": payload.window_start,
                "window_end": payload.window_end,
                "as_of": payload.as_of,
                "minimum_complete_samples": payload.minimum_complete_samples,
                "source_hashes": sorted(fact.content_hash for fact in facts),
            }
        )
        baseline_key = f"historical:workflow:{query_fingerprint}"
        with self.protocol.serialize(
            tenant_id=tenant_id,
            actor_id="baseline-workflow-group",
            request_id=baseline_key,
        ):
            existing = self.db.scalar(
                select(ProfileBaselineSnapshot).where(
                    ProfileBaselineSnapshot.tenant_id == tenant_id,
                    ProfileBaselineSnapshot.baseline_key == baseline_key,
                )
            )
            if existing is not None:
                return existing, True

            quality_status, quality_score, quality_issues = self._workflow_quality(
                facts,
                payload.minimum_complete_samples,
            )
            baseline = ProfileBaselineSnapshot(
                id=str(uuid.uuid4()),
                tenant_id=tenant_id,
                baseline_key=baseline_key,
                baseline_type="historical_cohort",
                dimension_type="workflow",
                dimension_id=workflow_key,
                metric_key="median_submission_to_payment_elapsed_minutes",
                unit="minutes",
                original_currency=None,
                baseline_value=Decimal(
                    median([fact.elapsed_minutes for fact in facts])
                ).quantize(Decimal("0.0001")),
                window_start=payload.window_start,
                window_end=payload.window_end,
                sample_count=len(facts),
                method=f"median_completed_workflow_elapsed_{data_scope}_scope",
                query_fingerprint=query_fingerprint,
                data_quality_status=quality_status,
                data_quality_score=quality_score,
                quality_issues_json=[
                    issue.model_dump(mode="json") for issue in quality_issues
                ],
                algorithm_version=self.WORKFLOW_ALGORITHM_VERSION,
                frozen_at=now,
                frozen_by=actor_id,
                version=1,
                created_at=now,
            )
            self.db.add(baseline)
            self.db.flush()
            self._record_workflow_evidence(
                baseline,
                facts=facts,
                workflow_label=workflow_label,
                now=now,
            )
            self._record_event(
                baseline,
                payload=payload,
                actor_id=actor_id,
                actor_name=actor_name,
                request_fingerprint=request_fingerprint,
                now=now,
            )
            return baseline, False

    def _freeze_group(
        self,
        *,
        payload: SavingsBaselineGenerateRequest,
        tenant_id: str,
        actor_id: str,
        actor_name: str,
        request_fingerprint: str,
        dimension_type: str,
        dimension_id: str,
        dimension_label: str,
        currency: str,
        facts: list[ExpenseSavingsFact],
        data_scope: str,
        now: datetime,
    ) -> tuple[ProfileBaselineSnapshot, bool]:
        query_fingerprint = stable_digest(
            {
                "algorithm_version": self.ALGORITHM_VERSION,
                "tenant_id": tenant_id,
                "data_scope": data_scope,
                "dimension_type": dimension_type,
                "dimension_id": dimension_id,
                "currency": currency,
                "window_start": payload.window_start,
                "window_end": payload.window_end,
                "as_of": payload.as_of,
                "minimum_complete_samples": payload.minimum_complete_samples,
                "source_hashes": sorted(fact.content_hash for fact in facts),
            }
        )
        baseline_key = f"historical:{dimension_type}:{query_fingerprint}"
        with self.protocol.serialize(
            tenant_id=tenant_id,
            actor_id="baseline-group",
            request_id=baseline_key,
        ):
            existing = self.db.scalar(
                select(ProfileBaselineSnapshot).where(
                    ProfileBaselineSnapshot.tenant_id == tenant_id,
                    ProfileBaselineSnapshot.baseline_key == baseline_key,
                )
            )
            if existing is not None:
                return existing, True

            sample_count = len(facts)
            quality_status, quality_score, quality_issues = self._quality(
                facts,
                payload.minimum_complete_samples,
            )
            baseline = ProfileBaselineSnapshot(
                id=str(uuid.uuid4()),
                tenant_id=tenant_id,
                baseline_key=baseline_key,
                baseline_type="historical_cohort",
                dimension_type=dimension_type,
                dimension_id=dimension_id,
                metric_key="median_expense_fact_amount",
                unit="currency",
                original_currency=currency,
                baseline_value=Decimal(median([fact.amount for fact in facts])).quantize(
                    Decimal("0.0001")
                ),
                window_start=payload.window_start,
                window_end=payload.window_end,
                sample_count=sample_count,
                method=f"median_archived_expense_facts_{data_scope}_scope",
                query_fingerprint=query_fingerprint,
                data_quality_status=quality_status,
                data_quality_score=quality_score,
                quality_issues_json=[
                    issue.model_dump(mode="json") for issue in quality_issues
                ],
                algorithm_version=self.ALGORITHM_VERSION,
                frozen_at=now,
                frozen_by=actor_id,
                version=1,
                created_at=now,
            )
            self.db.add(baseline)
            self.db.flush()
            self._record_evidence(
                baseline,
                facts=facts,
                dimension_label=dimension_label,
                now=now,
            )
            self._record_event(
                baseline,
                payload=payload,
                actor_id=actor_id,
                actor_name=actor_name,
                request_fingerprint=request_fingerprint,
                now=now,
            )
            return baseline, False

    def _record_evidence(
        self,
        baseline: ProfileBaselineSnapshot,
        *,
        facts: list[ExpenseSavingsFact],
        dimension_label: str,
        now: datetime,
    ) -> None:
        source_ids = sorted({fact.source_id for fact in facts})
        self.db.add(
            SavingsEvidenceLink(
                id=str(uuid.uuid4()),
                tenant_id=baseline.tenant_id,
                evidence_key=f"baseline-cohort:{baseline.query_fingerprint}",
                entity_type="baseline",
                entity_id=baseline.id,
                baseline_snapshot_id=baseline.id,
                evidence_role="archived_expense_cohort",
                resource_type="expense_claim_fact_set",
                resource_id=baseline.query_fingerprint,
                source_system="x-financial",
                external_event_id=None,
                content_hash=baseline.query_fingerprint,
                occurred_at=baseline.window_end or now,
                collected_at=now,
                verification_status="verified",
                verified_by="savings-baseline-generator",
                verified_at=now,
                metadata_json={
                    "dimension_label": dimension_label,
                    "source_count": len(source_ids),
                    "source_ids": source_ids[:100],
                    "source_ids_truncated": len(source_ids) > 100,
                },
                created_at=now,
            )
        )

    def _record_workflow_evidence(
        self,
        baseline: ProfileBaselineSnapshot,
        *,
        facts: list[ExpenseWorkflowCycleFact],
        workflow_label: str,
        now: datetime,
    ) -> None:
        source_ids = sorted({fact.source_id for fact in facts})
        self.db.add(
            SavingsEvidenceLink(
                id=str(uuid.uuid4()),
                tenant_id=baseline.tenant_id,
                evidence_key=f"baseline-workflow:{baseline.query_fingerprint}",
                entity_type="baseline",
                entity_id=baseline.id,
                baseline_snapshot_id=baseline.id,
                evidence_role="completed_workflow_cycle_cohort",
                resource_type="business_event_cycle_fact_set",
                resource_id=baseline.query_fingerprint,
                source_system="x-financial",
                external_event_id=None,
                content_hash=baseline.query_fingerprint,
                occurred_at=baseline.window_end or now,
                collected_at=now,
                verification_status="verified",
                verified_by="savings-baseline-generator",
                verified_at=now,
                metadata_json={
                    "workflow_label": workflow_label,
                    "metric_semantics": "elapsed_cycle_not_active_labor",
                    "active_labor_available": False,
                    "source_count": len(source_ids),
                    "source_ids": source_ids[:100],
                    "source_ids_truncated": len(source_ids) > 100,
                },
                created_at=now,
            )
        )

    def _record_event(
        self,
        baseline: ProfileBaselineSnapshot,
        *,
        payload: SavingsBaselineGenerateRequest,
        actor_id: str,
        actor_name: str,
        request_fingerprint: str,
        now: datetime,
    ) -> None:
        event_request_id = f"{payload.request_id[:96]}:{baseline.query_fingerprint[:16]}"
        state = {
            "id": baseline.id,
            "baseline_key": baseline.baseline_key,
            "dimension_type": baseline.dimension_type,
            "dimension_id": baseline.dimension_id,
            "baseline_value": str(baseline.baseline_value),
            "currency": baseline.original_currency,
            "sample_count": baseline.sample_count,
            "data_quality_status": baseline.data_quality_status,
            "query_fingerprint": baseline.query_fingerprint,
        }
        self.db.add(
            SavingsEvent(
                id=str(uuid.uuid4()),
                tenant_id=baseline.tenant_id,
                aggregate_type="baseline",
                aggregate_id=baseline.id,
                baseline_snapshot_id=baseline.id,
                action="historical_baseline_frozen",
                actor_id=actor_id,
                actor_name=actor_name,
                actor_type="user",
                request_id=event_request_id,
                expected_version=0,
                result_version=1,
                payload_fingerprint=savings_payload_fingerprint(
                    action="historical_baseline_frozen",
                    aggregate_type="baseline",
                    aggregate_id=baseline.id,
                    actor_id=actor_id,
                    payload={
                        "request_fingerprint": request_fingerprint,
                        "query_fingerprint": baseline.query_fingerprint,
                    },
                ),
                payload_json={
                    "request_fingerprint": request_fingerprint,
                    "window_start": payload.window_start.isoformat(),
                    "window_end": payload.window_end.isoformat(),
                    "as_of": payload.as_of.isoformat(),
                },
                before_json={},
                after_json=state,
                response_json=state,
                correlation_id=request_fingerprint,
                occurred_at=now,
            )
        )

    @staticmethod
    def _quality(
        facts: list[ExpenseSavingsFact],
        minimum_complete_samples: int,
    ) -> tuple[str, Decimal, list[SavingsAnalysisQualityIssue]]:
        sample_count = len(facts)
        distinct_claim_count = len({fact.claim_id for fact in facts})
        issues: list[SavingsAnalysisQualityIssue] = []
        if (
            sample_count >= minimum_complete_samples
            and distinct_claim_count >= minimum_complete_samples
        ):
            status = "complete"
            score = Decimal("1.0000")
        elif sample_count >= 2 and distinct_claim_count >= 2:
            status = "partial"
            score = Decimal("0.7000")
            issues.append(
                SavingsAnalysisQualityIssue(
                    code="baseline_sample_partial",
                    message="样本量低于完整基线阈值，只能用于提示性分析。",
                    metadata={
                        "sample_count": sample_count,
                        "distinct_claim_count": distinct_claim_count,
                        "minimum_complete_samples": minimum_complete_samples,
                    },
                )
            )
        else:
            status = "insufficient"
            score = Decimal("0.3000")
            issues.append(
                SavingsAnalysisQualityIssue(
                    code="baseline_sample_insufficient",
                    message="基线缺少至少两个独立单据样本，禁止据此计算货币化节省。",
                    severity="error",
                    metadata={
                        "sample_count": sample_count,
                        "distinct_claim_count": distinct_claim_count,
                    },
                )
            )
        fallback_count = sum(fact.source_level == "claim_fallback" for fact in facts)
        if fallback_count:
            status = "partial" if status == "complete" else status
            score = min(score, Decimal("0.8000"))
            issues.append(
                SavingsAnalysisQualityIssue(
                    code="baseline_contains_claim_fallback",
                    message="基线包含缺少明细的单据总额，不能与标准费用明细直接比较。",
                    metadata={"fallback_count": fallback_count},
                )
            )
        return status, score, issues

    @staticmethod
    def _workflow_quality(
        facts: list[ExpenseWorkflowCycleFact],
        minimum_complete_samples: int,
    ) -> tuple[str, Decimal, list[SavingsAnalysisQualityIssue]]:
        sample_count = len(facts)
        distinct_claim_count = len({fact.claim_id for fact in facts})
        issues: list[SavingsAnalysisQualityIssue] = [
            SavingsAnalysisQualityIssue(
                code="workflow_elapsed_not_active_labor",
                message="该基线只表示端到端经过时间，不能换算人工活跃分钟或工时价值。",
                severity="info",
                dimension_type="workflow",
            )
        ]
        if (
            sample_count >= minimum_complete_samples
            and distinct_claim_count >= minimum_complete_samples
        ):
            return "complete", Decimal("1.0000"), issues
        if sample_count >= 2 and distinct_claim_count >= 2:
            issues.append(
                SavingsAnalysisQualityIssue(
                    code="workflow_baseline_sample_partial",
                    message="完成流程样本低于完整基线阈值，只能用于周期提示。",
                    dimension_type="workflow",
                    metadata={
                        "sample_count": sample_count,
                        "distinct_claim_count": distinct_claim_count,
                        "minimum_complete_samples": minimum_complete_samples,
                    },
                )
            )
            return "partial", Decimal("0.7000"), issues
        issues.append(
            SavingsAnalysisQualityIssue(
                code="workflow_baseline_sample_insufficient",
                message="流程周期缺少至少两个独立完成单据，不用于经营判断。",
                severity="error",
                dimension_type="workflow",
                metadata={
                    "sample_count": sample_count,
                    "distinct_claim_count": distinct_claim_count,
                },
            )
        )
        return "insufficient", Decimal("0.3000"), issues

    @staticmethod
    def _supplier_coverage_issue() -> SavingsAnalysisQualityIssue:
        return SavingsAnalysisQualityIssue(
            code="supplier_dimension_unavailable",
            message="当前费用明细没有经过核验的供应商主数据，未生成供应商基线或价格漂移金额。",
            severity="error",
            dimension_type="supplier",
            metadata={
                "required_sources": [
                    "verified_supplier_id",
                    "invoice_line_quantity",
                    "invoice_line_unit_price",
                ]
            },
        )

    @staticmethod
    def _request_fingerprint(
        payload: SavingsBaselineGenerateRequest,
        actor_id: str,
    ) -> str:
        return savings_payload_fingerprint(
            action="generate_historical_baselines",
            aggregate_type="baseline_batch",
            aggregate_id=payload.request_id,
            actor_id=actor_id,
            payload=payload.model_dump(mode="json", exclude={"request_id"}),
        )
