from __future__ import annotations

import re
from datetime import UTC, datetime
from decimal import Decimal, InvalidOperation
from typing import Any

from sqlalchemy import select

from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.services.expense_claim_constants import (
    DOCUMENT_FACT_ITEM_TYPES,
    SYSTEM_GENERATED_ITEM_TYPES,
)

APPROVED_APPLICATION_LINK_STATUSES = {"approved", "completed"}
INACTIVE_APPLICATION_LINK_REIMBURSEMENT_STATUSES = {
    "cancelled",
    "canceled",
    "deleted",
}


class ExpenseClaimApplicationLinkMixin:
    def _sync_application_link_draft_without_items(self, claim: ExpenseClaim) -> None:
        claim.amount = Decimal("0.00")
        claim.invoice_count = 0
        claim.risk_flags_json = self._merge_claim_attachment_risk_flags(claim, [])
        claim.risk_flags_json = self._merge_claim_platform_risk_preview_flags(claim, [])

    def _clear_application_link_placeholder_items(
        self,
        claim: ExpenseClaim,
        *,
        context_json: dict[str, Any],
    ) -> None:
        application_amounts = self._resolve_application_amount_candidates(context_json)
        for item in list(claim.items or []):
            if not self._is_application_link_placeholder_item(
                item,
                claim=claim,
                context_json=context_json,
                application_amounts=application_amounts,
            ):
                continue
            claim.items.remove(item)
            self.db.delete(item)

    def _is_application_link_placeholder_item(
        self,
        item: ExpenseClaimItem,
        *,
        claim: ExpenseClaim,
        context_json: dict[str, Any],
        application_amounts: set[Decimal],
    ) -> bool:
        if str(item.invoice_id or "").strip():
            return False

        item_type = str(item.item_type or "").strip().lower()
        if item_type in DOCUMENT_FACT_ITEM_TYPES:
            return False
        if item_type in SYSTEM_GENERATED_ITEM_TYPES:
            return True

        claim_type = str(claim.expense_type or "").strip().lower()
        if item_type and claim_type and item_type != claim_type:
            return False

        amount = self._parse_context_money_amount(item.item_amount)
        if (
            application_amounts
            and amount is not None
            and amount > Decimal("0.00")
            and amount not in application_amounts
        ):
            return False

        reason = str(item.item_reason or "").strip()
        if not reason or reason == "待补充":
            return True

        review_values = self._normalize_context_object(
            context_json.get("review_form_values")
        )
        linked_reasons = {
            str(review_values.get(key) or "").strip()
            for key in ("application_reason", "reason", "business_reason")
        }
        linked_reasons.add(str(claim.reason or "").strip())
        return reason in {value for value in linked_reasons if value}

    def _should_skip_application_link_placeholder_item(
        self,
        *,
        claim: ExpenseClaim | None,
        context_json: dict[str, Any],
        document_specs: list[dict[str, Any]],
        attachment_count: int,
        amount: Decimal | None,
    ) -> bool:
        if document_specs or attachment_count > 0:
            return False
        if self._build_application_link_flag(context_json) is None:
            return False

        return True

    @classmethod
    def _resolve_application_amount_candidates(
        cls,
        context_json: dict[str, Any],
    ) -> set[Decimal]:
        review_values = cls._normalize_context_object(
            context_json.get("review_form_values")
        )
        scene_selection = cls._normalize_context_object(
            context_json.get("expense_scene_selection")
        )
        candidates: set[Decimal] = set()
        for source in (review_values, scene_selection, context_json):
            for key in (
                "application_amount",
                "application_amount_label",
                "applicationAmount",
                "applicationAmountLabel",
            ):
                parsed = cls._parse_context_money_amount(source.get(key))
                if parsed is not None:
                    candidates.add(parsed)
        return candidates

    @staticmethod
    def _parse_context_money_amount(value: Any) -> Decimal | None:
        raw_value = str(value or "").strip()
        if not raw_value:
            return None
        compact = re.sub(r"[^\d.\-]", "", raw_value.replace(",", ""))
        if not compact or compact in {"-", ".", "-."}:
            return None
        try:
            return Decimal(compact).quantize(Decimal("0.01"))
        except (InvalidOperation, ValueError):
            return None

    @staticmethod
    def _merge_application_link_flag(
        risk_flags: list[Any],
        *,
        context_json: dict[str, Any],
    ) -> list[Any]:
        link_flag = ExpenseClaimApplicationLinkMixin._build_application_link_flag(
            context_json
        )
        if link_flag is None:
            return list(risk_flags or [])

        application_claim_no = str(link_flag.get("application_claim_no") or "").strip()
        for flag in list(risk_flags or []):
            if not isinstance(flag, dict):
                continue
            existing_no = str(
                flag.get("application_claim_no")
                or flag.get("applicationClaimNo")
                or ""
            ).strip()
            if existing_no and existing_no == application_claim_no:
                return list(risk_flags or [])
        return [*list(risk_flags or []), link_flag]

    def _build_application_link_block_result(
        self,
        *,
        context_json: dict[str, Any],
        target_claim: ExpenseClaim | None,
    ) -> dict[str, Any] | None:
        link_flag = self._build_application_link_flag(context_json)
        if link_flag is None:
            return None

        application_claim = self._find_application_claim_for_link(link_flag)
        application_claim_no = str(
            link_flag.get("application_claim_no") or ""
        ).strip()
        display_no = application_claim_no or "未编号申请单"
        if application_claim is None or not self._is_expense_application_claim(
            application_claim
        ):
            return self._build_application_link_rejected_result(
                f"未找到可关联的申请单 {display_no}。请先选择已审批通过的申请单。",
            )

        normalized_status = str(application_claim.status or "").strip().lower()
        if normalized_status not in APPROVED_APPLICATION_LINK_STATUSES:
            return self._build_application_link_rejected_result(
                f"申请单 {application_claim.claim_no} 当前不是已审批通过状态，不能用于快速报销关联。",
                application_claim=application_claim,
            )

        existing_reimbursement = self._find_existing_reimbursement_for_application_link(
            application_claim=application_claim,
            link_flag=link_flag,
            target_claim=target_claim,
        )
        if existing_reimbursement is not None:
            return self._build_application_link_rejected_result(
                (
                    f"申请单 {application_claim.claim_no} 已经关联报销单 "
                    f"{existing_reimbursement.claim_no}。"
                    "请进入该草稿或单据继续补充，不能重复生成。"
                ),
                application_claim=application_claim,
                existing_claim=existing_reimbursement,
            )

        return None

    def _find_application_claim_for_link(
        self,
        link_flag: dict[str, Any],
    ) -> ExpenseClaim | None:
        application_claim_id = str(
            link_flag.get("application_claim_id") or ""
        ).strip()
        application_claim_no = str(
            link_flag.get("application_claim_no") or ""
        ).strip()

        if application_claim_id:
            claim = self.db.get(ExpenseClaim, application_claim_id)
            if claim is not None and self._is_expense_application_claim(claim):
                return claim

        if application_claim_no:
            return self.db.scalar(
                select(ExpenseClaim)
                .where(ExpenseClaim.claim_no == application_claim_no)
                .limit(1)
            )

        return None

    def _find_existing_reimbursement_for_application_link(
        self,
        *,
        application_claim: ExpenseClaim,
        link_flag: dict[str, Any],
        target_claim: ExpenseClaim | None,
    ) -> ExpenseClaim | None:
        generated_draft = self._find_generated_reimbursement_from_application(
            application_claim=application_claim,
            target_claim=target_claim,
        )
        if generated_draft is not None:
            return generated_draft

        linked_ids, linked_nos = self._collect_application_link_reference_values(
            link_flag
        )
        linked_ids.add(str(application_claim.id or "").strip())
        linked_nos.add(str(application_claim.claim_no or "").strip().upper())
        linked_ids.discard("")
        linked_nos.discard("")

        for claim in list(self.db.scalars(select(ExpenseClaim)).all()):
            if self._is_same_target_claim(claim, target_claim):
                continue
            if self._is_expense_application_claim(claim):
                continue
            if self._is_inactive_application_link_reimbursement(claim):
                continue
            if self._claim_references_application(
                claim,
                linked_ids=linked_ids,
                linked_nos=linked_nos,
            ):
                return claim
        return None

    def _find_generated_reimbursement_from_application(
        self,
        *,
        application_claim: ExpenseClaim,
        target_claim: ExpenseClaim | None,
    ) -> ExpenseClaim | None:
        for flag in list(application_claim.risk_flags_json or []):
            if not isinstance(flag, dict):
                continue
            generated_draft_id = str(
                flag.get("generated_draft_claim_id")
                or flag.get("generatedDraftClaimId")
                or ""
            ).strip()
            generated_draft_no = str(
                flag.get("generated_draft_claim_no")
                or flag.get("generatedDraftClaimNo")
                or ""
            ).strip()

            claim = (
                self.db.get(ExpenseClaim, generated_draft_id)
                if generated_draft_id
                else None
            )
            if claim is None and generated_draft_no:
                claim = self.db.scalar(
                    select(ExpenseClaim)
                    .where(ExpenseClaim.claim_no == generated_draft_no)
                    .limit(1)
                )
            if claim is None:
                continue
            if self._is_same_target_claim(claim, target_claim):
                continue
            if self._is_expense_application_claim(claim):
                continue
            if self._is_inactive_application_link_reimbursement(claim):
                continue
            return claim
        return None

    @staticmethod
    def _is_same_target_claim(
        claim: ExpenseClaim,
        target_claim: ExpenseClaim | None,
    ) -> bool:
        return bool(target_claim is not None and claim.id == target_claim.id)

    @staticmethod
    def _is_inactive_application_link_reimbursement(claim: ExpenseClaim) -> bool:
        status = str(claim.status or "").strip().lower()
        return status in INACTIVE_APPLICATION_LINK_REIMBURSEMENT_STATUSES

    @classmethod
    def _claim_references_application(
        cls,
        claim: ExpenseClaim,
        *,
        linked_ids: set[str],
        linked_nos: set[str],
    ) -> bool:
        for flag in list(claim.risk_flags_json or []):
            flag_ids, flag_nos = cls._collect_application_link_reference_values(flag)
            if flag_ids.intersection(linked_ids) or flag_nos.intersection(linked_nos):
                return True
        return False

    @classmethod
    def _collect_application_link_reference_values(
        cls,
        payload: Any,
    ) -> tuple[set[str], set[str]]:
        ids: set[str] = set()
        claim_nos: set[str] = set()
        if not isinstance(payload, dict):
            return ids, claim_nos

        cls._add_application_link_reference(ids, claim_nos, payload)
        for key in (
            "application_detail",
            "applicationDetail",
            "review_form_values",
            "reviewFormValues",
            "expense_scene_selection",
            "expenseSceneSelection",
        ):
            nested_ids, nested_nos = cls._collect_application_link_reference_values(
                payload.get(key)
            )
            ids.update(nested_ids)
            claim_nos.update(nested_nos)
        ids.discard("")
        claim_nos.discard("")
        return ids, claim_nos

    @staticmethod
    def _add_application_link_reference(
        ids: set[str],
        claim_nos: set[str],
        payload: dict[str, Any],
    ) -> None:
        for key in ("application_claim_id", "applicationClaimId"):
            ids.add(str(payload.get(key) or "").strip())
        for key in ("application_claim_no", "applicationClaimNo"):
            claim_nos.add(str(payload.get(key) or "").strip().upper())

    @staticmethod
    def _build_application_link_rejected_result(
        message: str,
        *,
        application_claim: ExpenseClaim | None = None,
        existing_claim: ExpenseClaim | None = None,
    ) -> dict[str, Any]:
        result: dict[str, Any] = {
            "message": message,
            "draft_only": False,
            "status": "blocked",
            "application_link_blocked": True,
            "submission_blocked": True,
            "submission_blocked_reasons": [message],
            "missing_fields": [message],
            "risk_flags": ["application_link_blocked"],
        }
        if application_claim is not None:
            result["application_claim_id"] = application_claim.id
            result["application_claim_no"] = application_claim.claim_no
            result["application_status"] = application_claim.status
        if existing_claim is not None:
            result["existing_claim_id"] = existing_claim.id
            result["existing_claim_no"] = existing_claim.claim_no
            result["existing_claim_status"] = existing_claim.status
        return result

    @staticmethod
    def _build_application_link_flag(
        context_json: dict[str, Any],
    ) -> dict[str, Any] | None:
        review_values = ExpenseClaimApplicationLinkMixin._normalize_context_object(
            context_json.get("review_form_values")
        )
        scene_selection = ExpenseClaimApplicationLinkMixin._normalize_context_object(
            context_json.get("expense_scene_selection")
        )

        def pick(*keys: str) -> str:
            for source in (review_values, scene_selection, context_json):
                for key in keys:
                    value = str(source.get(key) or "").strip()
                    if value:
                        return value
            return ""

        application_claim_no = pick("application_claim_no", "applicationClaimNo")
        if not application_claim_no:
            return None

        application_claim_id = pick("application_claim_id", "applicationClaimId")
        application_amount = pick("application_amount", "applicationAmount")
        application_amount_label = pick(
            "application_amount_label", "applicationAmountLabel"
        )
        application_reason = pick(
            "application_reason", "applicationReason", "reason"
        )
        application_location = pick(
            "application_location", "applicationLocation", "location"
        )
        application_time = pick(
            "application_business_time",
            "applicationBusinessTime",
            "application_time",
            "applicationTime",
            "business_time",
            "businessTime",
            "time_range",
            "timeRange",
            "time",
        )
        application_date = pick("application_date", "applicationDate")
        application_days = pick("application_days", "applicationDays", "days")
        application_transport_mode = pick(
            "application_transport_mode",
            "applicationTransportMode",
            "transport_mode",
            "transportMode",
        )
        application_lodging_daily_cap = pick(
            "application_lodging_daily_cap",
            "applicationLodgingDailyCap",
            "lodging_daily_cap",
            "lodgingDailyCap",
        )
        application_subsidy_daily_cap = pick(
            "application_subsidy_daily_cap",
            "applicationSubsidyDailyCap",
            "subsidy_daily_cap",
            "subsidyDailyCap",
        )
        application_transport_policy = pick(
            "application_transport_policy",
            "applicationTransportPolicy",
            "transport_policy",
            "transportPolicy",
        )
        application_policy_estimate = pick(
            "application_policy_estimate",
            "applicationPolicyEstimate",
            "policy_estimate",
            "policyEstimate",
        )
        application_rule_name = pick(
            "application_rule_name",
            "applicationRuleName",
            "rule_name",
            "ruleName",
        )
        application_rule_version = pick(
            "application_rule_version",
            "applicationRuleVersion",
            "rule_version",
            "ruleVersion",
        )
        application_status = pick("application_status", "applicationStatus")
        application_status_label = pick(
            "application_status_label", "applicationStatusLabel"
        )

        return {
            "source": "application_link",
            "event_type": "expense_reimbursement_application_linked",
            "severity": "info",
            "label": "关联申请单",
            "message": f"报销草稿已关联申请单 {application_claim_no}。",
            "application_claim_id": application_claim_id,
            "application_claim_no": application_claim_no,
            "application_amount_label": application_amount_label,
            "application_status": application_status,
            "application_status_label": application_status_label,
            "application_detail": {
                "application_reason": application_reason,
                "application_location": application_location,
                "application_amount": application_amount,
                "application_amount_label": application_amount_label,
                "application_time": application_time or application_date,
                "application_business_time": application_time,
                "application_date": application_date,
                "application_days": application_days,
                "application_transport_mode": application_transport_mode,
                "application_lodging_daily_cap": application_lodging_daily_cap,
                "application_subsidy_daily_cap": application_subsidy_daily_cap,
                "application_transport_policy": application_transport_policy,
                "application_policy_estimate": application_policy_estimate,
                "application_rule_name": application_rule_name,
                "application_rule_version": application_rule_version,
            },
            "review_form_values": review_values,
            "expense_scene_selection": scene_selection,
            "created_at": datetime.now(UTC).isoformat(),
        }

    @staticmethod
    def _normalize_context_object(value: Any) -> dict[str, Any]:
        return dict(value) if isinstance(value, dict) else {}
