from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from datetime import UTC, datetime
from decimal import Decimal, InvalidOperation
from typing import Any

from sqlalchemy import Select, false, func, or_, select
from sqlalchemy.orm import Session, selectinload

from app.api.deps import CurrentUserContext
from app.models.expense_case import BusinessEvent, ExpenseCaseLink
from app.models.financial_record import ExpenseClaim
from app.schemas.savings_insights import SavingsAnalysisQualityIssue
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin
from app.services.savings_access_policy import SavingsAccessPolicy


@dataclass(frozen=True, slots=True)
class ExpenseSavingsFact:
    claim_id: str
    claim_no: str
    item_id: str | None
    employee_id: str
    employee_name: str
    department_id: str
    department_name: str
    project_code: str
    expense_type: str
    city: str
    amount: Decimal
    currency: str
    occurred_at: datetime
    source_updated_at: datetime | None
    source_level: str

    def dimension(self, dimension_type: str) -> tuple[str, str] | None:
        values = {
            "employee": (self.employee_id, self.employee_name),
            "department": (self.department_id, self.department_name),
            "expense_type": (self.expense_type, self.expense_type),
            "city": (self.city, self.city),
            "project": (self.project_code, self.project_code),
        }
        identity, label = values.get(dimension_type, ("", ""))
        normalized_identity = str(identity or "").strip()
        normalized_label = str(label or "").strip()
        if not normalized_identity and not normalized_label:
            return None
        if not normalized_identity:
            normalized_identity = f"name:{normalized_label.casefold()}"
        return normalized_identity, normalized_label or normalized_identity

    @property
    def source_id(self) -> str:
        return self.item_id or self.claim_id

    @property
    def content_hash(self) -> str:
        return stable_digest(
            {
                "claim_id": self.claim_id,
                "item_id": self.item_id,
                "amount": str(self.amount),
                "currency": self.currency,
                "occurred_at": self.occurred_at,
                "updated_at": self.source_updated_at,
            }
        )


@dataclass(frozen=True, slots=True)
class ExpenseWorkflowCycleFact:
    """首个付款完成事件形成的端到端流程历时，不代表人工活跃工时。"""

    claim_id: str
    claim_no: str
    workflow_key: str
    workflow_label: str
    started_at: datetime
    completed_at: datetime
    completion_event_id: str
    completion_event_fingerprint: str

    @property
    def elapsed_minutes(self) -> Decimal:
        seconds = Decimal(str((self.completed_at - self.started_at).total_seconds()))
        return (seconds / Decimal("60")).quantize(Decimal("0.0001"))

    @property
    def source_id(self) -> str:
        return self.completion_event_id

    @property
    def content_hash(self) -> str:
        return stable_digest(
            {
                "claim_id": self.claim_id,
                "workflow_key": self.workflow_key,
                "started_at": self.started_at,
                "completed_at": self.completed_at,
                "completion_event_id": self.completion_event_id,
                "completion_event_fingerprint": self.completion_event_fingerprint,
            }
        )


@dataclass(slots=True)
class ExpenseSavingsFactSet:
    facts: list[ExpenseSavingsFact] = field(default_factory=list)
    workflow_cycle_facts: list[ExpenseWorkflowCycleFact] = field(default_factory=list)
    claim_count: int = 0
    item_count: int = 0
    data_scope: str = "tenant"
    quality_issues: list[SavingsAnalysisQualityIssue] = field(default_factory=list)


class SavingsFactScopeReader:
    """按 Expense Case tenant link 与用户数据范围读取可审计费用事实。"""

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

    def load_archived_facts(
        self,
        *,
        window_start: datetime,
        window_end: datetime,
        as_of: datetime,
        current_user: CurrentUserContext,
        include_workflow_cycles: bool = False,
    ) -> ExpenseSavingsFactSet:
        SavingsAccessPolicy.require_tenant_value_read(current_user)
        statement = (
            select(ExpenseClaim)
            .options(selectinload(ExpenseClaim.items))
            .where(
                ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(
                    current_user.tenant_id
                ),
                ExpenseClaim.occurred_at >= window_start,
                ExpenseClaim.occurred_at <= window_end,
                ExpenseClaim.created_at <= as_of,
                ExpenseClaimAccessPolicy.build_archived_claim_condition(),
                self._reimbursement_claim_condition(),
            )
            .order_by(ExpenseClaim.occurred_at.asc(), ExpenseClaim.id.asc())
        )
        statement, data_scope, scope_issue = self._apply_data_scope(
            statement,
            current_user,
        )
        claims = list(self.db.scalars(statement).unique().all())
        result = ExpenseSavingsFactSet(
            claim_count=len(claims),
            data_scope=data_scope,
        )
        if scope_issue is not None:
            result.quality_issues.append(scope_issue)
        fallback_count = 0
        invalid_count = 0
        outside_window_count = 0
        modified_after_as_of_count = 0
        for claim in claims:
            currency = str(claim.currency or "").strip().upper()
            if len(currency) != 3:
                invalid_count += 1
                continue
            items = list(claim.items or [])
            if not items:
                fact = self._claim_fallback_fact(claim, currency)
                if fact is not None:
                    if not self._fact_in_window(
                        fact,
                        window_start=window_start,
                        window_end=window_end,
                        as_of=as_of,
                    ):
                        outside_window_count += 1
                    elif self._datetime_after(fact.source_updated_at, as_of):
                        modified_after_as_of_count += 1
                    else:
                        result.facts.append(fact)
                        fallback_count += 1
                else:
                    invalid_count += 1
                continue
            for item in items:
                fact = self._item_fact(claim, item, currency)
                if fact is None:
                    invalid_count += 1
                    continue
                if not self._fact_in_window(
                    fact,
                    window_start=window_start,
                    window_end=window_end,
                    as_of=as_of,
                ):
                    outside_window_count += 1
                    continue
                if self._datetime_after(fact.source_updated_at, as_of):
                    modified_after_as_of_count += 1
                    continue
                result.facts.append(fact)
                result.item_count += 1

        if fallback_count:
            result.quality_issues.append(
                SavingsAnalysisQualityIssue(
                    code="claim_amount_used_without_items",
                    message="部分已归档单据没有费用明细，基线使用单据总额并降低质量等级。",
                    metadata={"claim_count": fallback_count},
                )
            )
        if invalid_count:
            result.quality_issues.append(
                SavingsAnalysisQualityIssue(
                    code="invalid_expense_fact_excluded",
                    message="金额非正数、币种无效或日期缺失的费用事实已从分析中排除。",
                    metadata={"fact_count": invalid_count},
                )
            )
        if outside_window_count:
            result.quality_issues.append(
                SavingsAnalysisQualityIssue(
                    code="expense_fact_outside_window_excluded",
                    message="单据内日期不在分析窗口的费用明细已排除。",
                    metadata={"fact_count": outside_window_count},
                )
            )
        if modified_after_as_of_count:
            result.quality_issues.append(
                SavingsAnalysisQualityIssue(
                    code="expense_fact_modified_after_as_of_excluded",
                    message=(
                        "当前表没有双时态历史，截止时间之后被修改的事实无法安全回放，已排除。"
                    ),
                    severity="error",
                    metadata={"fact_count": modified_after_as_of_count},
                )
            )
        if not result.facts:
            result.quality_issues.append(
                SavingsAnalysisQualityIssue(
                    code="archived_expense_facts_unavailable",
                    message="当前租户和数据范围在指定窗口内没有可用于分析的已归档费用事实。",
                    severity="error",
                )
            )
        if include_workflow_cycles:
            workflow_facts, workflow_issues = self._load_workflow_cycle_facts(
                claims=claims,
                window_start=window_start,
                window_end=window_end,
                as_of=as_of,
                current_user=current_user,
            )
            result.workflow_cycle_facts = workflow_facts
            result.quality_issues.extend(workflow_issues)
        return result

    def _load_workflow_cycle_facts(
        self,
        *,
        claims: list[ExpenseClaim],
        window_start: datetime,
        window_end: datetime,
        as_of: datetime,
        current_user: CurrentUserContext,
    ) -> tuple[list[ExpenseWorkflowCycleFact], list[SavingsAnalysisQualityIssue]]:
        issues = [
            SavingsAnalysisQualityIssue(
                code="workflow_active_labor_unavailable",
                message=(
                    "流程基线只统计提交至首个付款完成事件的经过时间；当前没有人工活跃计时，"
                    "不得据此推算人工工时或工时价值。"
                ),
                severity="info",
                dimension_type="workflow",
                metadata={
                    "metric_key": "median_submission_to_payment_elapsed_minutes",
                    "unit": "minutes",
                    "active_labor_available": False,
                },
            )
        ]
        claim_by_id = {str(claim.id): claim for claim in claims}
        if not claim_by_id:
            issues.append(self._workflow_unavailable_issue())
            return [], issues

        tenant_id = ExpenseClaimTenantScopeMixin.normalize_tenant_id(
            current_user.tenant_id
        )
        cutoff = min(self._utc_datetime(window_end), self._utc_datetime(as_of))
        event_matches_claim_case = (
            select(ExpenseCaseLink.id)
            .where(
                ExpenseCaseLink.tenant_id == tenant_id,
                ExpenseCaseLink.expense_case_id == BusinessEvent.expense_case_id,
                ExpenseCaseLink.resource_type == "expense_claim",
                ExpenseCaseLink.resource_id == BusinessEvent.aggregate_id,
            )
            .exists()
        )
        events = list(
            self.db.scalars(
                select(BusinessEvent)
                .where(
                    BusinessEvent.tenant_id == tenant_id,
                    BusinessEvent.aggregate_type == "expense_claim",
                    BusinessEvent.aggregate_id.in_(tuple(claim_by_id)),
                    BusinessEvent.event_type == "payment_completed",
                    BusinessEvent.occurred_at <= cutoff,
                    event_matches_claim_case,
                )
                .order_by(
                    BusinessEvent.aggregate_id.asc(),
                    BusinessEvent.occurred_at.asc(),
                    BusinessEvent.id.asc(),
                )
            ).all()
        )
        first_completion_by_claim: dict[str, BusinessEvent] = {}
        for event in events:
            first_completion_by_claim.setdefault(str(event.aggregate_id), event)

        facts: list[ExpenseWorkflowCycleFact] = []
        invalid_count = 0
        modified_after_as_of_count = 0
        for claim_id, event in first_completion_by_claim.items():
            claim = claim_by_id.get(claim_id)
            if claim is None:
                continue
            completed_at = self._utc_datetime(event.occurred_at)
            if not (
                self._utc_datetime(window_start)
                <= completed_at
                <= self._utc_datetime(window_end)
            ):
                continue
            if self._datetime_after(claim.updated_at, as_of):
                modified_after_as_of_count += 1
                continue
            if claim.submitted_at is None:
                invalid_count += 1
                continue
            started_at = self._utc_datetime(claim.submitted_at)
            if started_at > completed_at:
                invalid_count += 1
                continue
            event_fingerprint = stable_digest(
                {
                    "event_id": event.id,
                    "event_type": event.event_type,
                    "idempotency_key": event.idempotency_key,
                    "correlation_id": event.correlation_id,
                    "occurred_at": completed_at,
                }
            )
            facts.append(
                ExpenseWorkflowCycleFact(
                    claim_id=claim_id,
                    claim_no=str(claim.claim_no or claim_id),
                    workflow_key="reimbursement_submission_to_payment",
                    workflow_label="报销提交至首个付款完成",
                    started_at=started_at,
                    completed_at=completed_at,
                    completion_event_id=str(event.id),
                    completion_event_fingerprint=event_fingerprint,
                )
            )

        if invalid_count:
            issues.append(
                SavingsAnalysisQualityIssue(
                    code="workflow_cycle_timestamp_invalid",
                    message="缺少提交时间或完成早于提交的流程事实已排除。",
                    dimension_type="workflow",
                    metadata={"fact_count": invalid_count},
                )
            )
        if modified_after_as_of_count:
            issues.append(
                SavingsAnalysisQualityIssue(
                    code="workflow_cycle_modified_after_as_of_excluded",
                    message=(
                        "当前单据表没有双时态历史，截止时间后被修改的流程起点无法安全回放，已排除。"
                    ),
                    severity="error",
                    dimension_type="workflow",
                    metadata={"fact_count": modified_after_as_of_count},
                )
            )
        if not facts:
            issues.append(self._workflow_unavailable_issue())
        facts.sort(key=lambda fact: (fact.completed_at, fact.claim_id))
        return facts, issues

    @staticmethod
    def _workflow_unavailable_issue() -> SavingsAnalysisQualityIssue:
        return SavingsAnalysisQualityIssue(
            code="workflow_completion_event_unavailable",
            message="当前窗口没有同租户可核验的首个付款完成事件，未生成流程周期基线。",
            dimension_type="workflow",
        )

    @classmethod
    def _fact_in_window(
        cls,
        fact: ExpenseSavingsFact,
        *,
        window_start: datetime,
        window_end: datetime,
        as_of: datetime,
    ) -> bool:
        occurred_at = cls._utc_datetime(fact.occurred_at)
        return (
            cls._utc_datetime(window_start)
            <= occurred_at
            <= cls._utc_datetime(window_end)
            and occurred_at <= cls._utc_datetime(as_of)
        )

    @classmethod
    def _datetime_after(cls, value: datetime | None, boundary: datetime) -> bool:
        return bool(value and cls._utc_datetime(value) > cls._utc_datetime(boundary))

    @staticmethod
    def _utc_datetime(value: datetime) -> datetime:
        if value.tzinfo is None:
            return value.replace(tzinfo=UTC)
        return value.astimezone(UTC)

    @staticmethod
    def _reimbursement_claim_condition() -> Any:
        normalized_type = func.lower(func.coalesce(ExpenseClaim.expense_type, ""))
        claim_no = func.upper(func.coalesce(ExpenseClaim.claim_no, ""))
        return ~or_(
            claim_no.like("AP-%"),
            claim_no.like("APP-%"),
            claim_no.like("A________"),
            normalized_type == "application",
            normalized_type.like("%\\_application", escape="\\"),
        )

    @staticmethod
    def _apply_data_scope(
        statement: Select[tuple[ExpenseClaim]],
        current_user: CurrentUserContext,
    ) -> tuple[
        Select[tuple[ExpenseClaim]],
        str,
        SavingsAnalysisQualityIssue | None,
    ]:
        if SavingsAccessPolicy.can_read_tenant_value(current_user):
            return statement, "tenant", None
        department_id = str(current_user.department_id or "").strip()
        department_name = str(current_user.department_name or "").strip()
        if department_id:
            return (
                statement.where(ExpenseClaim.department_id == department_id),
                "department",
                None,
            )
        if department_name:
            return (
                statement.where(ExpenseClaim.department_name == department_name),
                "department",
                None,
            )
        return (
            statement.where(false()),
            "unavailable",
            SavingsAnalysisQualityIssue(
                code="claim_cost_center_scope_unavailable",
                message="费用单据没有独立成本中心字段，不能把仅成本中心权限安全映射到单据事实。",
                severity="error",
            ),
        )

    @classmethod
    def _item_fact(
        cls,
        claim: ExpenseClaim,
        item: Any,
        currency: str,
    ) -> ExpenseSavingsFact | None:
        amount = cls._positive_money(item.item_amount)
        if amount is None or item.item_date is None:
            return None
        occurred_at = datetime.combine(item.item_date, datetime.min.time(), tzinfo=UTC)
        return cls._fact(
            claim,
            item_id=str(item.id),
            amount=amount,
            currency=currency,
            occurred_at=occurred_at,
            expense_type=str(item.item_type or claim.expense_type or "").strip(),
            city=str(item.item_location or claim.location or "").strip(),
            source_updated_at=cls._latest_datetime(item.updated_at, claim.updated_at),
            source_level="claim_item",
        )

    @classmethod
    def _claim_fallback_fact(
        cls,
        claim: ExpenseClaim,
        currency: str,
    ) -> ExpenseSavingsFact | None:
        amount = cls._positive_money(claim.amount)
        if amount is None or claim.occurred_at is None:
            return None
        return cls._fact(
            claim,
            item_id=None,
            amount=amount,
            currency=currency,
            occurred_at=claim.occurred_at,
            expense_type=str(claim.expense_type or "").strip(),
            city=str(claim.location or "").strip(),
            source_updated_at=claim.updated_at,
            source_level="claim_fallback",
        )

    @staticmethod
    def _fact(
        claim: ExpenseClaim,
        *,
        item_id: str | None,
        amount: Decimal,
        currency: str,
        occurred_at: datetime,
        expense_type: str,
        city: str,
        source_updated_at: datetime | None,
        source_level: str,
    ) -> ExpenseSavingsFact:
        return ExpenseSavingsFact(
            claim_id=str(claim.id),
            claim_no=str(claim.claim_no or claim.id),
            item_id=item_id,
            employee_id=str(claim.employee_id or "").strip(),
            employee_name=str(claim.employee_name or "").strip(),
            department_id=str(claim.department_id or "").strip(),
            department_name=str(claim.department_name or "").strip(),
            project_code=str(claim.project_code or "").strip(),
            expense_type=expense_type,
            city=city,
            amount=amount,
            currency=currency,
            occurred_at=occurred_at,
            source_updated_at=source_updated_at,
            source_level=source_level,
        )

    @staticmethod
    def _positive_money(value: Any) -> Decimal | None:
        try:
            amount = Decimal(str(value or "0")).quantize(Decimal("0.0001"))
        except (InvalidOperation, ValueError):
            return None
        return amount if amount > 0 else None

    @classmethod
    def _latest_datetime(
        cls,
        *values: datetime | None,
    ) -> datetime | None:
        available = [value for value in values if value is not None]
        if not available:
            return None
        return max(available, key=cls._utc_datetime)


def stable_digest(value: Any) -> str:
    canonical = json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        default=lambda item: item.isoformat() if hasattr(item, "isoformat") else str(item),
    )
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
