from __future__ import annotations

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

from sqlalchemy import or_, select

from app.models.financial_record import ExpenseClaim
from app.services.expense_claim_risk_stage import with_risk_business_stage
from app.services.expense_claim_workflow_constants import (
    APPLICATION_ARCHIVE_STAGE,
    APPLICATION_LINK_STATUS_STAGE,
)


APPLICATION_REIMBURSEMENT_TYPE_MAP = {
    "travel_application": "travel",
    "purchase_application": "office",
    "meeting_application": "meeting",
    "expense_application": "other",
    "application": "other",
}
APPLICATION_LINK_FLAG_SOURCES = {"application_handoff", "application_link"}


class ExpenseClaimApplicationHandoffMixin:
    @staticmethod
    def _resolve_reimbursement_type_from_application(expense_type: str | None) -> str:
        normalized = str(expense_type or "").strip().lower()
        if normalized in APPLICATION_REIMBURSEMENT_TYPE_MAP:
            return APPLICATION_REIMBURSEMENT_TYPE_MAP[normalized]
        if normalized.endswith("_application"):
            return normalized.removesuffix("_application") or "other"
        return normalized or "other"

    @staticmethod
    def _resolve_application_detail(application_claim: ExpenseClaim) -> dict[str, str]:
        for flag in list(application_claim.risk_flags_json or []):
            if not isinstance(flag, dict) or str(flag.get("source") or "").strip() != "application_detail":
                continue
            detail = flag.get("application_detail") or flag.get("applicationDetail") or {}
            if isinstance(detail, dict):
                return {str(key): str(value or "").strip() for key, value in detail.items()}
        return {}

    @staticmethod
    def _build_application_handoff_detail(application_claim: ExpenseClaim) -> dict[str, str]:
        detail = ExpenseClaimApplicationHandoffMixin._resolve_application_detail(application_claim)
        application_time = str(detail.get("time") or "").strip()
        if not application_time and application_claim.occurred_at is not None:
            application_time = application_claim.occurred_at.isoformat()

        application_amount = str(detail.get("amount") or "").strip()
        if not application_amount:
            application_amount = str(application_claim.amount or Decimal("0.00"))

        return {
            "application_type": str(detail.get("application_type") or application_claim.expense_type or "").strip(),
            "application_content": " / ".join(
                item
                for item in [
                    str(detail.get("application_type") or application_claim.expense_type or "").strip(),
                    str(detail.get("location") or application_claim.location or "").strip(),
                ]
                if item
            ),
            "application_reason": str(detail.get("reason") or application_claim.reason or "").strip(),
            "application_days": str(detail.get("days") or "").strip(),
            "application_location": str(detail.get("location") or application_claim.location or "").strip(),
            "application_amount": application_amount,
            "application_time": application_time,
            "application_transport_mode": str(detail.get("transport_mode") or "").strip(),
            "application_lodging_daily_cap": str(detail.get("lodging_daily_cap") or "").strip(),
            "application_subsidy_daily_cap": str(detail.get("subsidy_daily_cap") or "").strip(),
            "application_transport_policy": str(detail.get("transport_policy") or "").strip(),
            "application_policy_estimate": str(detail.get("policy_estimate") or "").strip(),
            "application_rule_name": str(detail.get("rule_name") or "").strip(),
            "application_rule_version": str(detail.get("rule_version") or "").strip(),
        }

    def _create_reimbursement_draft_from_application(
        self,
        *,
        application_claim: ExpenseClaim,
        approval_flag: dict[str, Any],
        operator: str,
    ) -> ExpenseClaim:
        occurred_at = application_claim.occurred_at or datetime.now(UTC)
        created_at = datetime.now(UTC)
        draft_claim = ExpenseClaim(
            tenant_id=application_claim.tenant_id,
            claim_no=self._generate_claim_no(occurred_at),
            employee_id=application_claim.employee_id,
            employee_name=application_claim.employee_name,
            department_id=application_claim.department_id,
            department_name=application_claim.department_name,
            project_code=application_claim.project_code,
            expense_type=self._resolve_reimbursement_type_from_application(application_claim.expense_type),
            reason=application_claim.reason,
            location=application_claim.location,
            amount=application_claim.amount or Decimal("0.00"),
            currency=application_claim.currency or "CNY",
            invoice_count=0,
            occurred_at=occurred_at,
            submitted_at=None,
            status="draft",
            approval_stage="待提交",
            risk_flags_json=[
                {
                    "source": "application_handoff",
                    "event_type": "expense_application_to_reimbursement_draft",
                    "handoff_event_id": str(uuid.uuid4()),
                    "severity": "info",
                    "label": "申请转报销草稿",
                    "message": (
                        f"费用申请 {application_claim.claim_no} 已由 {operator} 确认审核，"
                        "系统已生成报销草稿。"
                    ),
                    "application_claim_id": application_claim.id,
                    "application_claim_no": application_claim.claim_no,
                    "application_budget_amount": str(application_claim.amount or Decimal("0.00")),
                    "application_detail": self._build_application_handoff_detail(application_claim),
                    "application_approval_event_id": str(approval_flag.get("approval_event_id") or ""),
                    "leader_opinion": str(
                        approval_flag.get("leader_opinion") or approval_flag.get("opinion") or ""
                    ).strip(),
                    "budget_opinion": str(approval_flag.get("budget_opinion") or "").strip(),
                    "created_at": created_at.isoformat(),
                }
            ],
        )
        self.db.add(draft_claim)
        self.db.flush()

        approval_flag["generated_draft_claim_id"] = draft_claim.id
        approval_flag["generated_draft_claim_no"] = draft_claim.claim_no
        approval_flag["handoff_event_type"] = "expense_application_to_reimbursement_draft"
        approval_flag["handoff_message"] = f"已生成报销草稿 {draft_claim.claim_no}。"
        return draft_claim

    @staticmethod
    def _collect_application_references_from_reimbursement(claim: ExpenseClaim) -> tuple[set[str], set[str]]:
        application_ids: set[str] = set()
        application_nos: set[str] = set()
        for flag in list(claim.risk_flags_json or []):
            if not isinstance(flag, dict):
                continue
            source = str(flag.get("source") or "").strip()
            has_application_reference = any(
                str(flag.get(key) or "").strip()
                for key in (
                    "application_claim_id",
                    "applicationClaimId",
                    "application_claim_no",
                    "applicationClaimNo",
                )
            )
            if source not in APPLICATION_LINK_FLAG_SOURCES and not has_application_reference:
                continue
            application_id = str(flag.get("application_claim_id") or flag.get("applicationClaimId") or "").strip()
            application_no = str(flag.get("application_claim_no") or flag.get("applicationClaimNo") or "").strip()
            if application_id:
                application_ids.add(application_id)
            if application_no:
                application_nos.add(application_no)
        return application_ids, application_nos

    def _find_linked_application_claims(self, reimbursement_claim: ExpenseClaim) -> list[ExpenseClaim]:
        application_ids, application_nos = self._collect_application_references_from_reimbursement(reimbursement_claim)
        conditions = []
        if application_ids:
            conditions.append(ExpenseClaim.id.in_(application_ids))
        if application_nos:
            conditions.append(ExpenseClaim.claim_no.in_(application_nos))
        if not conditions:
            return []

        claims = list(self.db.scalars(select(ExpenseClaim).where(or_(*conditions))).all())
        return [claim for claim in claims if self._is_expense_application_claim(claim)]

    def _archive_linked_applications_after_reimbursement_paid(
        self,
        *,
        reimbursement_claim: ExpenseClaim,
        payment_flag: dict[str, Any],
        operator: str,
        current_user: Any,
    ) -> list[dict[str, str]]:
        archived_applications: list[dict[str, str]] = []
        payment_event_id = str(payment_flag.get("payment_event_id") or "").strip()
        for application_claim in self._find_linked_application_claims(reimbursement_claim):
            previous_status = str(application_claim.status or "").strip()
            previous_stage = str(application_claim.approval_stage or "").strip()
            if previous_stage == APPLICATION_ARCHIVE_STAGE:
                continue

            normalized_status = previous_status.lower()
            if normalized_status not in {"approved", "completed"}:
                continue

            before_json = self._serialize_claim(application_claim)
            archive_flag = with_risk_business_stage(
                {
                    "source": "application_archive_sync",
                    "event_type": "expense_application_archived_by_reimbursement",
                    "archive_event_id": str(uuid.uuid4()),
                    "severity": "info",
                    "label": "申请归档",
                    "message": (
                        f"关联报销单 {reimbursement_claim.claim_no} 已完成付款，"
                        "系统同步将申请单归档。"
                    ),
                    "operator": operator,
                    "operator_username": getattr(current_user, "username", ""),
                    "operator_role_codes": [
                        str(item).strip().lower()
                        for item in getattr(current_user, "role_codes", [])
                        if str(item).strip()
                    ],
                    "application_claim_id": application_claim.id,
                    "application_claim_no": application_claim.claim_no,
                    "reimbursement_claim_id": reimbursement_claim.id,
                    "reimbursement_claim_no": reimbursement_claim.claim_no,
                    "payment_event_id": payment_event_id,
                    "previous_status": previous_status,
                    "previous_approval_stage": previous_stage,
                    "next_status": "approved",
                    "next_approval_stage": APPLICATION_ARCHIVE_STAGE,
                    "created_at": datetime.now(UTC).isoformat(),
                },
                "expense_application",
            )
            application_claim.status = "approved"
            application_claim.approval_stage = APPLICATION_ARCHIVE_STAGE
            application_claim.risk_flags_json = [*list(application_claim.risk_flags_json or []), archive_flag]
            archived_applications.append(
                {
                    "application_claim_id": application_claim.id,
                    "application_claim_no": str(application_claim.claim_no or "").strip(),
                    "archive_event_id": str(archive_flag.get("archive_event_id") or ""),
                    "previous_status": previous_status,
                    "previous_approval_stage": previous_stage,
                    "next_status": "approved",
                    "next_approval_stage": APPLICATION_ARCHIVE_STAGE,
                }
            )
            self.audit_service.log_action(
                actor=operator,
                action="expense_application.archive_by_reimbursement",
                resource_type="expense_claim",
                resource_id=application_claim.id,
                before_json=before_json,
                after_json=self._serialize_claim(application_claim),
                commit=False,
            )

        return archived_applications

    @staticmethod
    def _reference_matches_deleted_reimbursement(
        flag: dict[str, Any],
        *,
        reimbursement_claim_id: str,
        reimbursement_claim_no: str,
    ) -> bool:
        reference_ids = {
            str(flag.get(key) or "").strip()
            for key in (
                "generated_draft_claim_id",
                "generatedDraftClaimId",
                "reimbursement_claim_id",
                "reimbursementClaimId",
            )
        }
        reference_nos = {
            str(flag.get(key) or "").strip().upper()
            for key in (
                "generated_draft_claim_no",
                "generatedDraftClaimNo",
                "reimbursement_claim_no",
                "reimbursementClaimNo",
            )
        }
        return (
            bool(reimbursement_claim_id and reimbursement_claim_id in reference_ids)
            or bool(reimbursement_claim_no and reimbursement_claim_no.upper() in reference_nos)
        )

    @classmethod
    def _remove_deleted_reimbursement_link_references(
        cls,
        risk_flags: list[Any],
        *,
        reimbursement_claim_id: str,
        reimbursement_claim_no: str,
    ) -> tuple[list[Any], bool]:
        next_flags: list[Any] = []
        changed = False
        stale_link_keys = {
            "generated_draft_claim_id",
            "generatedDraftClaimId",
            "generated_draft_claim_no",
            "generatedDraftClaimNo",
            "reimbursement_claim_id",
            "reimbursementClaimId",
            "reimbursement_claim_no",
            "reimbursementClaimNo",
            "handoff_event_type",
            "handoffEventType",
            "handoff_message",
            "handoffMessage",
        }
        for flag in list(risk_flags or []):
            if not isinstance(flag, dict):
                next_flags.append(flag)
                continue
            if not cls._reference_matches_deleted_reimbursement(
                flag,
                reimbursement_claim_id=reimbursement_claim_id,
                reimbursement_claim_no=reimbursement_claim_no,
            ):
                next_flags.append(flag)
                continue

            next_flag = dict(flag)
            for key in stale_link_keys:
                if key in next_flag:
                    next_flag.pop(key, None)
                    changed = True
            next_flags.append(next_flag)
        return next_flags, changed

    def _sync_linked_applications_after_reimbursement_deleted(
        self,
        *,
        reimbursement_claim: ExpenseClaim,
        operator: str,
        current_user: Any,
    ) -> list[dict[str, str]]:
        reimbursement_claim_id = str(reimbursement_claim.id or "").strip()
        reimbursement_claim_no = str(reimbursement_claim.claim_no or "").strip()
        synced_applications: list[dict[str, str]] = []

        for application_claim in self._find_linked_application_claims(reimbursement_claim):
            previous_status = str(application_claim.status or "").strip()
            previous_stage = str(application_claim.approval_stage or "").strip()
            before_json = self._serialize_claim(application_claim)
            next_flags, removed_link_references = self._remove_deleted_reimbursement_link_references(
                list(application_claim.risk_flags_json or []),
                reimbursement_claim_id=reimbursement_claim_id,
                reimbursement_claim_no=reimbursement_claim_no,
            )
            sync_flag = with_risk_business_stage(
                {
                    "source": "application_link_sync",
                    "event_type": "expense_application_reimbursement_deleted",
                    "sync_event_id": str(uuid.uuid4()),
                    "severity": "info",
                    "actionability": "system_trace",
                    "label": "关联报销单已删除",
                    "message": (
                        f"关联报销单 {reimbursement_claim_no or reimbursement_claim_id} 已删除，"
                        "申请单已回到待关联状态。"
                    ),
                    "operator": operator,
                    "operator_username": getattr(current_user, "username", ""),
                    "operator_role_codes": [
                        str(item).strip().lower()
                        for item in getattr(current_user, "role_codes", [])
                        if str(item).strip()
                    ],
                    "application_claim_id": application_claim.id,
                    "application_claim_no": application_claim.claim_no,
                    "deleted_reimbursement_claim_id": reimbursement_claim_id,
                    "deleted_reimbursement_claim_no": reimbursement_claim_no,
                    "previous_status": previous_status,
                    "previous_approval_stage": previous_stage,
                    "next_status": "approved",
                    "next_approval_stage": APPLICATION_LINK_STATUS_STAGE,
                    "removed_link_references": removed_link_references,
                    "created_at": datetime.now(UTC).isoformat(),
                },
                "expense_application",
            )

            application_claim.status = "approved"
            application_claim.approval_stage = APPLICATION_LINK_STATUS_STAGE
            application_claim.risk_flags_json = [*next_flags, sync_flag]
            synced_applications.append(
                {
                    "application_claim_id": application_claim.id,
                    "application_claim_no": str(application_claim.claim_no or "").strip(),
                    "next_approval_stage": APPLICATION_LINK_STATUS_STAGE,
                }
            )
            self.audit_service.log_action(
                actor=operator,
                action="expense_application.unlink_deleted_reimbursement",
                resource_type="expense_claim",
                resource_id=application_claim.id,
                before_json=before_json,
                after_json=self._serialize_claim(application_claim),
                commit=False,
            )

        return synced_applications
