from __future__ import annotations

import hashlib
import uuid
from datetime import UTC, datetime
from decimal import Decimal
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload

from app.models.expense_case import BusinessEvent, ExpenseCase, ExpenseCaseLink
from app.models.financial_record import ExpenseClaim
from app.services.document_numbering import is_application_claim_no

DEFAULT_TENANT_ID = "default"


class ExpenseCaseService:
    """费用事件编排的最小持久化边界。

    该服务只允许 add/flush，不负责 commit。调用方必须让业务状态与事件在同一事务中提交。
    """

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

    @staticmethod
    def normalize_tenant_id(value: str | None) -> str:
        return str(value or DEFAULT_TENANT_ID).strip() or DEFAULT_TENANT_ID

    @staticmethod
    def normalize_correlation_id(value: str | None) -> str:
        text = str(value or "").strip()
        if not text:
            return uuid.uuid4().hex
        if len(text) <= 64:
            return text
        digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
        return f"sha256:{digest[:57]}"

    def ensure_case_for_claim(
        self,
        claim: ExpenseClaim,
        *,
        tenant_id: str | None = None,
        relation_type: str | None = None,
    ) -> ExpenseCase:
        if not claim.id:
            self.db.flush()
        if not claim.id:
            raise ValueError("费用单据尚未生成 ID，无法关联费用事件。")

        normalized_tenant = self.normalize_tenant_id(tenant_id)
        existing_link = self.db.scalar(
            select(ExpenseCaseLink).where(
                ExpenseCaseLink.resource_type == "expense_claim",
                ExpenseCaseLink.resource_id == claim.id,
            )
        )
        if existing_link is not None:
            if existing_link.tenant_id != normalized_tenant:
                raise PermissionError("费用单据已属于其他租户。")
            expense_case = self.db.get(ExpenseCase, existing_link.expense_case_id)
            if expense_case is None or expense_case.tenant_id != normalized_tenant:
                raise RuntimeError("费用事件关联已损坏。")
            return expense_case

        case_no = f"CASE-{str(claim.claim_no or claim.id).strip()}"
        expense_case = self.db.scalar(
            select(ExpenseCase).where(
                ExpenseCase.tenant_id == normalized_tenant,
                ExpenseCase.case_no == case_no,
            )
        )
        if expense_case is None:
            expense_case = ExpenseCase(
                id=str(uuid.uuid4()),
                tenant_id=normalized_tenant,
                case_no=case_no,
                scene_code=self._scene_code(claim),
                title=self._case_title(claim),
                owner_employee_id=claim.employee_id,
                current_stage=self._stage_for_claim(claim),
                status=self._case_status_for_claim(claim),
            )
            self.db.add(expense_case)
            self.db.flush()

        self.link_claim(
            expense_case,
            claim,
            tenant_id=normalized_tenant,
            relation_type=relation_type or self._relation_type(claim),
        )
        return expense_case

    def link_claim(
        self,
        expense_case: ExpenseCase,
        claim: ExpenseClaim,
        *,
        tenant_id: str | None = None,
        relation_type: str,
    ) -> ExpenseCaseLink:
        if not claim.id:
            self.db.flush()
        normalized_tenant = self.normalize_tenant_id(tenant_id or expense_case.tenant_id)
        if expense_case.tenant_id != normalized_tenant:
            raise PermissionError("不能把费用单据关联到其他租户的费用事件。")

        existing_link = self.db.scalar(
            select(ExpenseCaseLink).where(
                ExpenseCaseLink.resource_type == "expense_claim",
                ExpenseCaseLink.resource_id == claim.id,
            )
        )
        if existing_link is not None:
            if (
                existing_link.tenant_id != normalized_tenant
                or existing_link.expense_case_id != expense_case.id
            ):
                raise PermissionError("费用单据已经关联到其他费用事件。")
            return existing_link

        link = ExpenseCaseLink(
            id=str(uuid.uuid4()),
            tenant_id=normalized_tenant,
            expense_case_id=expense_case.id,
            resource_type="expense_claim",
            resource_id=claim.id,
            relation_type=relation_type,
        )
        self.db.add(link)
        self.db.flush()
        return link

    def link_resource(
        self,
        expense_case: ExpenseCase,
        *,
        resource_type: str,
        resource_id: str,
        relation_type: str,
        tenant_id: str | None = None,
    ) -> ExpenseCaseLink:
        """把非 Claim 资源幂等关联到费用事件，不负责提交事务。"""
        normalized_tenant = self.normalize_tenant_id(tenant_id or expense_case.tenant_id)
        normalized_type = str(resource_type or "").strip()
        normalized_id = str(resource_id or "").strip()
        normalized_relation = str(relation_type or "").strip()
        if expense_case.tenant_id != normalized_tenant:
            raise PermissionError("不能把资源关联到其他租户的费用事件。")
        if not normalized_type or not normalized_id or not normalized_relation:
            raise ValueError("费用事件资源关联缺少必要字段。")

        existing_link = self.db.scalar(
            select(ExpenseCaseLink).where(
                ExpenseCaseLink.resource_type == normalized_type,
                ExpenseCaseLink.resource_id == normalized_id,
            )
        )
        if existing_link is not None:
            if (
                existing_link.tenant_id != normalized_tenant
                or existing_link.expense_case_id != expense_case.id
            ):
                raise PermissionError("资源已经关联到其他费用事件。")
            return existing_link

        link = ExpenseCaseLink(
            id=str(uuid.uuid4()),
            tenant_id=normalized_tenant,
            expense_case_id=expense_case.id,
            resource_type=normalized_type,
            resource_id=normalized_id,
            relation_type=normalized_relation,
        )
        self.db.add(link)
        self.db.flush()
        return link

    def record_resource_event(
        self,
        expense_case: ExpenseCase,
        *,
        aggregate_type: str,
        aggregate_id: str,
        event_type: str,
        actor_id: str,
        idempotency_key: str,
        tenant_id: str | None = None,
        correlation_id: str | None = None,
        causation_id: str | None = None,
        payload: dict[str, Any] | None = None,
        delivery_status: str = "pending",
    ) -> BusinessEvent:
        """为票据等通用资源写入幂等业务事件，不负责提交事务。"""
        normalized_tenant = self.normalize_tenant_id(tenant_id or expense_case.tenant_id)
        normalized_type = str(aggregate_type or "").strip()
        normalized_id = str(aggregate_id or "").strip()
        normalized_event_type = str(event_type or "").strip()
        if expense_case.tenant_id != normalized_tenant:
            raise PermissionError("不能向其他租户的费用事件写入业务事件。")
        if not normalized_type or not normalized_id or not normalized_event_type:
            raise ValueError("业务事件缺少聚合或事件类型。")

        normalized_idempotency_key = self._normalize_idempotency_key(idempotency_key)
        existing_event = self.db.scalar(
            select(BusinessEvent).where(
                BusinessEvent.tenant_id == normalized_tenant,
                BusinessEvent.aggregate_type == normalized_type,
                BusinessEvent.aggregate_id == normalized_id,
                BusinessEvent.event_type == normalized_event_type,
                BusinessEvent.idempotency_key == normalized_idempotency_key,
            )
        )
        if existing_event is not None:
            if existing_event.expense_case_id != expense_case.id:
                raise PermissionError("业务事件已经属于其他费用事件。")
            return existing_event

        normalized_correlation_id = self.normalize_correlation_id(correlation_id)
        event = BusinessEvent(
            id=str(uuid.uuid4()),
            tenant_id=normalized_tenant,
            expense_case_id=expense_case.id,
            aggregate_type=normalized_type,
            aggregate_id=normalized_id,
            event_type=normalized_event_type,
            event_version=1,
            idempotency_key=normalized_idempotency_key,
            correlation_id=normalized_correlation_id,
            causation_id=self.normalize_correlation_id(causation_id) if causation_id else None,
            actor_id=str(actor_id or "system").strip() or "system",
            actor_type="system" if str(actor_id or "").strip() == "system" else "user",
            payload_json=dict(payload or {}),
            delivery_status=str(delivery_status or "pending").strip() or "pending",
            occurred_at=datetime.now(UTC),
        )
        self.db.add(event)
        self.db.flush()
        return event

    @staticmethod
    def mark_claiming_started(expense_case: ExpenseCase) -> None:
        """首个报销资源进入 Case 后推进阶段，但不回退已进入审批的 Case。"""
        if str(expense_case.current_stage or "").strip() == "approved_to_spend":
            expense_case.current_stage = "claiming"
        if str(expense_case.status or "").strip() not in {"closed", "cancelled"}:
            expense_case.status = "active"

    def record_claim_event(
        self,
        claim: ExpenseClaim,
        *,
        event_type: str,
        actor_id: str,
        tenant_id: str | None = None,
        correlation_id: str | None = None,
        idempotency_key: str | None = None,
        causation_id: str | None = None,
        previous_status: str | None = None,
        previous_approval_stage: str | None = None,
        extra_payload: dict[str, Any] | None = None,
        expense_case: ExpenseCase | None = None,
        relation_type: str | None = None,
        update_case_state: bool = True,
        delivery_status: str = "pending",
    ) -> tuple[ExpenseCase, BusinessEvent]:
        normalized_tenant = self.normalize_tenant_id(tenant_id)
        if expense_case is None:
            expense_case = self.ensure_case_for_claim(
                claim,
                tenant_id=normalized_tenant,
                relation_type=relation_type,
            )
        else:
            self.link_claim(
                expense_case,
                claim,
                tenant_id=normalized_tenant,
                relation_type=relation_type or self._relation_type(claim),
            )

        if update_case_state:
            expense_case.current_stage = self._stage_for_claim(claim)
            expense_case.status = self._case_status_for_claim(claim)
        if claim.employee_id and not expense_case.owner_employee_id:
            expense_case.owner_employee_id = claim.employee_id

        payload: dict[str, Any] = {
            "claim_no": str(claim.claim_no or ""),
            "expense_type": str(claim.expense_type or ""),
            "amount": self._money_text(claim.amount),
            "currency": str(claim.currency or "CNY"),
            "previous_status": str(previous_status or ""),
            "previous_approval_stage": str(previous_approval_stage or ""),
            "next_status": str(claim.status or ""),
            "next_approval_stage": str(claim.approval_stage or ""),
        }
        payload.update(extra_payload or {})

        normalized_correlation_id = self.normalize_correlation_id(correlation_id)
        normalized_idempotency_key = self._normalize_idempotency_key(
            idempotency_key or normalized_correlation_id
        )
        existing_event = self.db.scalar(
            select(BusinessEvent).where(
                BusinessEvent.tenant_id == normalized_tenant,
                BusinessEvent.aggregate_type == "expense_claim",
                BusinessEvent.aggregate_id == claim.id,
                BusinessEvent.event_type == str(event_type).strip(),
                BusinessEvent.idempotency_key == normalized_idempotency_key,
            )
        )
        if existing_event is not None:
            self._record_workflow_learning(existing_event)
            return expense_case, existing_event

        event = BusinessEvent(
            id=str(uuid.uuid4()),
            tenant_id=normalized_tenant,
            expense_case_id=expense_case.id,
            aggregate_type="expense_claim",
            aggregate_id=claim.id,
            event_type=str(event_type).strip(),
            event_version=1,
            idempotency_key=normalized_idempotency_key,
            correlation_id=normalized_correlation_id,
            causation_id=self.normalize_correlation_id(causation_id) if causation_id else None,
            actor_id=str(actor_id or "system").strip() or "system",
            actor_type="system" if str(actor_id or "").strip() == "system" else "user",
            payload_json=payload,
            delivery_status=str(delivery_status or "pending").strip() or "pending",
            occurred_at=datetime.now(UTC),
        )
        self.db.add(event)
        self.db.flush()
        self._record_workflow_learning(event)
        return expense_case, event

    def _record_workflow_learning(self, event: BusinessEvent) -> None:
        """在业务事件事务内同步生成可信 AI 结果证据，未命中事件会立即返回。"""

        from app.services.expense_workflow_learning import (
            ExpenseWorkflowLearningService,
        )

        ExpenseWorkflowLearningService(self.db).record_event(event)

    @staticmethod
    def _normalize_idempotency_key(value: str) -> str:
        text = str(value or "").strip()
        if not text:
            raise ValueError("业务事件必须提供幂等键。")
        if len(text) <= 120:
            return text
        digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
        return f"sha256:{digest}"

    def get_timeline_for_claim(
        self,
        claim_id: str,
        *,
        tenant_id: str | None = None,
    ) -> ExpenseCase | None:
        normalized_tenant = self.normalize_tenant_id(tenant_id)
        link = self.db.scalar(
            select(ExpenseCaseLink).where(
                ExpenseCaseLink.resource_type == "expense_claim",
                ExpenseCaseLink.resource_id == claim_id,
            )
        )
        if link is None or link.tenant_id != normalized_tenant:
            return None
        return self.db.scalar(
            select(ExpenseCase)
            .options(selectinload(ExpenseCase.links), selectinload(ExpenseCase.events))
            .where(
                ExpenseCase.id == link.expense_case_id,
                ExpenseCase.tenant_id == normalized_tenant,
            )
        )

    @staticmethod
    def _money_text(value: Decimal | None) -> str:
        return f"{Decimal(value or Decimal('0.00')).quantize(Decimal('0.01')):.2f}"

    @staticmethod
    def _scene_code(claim: ExpenseClaim) -> str:
        expense_type = str(claim.expense_type or "").strip().lower()
        return expense_type.removesuffix("_application") or "other"

    @staticmethod
    def _case_title(claim: ExpenseClaim) -> str:
        reason = str(claim.reason or "").strip()
        return reason[:200] or f"费用事件 {claim.claim_no}"

    @classmethod
    def _relation_type(cls, claim: ExpenseClaim) -> str:
        claim_no = str(claim.claim_no or "").strip()
        expense_type = str(claim.expense_type or "").strip().lower()
        is_application = (
            is_application_claim_no(claim_no)
            or expense_type == "application"
            or expense_type.endswith("_application")
        )
        return "application" if is_application else "claim"

    @classmethod
    def _stage_for_claim(cls, claim: ExpenseClaim) -> str:
        status = str(claim.status or "").strip().lower()
        if status in {"pending_payment"}:
            return "paying"
        if status in {"paid"}:
            return "accounting"
        if status in {"completed"}:
            return "closed"
        if status in {"submitted", "approved"}:
            if cls._relation_type(claim) == "application" and status == "approved":
                return "approved_to_spend"
            return "reviewing"
        return "claiming"

    @staticmethod
    def _case_status_for_claim(claim: ExpenseClaim) -> str:
        status = str(claim.status or "").strip().lower()
        if status in {"cancelled", "voided", "rejected"}:
            return "cancelled"
        if status == "completed":
            return "closed"
        return "active"
