from __future__ import annotations

import logging
import uuid
from dataclasses import dataclass
from decimal import Decimal
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.ai_application_preview import AIApplicationPreviewDecision
from app.models.ai_learning import AIDecision, AIDecisionFeedback, WorkflowOutcome
from app.models.expense_case import BusinessEvent
from app.models.financial_record import ExpenseClaim
from app.schemas.user_agent import UserAgentRequest
from app.services.expense_application_memory import ExpenseApplicationMemoryService
from app.services.expense_application_preview_decisions import (
    ExpenseApplicationPreviewDecisionService,
)
from app.services.expense_application_snapshot import (
    PREVIEW_FIELD_ALIASES,
    content_fingerprint,
    field_value_fingerprint,
    hmac_fingerprint,
    safe_fact_snapshot,
    snapshot_reference,
)
from app.services.expense_cases import ExpenseCaseService
from app.services.expense_workflow_learning import ExpenseWorkflowLearningService

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class ExpenseApplicationLearningRecords:
    decision: AIDecision
    feedback: AIDecisionFeedback
    outcome: WorkflowOutcome


class ExpenseApplicationLearningService:
    """把 AI 申请预填、显式用户编辑和工作流结果原子写入学习账本。"""

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

    def record_action(
        self,
        payload: UserAgentRequest,
        facts: dict[str, str],
        claim: ExpenseClaim,
        current_user: CurrentUserContext,
        *,
        action_type: str,
        business_event: BusinessEvent,
        preview_decision: AIApplicationPreviewDecision | None = None,
        action_request_id: str = "",
    ) -> ExpenseApplicationLearningRecords | None:
        normalized_action = self._normalize_action_type(action_type)
        preview = self._application_preview(payload)
        if not self._is_learning_eligible_preview(preview):
            if preview_decision is not None:
                raise ValueError("服务端预览决策缺少可核验的申请字段。")
            return None
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        expense_case = ExpenseCaseService(self.db).ensure_case_for_claim(
            claim,
            tenant_id=tenant_id,
        )
        if (
            business_event.tenant_id != tenant_id
            or business_event.expense_case_id != expense_case.id
        ):
            raise PermissionError("AI 学习记录不能关联其他租户或费用事件的业务事件。")

        final_values = self._safe_fact_snapshot(facts)
        if preview_decision is not None:
            final_reference = self._snapshot_reference(
                final_values,
                key_version=preview_decision.fingerprint_key_version,
                allow_key_creation=False,
            )
            changed_field_references = self._verified_changed_field_references(
                preview_decision,
                final_values,
            )
            explicit_rejection = self._all_server_suggestions_cleared(
                preview_decision,
                final_values,
                changed_field_references,
            )
            suggestion_reference = {
                "field_keys": list(preview_decision.field_keys_json or []),
                "value_fingerprint": preview_decision.snapshot_fingerprint,
                "fingerprint_key_version": preview_decision.fingerprint_key_version,
            }
            evidence_source = "server_preview_decision"
            verification_status = "server_verified"
        else:
            final_reference = self._snapshot_reference(final_values)
            changed_fields = self._safe_changed_fields(payload, final_values)
            explicit_rejection = bool(changed_fields) and all(
                item["suggested_value"] and not item["final_value"]
                for item in changed_fields
            )
            suggested_values = dict(final_values)
            for item in changed_fields:
                suggested_values[item["field_key"]] = item["suggested_value"]
            changed_field_references = self._changed_field_references(changed_fields)
            suggestion_reference = self._snapshot_reference(suggested_values)
            evidence_source = (
                "client_explicit_field_edit"
                if changed_field_references
                else "client_action_confirmation"
            )
            verification_status = "client_observed"

        feedback_type = (
            "rejected"
            if explicit_rejection
            else ("edited" if changed_field_references else "accepted")
        )
        correlation_id = ExpenseCaseService.normalize_correlation_id(payload.run_id)
        fingerprint_payload = {
            "tenant_id": tenant_id,
            "expense_claim_id": claim.id,
            "action_type": normalized_action,
            "preview_decision_id": preview_decision.id if preview_decision is not None else None,
            "suggestion_reference": suggestion_reference,
            "final_reference": final_reference,
            "changed_fields": changed_field_references,
        }
        content_fingerprint = self._fingerprint(fingerprint_payload)
        idempotency_key = self._idempotency_key(
            normalized_action,
            claim.id,
            content_fingerprint,
        )
        existing = self._find_existing(tenant_id, idempotency_key)
        if existing is not None:
            ExpenseWorkflowLearningService(self.db).record_prior_events_for_decision(
                existing.decision
            )
            self._record_transport_memory_evidence(
                current_user=current_user,
                records=existing,
                claim=claim,
                facts=final_values,
            )
            return existing

        decision_id = self._stable_id("decision", tenant_id, idempotency_key)
        decision = AIDecision(
            id=decision_id,
            tenant_id=tenant_id,
            expense_case_id=expense_case.id,
            business_event_id=business_event.id,
            expense_claim_id=claim.id,
            preview_decision_id=(preview_decision.id if preview_decision is not None else None),
            agent_run_id=str(payload.run_id or "").strip() or None,
            correlation_id=correlation_id,
            subject_type="expense_claim",
            subject_id=claim.id,
            decision_type="expense_application_prefill",
            decision_source=(
                preview_decision.decision_source
                if preview_decision is not None
                else self._decision_source(preview, facts)
            ),
            status="executed",
            automation_mode="prefill",
            confidence=self._confidence(payload),
            suggestion_json=suggestion_reference,
            evidence_json={
                "evidence_source": (
                    "server_preview_decision"
                    if preview_decision is not None
                    else "client_preview_edit_trace"
                ),
                "trust_level": (
                    "server_snapshot_verified"
                    if preview_decision is not None
                    else "behavioral_analytics_only"
                ),
                "model_refined": bool(preview.get("modelRefined")),
                "model_review_status": self._safe_text(preview.get("modelReviewStatus"), 40),
            },
            version_json={
                "schema_version": 1,
                "fingerprint_key_version": suggestion_reference.get("fingerprint_key_version"),
                "parse_strategy": self._safe_text(preview.get("parseStrategy"), 60),
                "rule_name": self._safe_text(facts.get("rule_name"), 120),
                "rule_version": self._safe_text(facts.get("rule_version"), 80),
            },
            schema_version=1,
            training_eligible=False,
            idempotency_key=idempotency_key,
            content_fingerprint=content_fingerprint,
        )
        feedback = AIDecisionFeedback(
            id=self._stable_id("feedback", tenant_id, idempotency_key),
            tenant_id=tenant_id,
            decision_id=decision_id,
            expense_claim_id=claim.id,
            correlation_id=correlation_id,
            feedback_type=feedback_type,
            action_type=normalized_action,
            actor_id=str(current_user.username or "anonymous").strip() or "anonymous",
            actor_type="user",
            evidence_source=evidence_source,
            verification_status=verification_status,
            training_eligible=False,
            final_value_json=final_reference,
            changed_fields_json=changed_field_references,
            idempotency_key=f"feedback:{idempotency_key}"[:120],
            content_fingerprint=self._fingerprint(
                {
                    "decision_id": decision_id,
                    "feedback_type": feedback_type,
                    "final_reference": final_reference,
                    "changed_fields": changed_field_references,
                }
            ),
        )
        outcome_type = "application_submitted" if normalized_action == "submit" else "draft_saved"
        outcome = WorkflowOutcome(
            id=self._stable_id("outcome", tenant_id, idempotency_key),
            tenant_id=tenant_id,
            expense_case_id=expense_case.id,
            decision_id=decision_id,
            business_event_id=business_event.id,
            expense_claim_id=claim.id,
            correlation_id=correlation_id,
            outcome_type=outcome_type,
            outcome_status="recorded",
            actor_id=str(current_user.username or "anonymous").strip() or "anonymous",
            actor_type="user",
            result_json={
                "claim_status": "submitted" if normalized_action == "submit" else "draft",
                "approval_stage": ("直属领导审批" if normalized_action == "submit" else "待提交"),
                "feedback_type": feedback_type,
            },
            idempotency_key=f"outcome:{idempotency_key}"[:120],
            content_fingerprint=self._fingerprint(
                {
                    "decision_id": decision_id,
                    "outcome_type": outcome_type,
                    "feedback_type": feedback_type,
                }
            ),
        )
        self.db.add_all([decision, feedback, outcome])
        self.db.flush()
        ExpenseWorkflowLearningService(self.db).record_prior_events_for_decision(decision)
        records = ExpenseApplicationLearningRecords(decision, feedback, outcome)
        self._record_transport_memory_evidence(
            current_user=current_user,
            records=records,
            claim=claim,
            facts=final_values,
        )
        if preview_decision is not None:
            ExpenseApplicationPreviewDecisionService(self.db).consume(
                preview_decision,
                request_id=action_request_id,
                action_type=normalized_action,
                final_values=final_values,
                claim=claim,
                business_event=business_event,
            )
        return records

    def _record_transport_memory_evidence(
        self,
        *,
        current_user: CurrentUserContext,
        records: ExpenseApplicationLearningRecords,
        claim: ExpenseClaim,
        facts: dict[str, str],
    ) -> None:
        """记忆属于可降级派生能力，失败不能回滚申请和学习账本。"""

        try:
            with self.db.begin_nested():
                ExpenseApplicationMemoryService(self.db).record_transport_edit_evidence(
                    current_user=current_user,
                    decision=records.decision,
                    feedback=records.feedback,
                    outcome=records.outcome,
                    claim=claim,
                    transport_mode=str(facts.get("transport_mode") or ""),
                )
        except Exception:
            logger.warning("个人出行方式记忆证据写入失败，本次申请继续提交。", exc_info=True)

    def _find_existing(
        self,
        tenant_id: str,
        idempotency_key: str,
    ) -> ExpenseApplicationLearningRecords | None:
        decision = self.db.scalar(
            select(AIDecision).where(
                AIDecision.tenant_id == tenant_id,
                AIDecision.idempotency_key == idempotency_key,
            )
        )
        if decision is None:
            return None
        feedback = self.db.get(
            AIDecisionFeedback,
            self._stable_id("feedback", tenant_id, idempotency_key),
        )
        outcome = self.db.get(
            WorkflowOutcome,
            self._stable_id("outcome", tenant_id, idempotency_key),
        )
        if feedback is None or outcome is None:
            raise RuntimeError("AI 学习账本幂等状态不完整，请重试。")
        if (
            feedback.tenant_id != tenant_id
            or feedback.decision_id != decision.id
            or outcome.tenant_id != tenant_id
            or outcome.decision_id != decision.id
        ):
            raise RuntimeError("AI 学习账本幂等记录关联不一致，请联系管理员。")
        return ExpenseApplicationLearningRecords(decision, feedback, outcome)

    @classmethod
    def _safe_fact_snapshot(cls, facts: dict[str, str]) -> dict[str, str]:
        return safe_fact_snapshot(facts)

    @classmethod
    def _safe_changed_fields(
        cls,
        payload: UserAgentRequest,
        final_values: dict[str, str],
    ) -> list[dict[str, str]]:
        raw_feedback = cls._application_preview(payload).get("aiDecisionFeedback")
        if not isinstance(raw_feedback, dict):
            return []
        raw_changes = raw_feedback.get("changedFields")
        if not isinstance(raw_changes, list):
            return []

        changes: dict[str, dict[str, str]] = {}
        for raw_item in raw_changes[:30]:
            if not isinstance(raw_item, dict):
                continue
            alias = cls._safe_text(raw_item.get("fieldKey"), 60)
            field_key = PREVIEW_FIELD_ALIASES.get(alias)
            if not field_key or field_key not in final_values:
                continue
            suggested_value = cls._safe_text(raw_item.get("suggestedValue"), 500)
            final_value = cls._safe_text(final_values.get(field_key), 500)
            if suggested_value == final_value:
                continue
            changes[field_key] = {
                "field_key": field_key,
                "suggested_value": suggested_value,
                "final_value": final_value,
            }
        return [changes[key] for key in sorted(changes)]

    @staticmethod
    def _application_preview(payload: UserAgentRequest) -> dict[str, Any]:
        context = payload.context_json if isinstance(payload.context_json, dict) else {}
        preview = context.get("application_preview")
        return preview if isinstance(preview, dict) else {}

    @staticmethod
    def _is_learning_eligible_preview(preview: dict[str, Any]) -> bool:
        fields = preview.get("fields")
        if not isinstance(fields, dict) or not fields:
            return False
        if bool(preview.get("applicationEditMode") or preview.get("application_edit_mode")):
            return False
        return str(preview.get("modelReviewStatus") or "").strip().lower() != "template"

    @classmethod
    def _snapshot_reference(
        cls,
        values: dict[str, str],
        *,
        key_version: str | None = None,
        allow_key_creation: bool = True,
    ) -> dict[str, Any]:
        if key_version is None:
            return snapshot_reference(values)
        return snapshot_reference(
            values,
            key_version=key_version,
            allow_key_creation=allow_key_creation,
        )

    @staticmethod
    def _verified_changed_field_references(
        decision: AIApplicationPreviewDecision,
        final_values: dict[str, str],
    ) -> list[dict[str, str]]:
        fingerprints = decision.field_fingerprints_json or {}
        changes: list[dict[str, str]] = []
        suggested_field_keys = set(decision.field_keys_json or [])
        final_field_keys = set(final_values)
        for field_key in sorted(suggested_field_keys | final_field_keys):
            suggested_fingerprint = str(fingerprints.get(field_key) or "").strip()
            if not suggested_fingerprint:
                suggested_fingerprint = field_value_fingerprint(
                    "",
                    present=False,
                    key_version=decision.fingerprint_key_version,
                    allow_key_creation=False,
                )
            final_fingerprint = field_value_fingerprint(
                final_values.get(field_key, ""),
                present=field_key in final_values,
                key_version=decision.fingerprint_key_version,
                allow_key_creation=False,
            )
            if suggested_fingerprint == final_fingerprint:
                continue
            changes.append(
                {
                    "field_key": field_key,
                    "suggested_value_fingerprint": suggested_fingerprint,
                    "final_value_fingerprint": final_fingerprint,
                }
            )
        return changes

    @classmethod
    def _changed_field_references(
        cls,
        changes: list[dict[str, str]],
    ) -> list[dict[str, str]]:
        return [
            {
                "field_key": item["field_key"],
                "suggested_value_fingerprint": hmac_fingerprint(item["suggested_value"]),
                "final_value_fingerprint": hmac_fingerprint(item["final_value"]),
            }
            for item in changes
        ]

    @staticmethod
    def _all_server_suggestions_cleared(
        decision: AIApplicationPreviewDecision,
        final_values: dict[str, str],
        changes: list[dict[str, str]],
    ) -> bool:
        if not changes:
            return False
        suggested_keys = set(decision.field_keys_json or [])
        return all(
            item["field_key"] in suggested_keys
            and not str(final_values.get(item["field_key"]) or "").strip()
            for item in changes
        )

    @staticmethod
    def _decision_source(preview: dict[str, Any], facts: dict[str, str]) -> str:
        if preview.get("modelRefined") and facts.get("rule_version"):
            return "hybrid"
        if preview.get("modelRefined"):
            return "model"
        if facts.get("rule_version"):
            return "rule"
        return "heuristic"

    @staticmethod
    def _confidence(payload: UserAgentRequest) -> Decimal | None:
        raw_value = getattr(payload.ontology, "confidence", None)
        if raw_value is None:
            return None
        try:
            value = Decimal(str(raw_value))
        except Exception:
            return None
        return max(Decimal("0"), min(Decimal("1"), value)).quantize(Decimal("0.0001"))

    @staticmethod
    def _normalize_action_type(action_type: str) -> str:
        normalized = str(action_type or "").strip().lower()
        if normalized not in {"save_draft", "submit"}:
            raise ValueError("不支持的 AI 申请动作。")
        return normalized

    @staticmethod
    def _safe_text(value: object, limit: int) -> str:
        return str(value or "").strip()[:limit]

    @staticmethod
    def _fingerprint(payload: object) -> str:
        return content_fingerprint(payload)

    @staticmethod
    def _idempotency_key(action_type: str, claim_id: str, fingerprint: str) -> str:
        digest = fingerprint.removeprefix("sha256:")
        return f"ai-application:{action_type}:{claim_id}:{digest}"[:120]

    @staticmethod
    def _stable_id(record_type: str, tenant_id: str, idempotency_key: str) -> str:
        return str(uuid.uuid5(uuid.NAMESPACE_URL, f"{record_type}:{tenant_id}:{idempotency_key}"))
