from __future__ import annotations

import hashlib
import json
import uuid
from datetime import UTC, datetime, timedelta
from decimal import Decimal, InvalidOperation
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.expense_case import ExpenseCase
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.savings import (
    ProfileBaselineSnapshot,
    SavingsEvent,
    SavingsEvidenceLink,
    SavingsOpportunity,
)
from app.services.expense_cases import ExpenseCaseService
from app.services.savings_protocol import savings_payload_fingerprint


class SavingsDiscoveryService:
    """只从服务端已核验业务事实发现机会；不负责提交事务。"""

    ALGORITHM_VERSION = "standard-adjustment-v1"

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

    def discover_standard_adjustments(
        self,
        *,
        claim: ExpenseClaim,
        items_by_id: dict[str, ExpenseClaimItem],
        adjustment_flags: list[dict[str, Any]],
        current_user: CurrentUserContext,
        request_id: str | None,
    ) -> list[SavingsOpportunity]:
        tenant_id = self._tenant(current_user)
        expense_case = self.expense_cases.ensure_case_for_claim(
            claim,
            tenant_id=tenant_id,
            relation_type="reimbursement",
        )
        if expense_case.tenant_id != tenant_id:
            raise PermissionError("报销单费用事件与当前租户不一致。")

        discovered: list[SavingsOpportunity] = []
        now = datetime.now(UTC)
        for flag in adjustment_flags:
            item_id = str(flag.get("item_id") or "").strip()
            item = items_by_id.get(item_id)
            if item is None:
                raise ValueError("节省机会对应的费用明细已不存在。")
            original = self._money(flag.get("original_amount"))
            target = self._money(flag.get("reimbursable_amount"))
            saving = (original - target).quantize(Decimal("0.0001"))
            if saving <= Decimal("0"):
                continue
            currency = str(claim.currency or "CNY").strip().upper()
            if len(currency) != 3:
                continue

            calculation_fingerprint = str(flag.get("calculation_fingerprint") or "").strip()
            policy_version = str(flag.get("policy_rule_version") or "").strip()
            if not calculation_fingerprint or not policy_version:
                continue
            stable_material = {
                "tenant_id": tenant_id,
                "claim_id": claim.id,
                "item_id": item.id,
                "policy_version": policy_version,
                "calculation_fingerprint": calculation_fingerprint,
            }
            stable_digest = self._digest(stable_material)
            baseline_key = f"standard-adjustment:{stable_digest}"
            opportunity_key = f"standard-adjustment:{stable_digest}"
            existing = self.db.scalar(
                select(SavingsOpportunity).where(
                    SavingsOpportunity.tenant_id == tenant_id,
                    SavingsOpportunity.opportunity_key == opportunity_key,
                )
            )
            if existing is not None:
                discovered.append(existing)
                continue

            discovery_event = self._record_adjustment_event(
                expense_case,
                claim=claim,
                item=item,
                flag=flag,
                current_user=current_user,
                stable_digest=stable_digest,
                request_id=request_id,
            )
            baseline = self._ensure_baseline(
                tenant_id=tenant_id,
                baseline_key=baseline_key,
                claim=claim,
                item=item,
                flag=flag,
                original=original,
                policy_version=policy_version,
                calculation_fingerprint=calculation_fingerprint,
                actor_id=self._actor_id(current_user),
                now=now,
            )
            baseline_snapshot = self._baseline_snapshot(baseline)
            confidence = (
                Decimal("1.0000")
                if baseline.data_quality_status == "complete"
                else Decimal("0.9500")
            )
            opportunity = SavingsOpportunity(
                id=str(uuid.uuid4()),
                tenant_id=tenant_id,
                opportunity_key=opportunity_key,
                benefit_key=f"claim-item:{claim.id}:{item.id}:policy-difference",
                expense_case_id=expense_case.id,
                claim_id=claim.id,
                claim_no_snapshot=str(claim.claim_no or claim.id),
                claim_item_id=item.id,
                discovery_business_event_id=discovery_event.id,
                source_type="standard_adjustment",
                source_id=item.id,
                category="policy_compliance",
                value_kind="cash",
                title="住宿标准重算节省机会",
                description=str(flag.get("message") or "按职级住宿标准降低实际报销金额。"),
                exposure_amount=original,
                baseline_snapshot_id=baseline.id,
                baseline_amount=original,
                target_amount=target,
                estimated_gross=saving,
                estimated_cost=Decimal("0.0000"),
                estimated_net=saving,
                estimated_low=saving,
                estimated_high=saving,
                confidence=confidence,
                currency=currency,
                reporting_currency=currency,
                attribution_method="server_policy_counterfactual",
                suggested_action="按锁定职级住宿标准完成审批和付款，并由独立财务复核。",
                owner_id="finance",
                owner_name=str(claim.finance_owner_name or "财务运营"),
                owner_role="finance",
                due_at=now + timedelta(days=30),
                status="in_progress",
                version=1,
                dimension_json=self._dimensions(claim, item, flag),
                baseline_snapshot_json=baseline_snapshot,
                evidence_json=[
                    {
                        "role": "server_policy_calculation",
                        "resource_type": "expense_claim_item",
                        "resource_id": item.id,
                        "content_hash": calculation_fingerprint,
                        "business_event_id": discovery_event.id,
                    }
                ],
                accepted_at=now,
                started_at=now,
                created_at=now,
                updated_at=now,
            )
            self.db.add(opportunity)
            self.db.flush()
            self._add_evidence_links(
                baseline,
                opportunity,
                item=item,
                discovery_event_id=discovery_event.id,
                content_hash=calculation_fingerprint,
                occurred_at=self._item_occurred_at(item, now),
                now=now,
            )
            self._add_opportunity_event(
                opportunity,
                current_user=current_user,
                stable_digest=stable_digest,
                causation_id=discovery_event.id,
                now=now,
            )
            self.expense_cases.link_resource(
                expense_case,
                resource_type="savings_opportunity",
                resource_id=opportunity.id,
                relation_type="savings",
                tenant_id=tenant_id,
            )
            self.expense_cases.record_resource_event(
                expense_case,
                aggregate_type="savings_opportunity",
                aggregate_id=opportunity.id,
                event_type="saving_opportunity_created",
                actor_id=current_user.username,
                idempotency_key=opportunity_key[:120],
                tenant_id=tenant_id,
                correlation_id=discovery_event.correlation_id,
                causation_id=discovery_event.id,
                payload={
                    "claim_id": claim.id,
                    "claim_item_id": item.id,
                    "estimated_net": str(saving),
                    "currency": currency,
                    "status": opportunity.status,
                },
            )
            discovered.append(opportunity)
        self.db.flush()
        return discovered

    def _ensure_baseline(
        self,
        *,
        tenant_id: str,
        baseline_key: str,
        claim: ExpenseClaim,
        item: ExpenseClaimItem,
        flag: dict[str, Any],
        original: Decimal,
        policy_version: str,
        calculation_fingerprint: str,
        actor_id: str,
        now: datetime,
    ) -> ProfileBaselineSnapshot:
        baseline = self.db.scalar(
            select(ProfileBaselineSnapshot).where(
                ProfileBaselineSnapshot.tenant_id == tenant_id,
                ProfileBaselineSnapshot.baseline_key == baseline_key,
            )
        )
        if baseline is not None:
            return baseline
        version_source = str(flag.get("policy_rule_version_source") or "").strip()
        has_published_version = version_source == "published"
        quality_issues = [] if has_published_version else [
            {
                "code": (
                    "policy_version_uses_content_fingerprint"
                    if version_source
                    else "policy_version_source_missing"
                ),
                "message": (
                    "政策版本来自内容指纹，具备可重放性，但不是正式发布版本。"
                    if version_source
                    else "政策版本来源未记录，基线按部分可信处理。"
                ),
                "source": version_source or "unknown",
            }
        ]
        baseline = ProfileBaselineSnapshot(
            id=str(uuid.uuid4()),
            tenant_id=tenant_id,
            baseline_key=baseline_key,
            baseline_type="policy_counterfactual",
            dimension_type="expense_claim_item",
            dimension_id=item.id,
            metric_key="pre_adjustment_reimbursable_amount",
            unit="currency",
            original_currency=str(claim.currency or "CNY").strip().upper(),
            baseline_value=original,
            sample_count=1,
            method="locked_claim_item_before_policy_acceptance",
            query_fingerprint=calculation_fingerprint,
            data_quality_status="complete" if has_published_version else "partial",
            data_quality_score=(
                Decimal("1.0000") if has_published_version else Decimal("0.9500")
            ),
            quality_issues_json=quality_issues,
            algorithm_version=self.ALGORITHM_VERSION,
            policy_version=policy_version,
            policy_effective_from=item.item_date,
            target_resource_type="expense_claim_item",
            target_resource_id=item.id,
            frozen_at=now,
            frozen_by=actor_id,
            version=1,
            created_at=now,
        )
        self.db.add(baseline)
        self.db.flush()
        event = SavingsEvent(
            id=str(uuid.uuid4()),
            tenant_id=tenant_id,
            aggregate_type="baseline",
            aggregate_id=baseline.id,
            baseline_snapshot_id=baseline.id,
            action="baseline_frozen",
            actor_id="savings-ledger",
            actor_name="Savings Ledger",
            actor_type="service",
            request_id=f"baseline:{baseline_key}"[:120],
            expected_version=0,
            result_version=1,
            payload_fingerprint=savings_payload_fingerprint(
                action="baseline_frozen",
                aggregate_type="baseline",
                aggregate_id=baseline.id,
                actor_id="savings-ledger",
                payload={"query_fingerprint": calculation_fingerprint},
            ),
            payload_json={"policy_version": policy_version},
            before_json={},
            after_json=self._baseline_snapshot(baseline),
            response_json=self._baseline_snapshot(baseline),
            correlation_id=str(uuid.uuid4()),
            occurred_at=now,
        )
        self.db.add(event)
        return baseline

    def _record_adjustment_event(
        self,
        expense_case: ExpenseCase,
        *,
        claim: ExpenseClaim,
        item: ExpenseClaimItem,
        flag: dict[str, Any],
        current_user: CurrentUserContext,
        stable_digest: str,
        request_id: str | None,
    ):
        return self.expense_cases.record_resource_event(
            expense_case,
            aggregate_type="expense_claim",
            aggregate_id=claim.id,
            event_type="standard_adjustment_accepted",
            actor_id=current_user.username,
            idempotency_key=f"standard-adjustment:{stable_digest}"[:120],
            tenant_id=expense_case.tenant_id,
            correlation_id=request_id or stable_digest,
            payload={
                "claim_item_id": item.id,
                "original_amount": str(flag.get("original_amount") or ""),
                "reimbursable_amount": str(flag.get("reimbursable_amount") or ""),
                "employee_absorbed_amount": str(flag.get("employee_absorbed_amount") or ""),
                "policy_rule_version": str(flag.get("policy_rule_version") or ""),
                "calculation_fingerprint": str(flag.get("calculation_fingerprint") or ""),
            },
        )

    def _add_evidence_links(
        self,
        baseline: ProfileBaselineSnapshot,
        opportunity: SavingsOpportunity,
        *,
        item: ExpenseClaimItem,
        discovery_event_id: str,
        content_hash: str,
        occurred_at: datetime,
        now: datetime,
    ) -> None:
        common = {
            "tenant_id": opportunity.tenant_id,
            "evidence_role": "server_policy_calculation",
            "resource_type": "expense_claim_item",
            "resource_id": item.id,
            "source_system": "x-financial",
            "external_event_id": discovery_event_id,
            "content_hash": content_hash,
            "occurred_at": occurred_at,
            "collected_at": now,
            "verification_status": "verified",
            "verified_by": "savings-ledger",
            "verified_at": now,
            "metadata_json": {"business_event_id": discovery_event_id},
            "created_at": now,
        }
        self.db.add_all(
            [
                SavingsEvidenceLink(
                    id=str(uuid.uuid4()),
                    evidence_key=f"baseline:{baseline.baseline_key}"[:180],
                    entity_type="baseline",
                    entity_id=baseline.id,
                    baseline_snapshot_id=baseline.id,
                    **common,
                ),
                SavingsEvidenceLink(
                    id=str(uuid.uuid4()),
                    evidence_key=f"opportunity:{opportunity.opportunity_key}"[:180],
                    entity_type="opportunity",
                    entity_id=opportunity.id,
                    opportunity_id=opportunity.id,
                    **common,
                ),
            ]
        )

    def _add_opportunity_event(
        self,
        opportunity: SavingsOpportunity,
        *,
        current_user: CurrentUserContext,
        stable_digest: str,
        causation_id: str,
        now: datetime,
    ) -> None:
        state = {
            "id": opportunity.id,
            "status": opportunity.status,
            "version": opportunity.version,
            "estimated_net": str(opportunity.estimated_net),
            "currency": opportunity.currency,
        }
        self.db.add(
            SavingsEvent(
                id=str(uuid.uuid4()),
                tenant_id=opportunity.tenant_id,
                aggregate_type="opportunity",
                aggregate_id=opportunity.id,
                opportunity_id=opportunity.id,
                action="discovered_and_started",
                actor_id=self._actor_id(current_user),
                actor_name=str(current_user.name or current_user.username),
                actor_type="user",
                request_id=f"discover:{stable_digest}"[:120],
                expected_version=0,
                result_version=1,
                payload_fingerprint=savings_payload_fingerprint(
                    action="discovered_and_started",
                    aggregate_type="opportunity",
                    aggregate_id=opportunity.id,
                    actor_id=self._actor_id(current_user),
                    payload={"causation_id": causation_id},
                ),
                payload_json={"causation_id": causation_id},
                before_json={},
                after_json=state,
                response_json=state,
                correlation_id=str(uuid.uuid4()),
                causation_id=causation_id,
                occurred_at=now,
            )
        )

    @staticmethod
    def _dimensions(
        claim: ExpenseClaim,
        item: ExpenseClaimItem,
        flag: dict[str, Any],
    ) -> dict[str, Any]:
        return {
            "department_id": str(claim.department_id or ""),
            "department_name": str(claim.department_name or ""),
            "project_code": str(claim.project_code or ""),
            "expense_type": str(claim.expense_type or item.item_type or ""),
            "supplier_id": "",
            "supplier_name": "",
            "city": str(flag.get("policy_matched_city") or item.item_location or ""),
            "employee_id": str(claim.employee_id or ""),
            "employee_grade": str(flag.get("policy_grade") or ""),
        }

    @staticmethod
    def _baseline_snapshot(baseline: ProfileBaselineSnapshot) -> dict[str, Any]:
        return {
            "id": baseline.id,
            "baseline_key": baseline.baseline_key,
            "baseline_value": str(baseline.baseline_value),
            "method": baseline.method,
            "query_fingerprint": baseline.query_fingerprint,
            "policy_version": baseline.policy_version,
            "policy_effective_from": baseline.policy_effective_from.isoformat()
            if baseline.policy_effective_from
            else None,
            "data_quality_status": baseline.data_quality_status,
            "algorithm_version": baseline.algorithm_version,
            "frozen_at": baseline.frozen_at.isoformat(),
        }

    @staticmethod
    def _digest(material: dict[str, Any]) -> str:
        canonical = json.dumps(
            material,
            ensure_ascii=False,
            sort_keys=True,
            separators=(",", ":"),
        )
        return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

    @staticmethod
    def _money(value: Any) -> Decimal:
        try:
            return Decimal(str(value or "0")).quantize(Decimal("0.0001"))
        except (InvalidOperation, ValueError) as error:
            raise ValueError("标准调整金额证据无效，不能创建节省机会。") from error

    @staticmethod
    def _item_occurred_at(item: ExpenseClaimItem, fallback: datetime) -> datetime:
        if item.item_date:
            return datetime.combine(item.item_date, datetime.min.time(), tzinfo=UTC)
        return fallback

    @staticmethod
    def _tenant(current_user: CurrentUserContext) -> str:
        return str(current_user.tenant_id or "default").strip() or "default"

    @staticmethod
    def _actor_id(current_user: CurrentUserContext) -> str:
        return str(current_user.employee_id or current_user.username).strip()
