from __future__ import annotations

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

from sqlalchemy import case, func, or_, select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.ai_learning import AIDecision, AIDecisionFeedback, WorkflowOutcome
from app.models.ai_memory import MemoryEntry, MemoryEvidenceLink
from app.models.expense_case import BusinessEvent
from app.models.financial_record import ExpenseClaim
from app.schemas.expense_application_memory import (
    ExpenseApplicationLearningReceipt,
    ExpenseApplicationMemoryApplication,
    ExpenseApplicationMemoryListRead,
    ExpenseApplicationMemoryRead,
    ExpenseApplicationMemoryRevokedRead,
)
from app.services.expense_application_memory_evidence import (
    ExpenseApplicationMemoryEvidenceValidator,
)
from app.services.expense_application_memory_resolution import (
    ExpenseApplicationMemoryResolution,
    ExpenseApplicationMemoryResolver,
    memory_scope_label,
    memory_source,
)
from app.services.expense_application_snapshot import hmac_fingerprint
from app.services.expense_cases import ExpenseCaseService

logger = logging.getLogger(__name__)

MEMORY_SCOPE_TYPE = "user"
MEMORY_SCENE = "travel_application"
MEMORY_FIELD_KEY = "transport_mode"
MEMORY_POLICY_VERSION = "expense_application_transport_memory.v1"
MEMORY_ACTIVATION_THRESHOLD = 3
MEMORY_APPROVED_THRESHOLD = 2
MEMORY_EVIDENCE_SPAN = timedelta(days=7)
MEMORY_CANDIDATE_TTL = timedelta(days=90)
MEMORY_ACTIVE_TTL = timedelta(days=180)
MEMORY_OUTCOME_EVENT_TYPES = {"application_approved", "application_returned"}
SUPPORTED_TRANSPORT_VALUES = {"飞机", "火车", "轮船"}


class ExpenseApplicationMemoryService:
    """从可信字段纠正证据生成、激活并应用个人费用申请记忆。"""

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

    def record_transport_edit_evidence(
        self,
        *,
        current_user: CurrentUserContext,
        decision: AIDecision,
        feedback: AIDecisionFeedback,
        outcome: WorkflowOutcome,
        claim: ExpenseClaim,
        transport_mode: str,
    ) -> ExpenseApplicationLearningReceipt | None:
        if not self._is_eligible_feedback(feedback, outcome):
            return None
        if not self._changed_transport_mode(feedback):
            return None

        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        if any(
            item != tenant_id
            for item in (decision.tenant_id, feedback.tenant_id, outcome.tenant_id)
        ):
            raise PermissionError("个人记忆证据不能关联其他租户的学习记录。")
        ExpenseApplicationMemoryEvidenceValidator(self.db).validate(
            current_user=current_user,
            decision=decision,
            feedback=feedback,
            outcome=outcome,
            claim=claim,
        )

        scope_id = self._scope_id(current_user)
        now = datetime.now(UTC)
        replay = self.db.execute(
            select(MemoryEvidenceLink, MemoryEntry)
            .join(MemoryEntry, MemoryEntry.id == MemoryEvidenceLink.memory_entry_id)
            .where(
                MemoryEvidenceLink.tenant_id == tenant_id,
                MemoryEntry.tenant_id == tenant_id,
                MemoryEntry.scope_type == MEMORY_SCOPE_TYPE,
                MemoryEntry.scope_id == scope_id,
                or_(
                    MemoryEvidenceLink.decision_id == decision.id,
                    MemoryEvidenceLink.feedback_id == feedback.id,
                    MemoryEvidenceLink.outcome_id == outcome.id,
                ),
            )
        ).first()
        if replay is not None:
            evidence, replay_entry = replay
            return self._build_receipt(
                replay_entry,
                evidence,
                activated=False,
            )

        normalized_value = self._normalize_transport_value(transport_mode)
        if not normalized_value:
            # 未知方式只作为旧偏好失效的负向信号，绝不保存新值。
            self._suppress_active_entries(
                tenant_id=tenant_id,
                scope_id=scope_id,
                now=now,
            )
            self.db.flush()
            return None

        value_fingerprint = self._value_fingerprint(normalized_value)
        entry = self._find_open_entry(
            tenant_id=tenant_id,
            scope_id=scope_id,
            value_fingerprint=value_fingerprint,
        )
        while entry is not None and self._is_expired(entry, now):
            entry.status = "expired"
            entry.expired_at = now
            self.db.flush()
            entry = self._find_open_entry(
                tenant_id=tenant_id,
                scope_id=scope_id,
                value_fingerprint=value_fingerprint,
            )
        if entry is None:
            entry = self._create_candidate_entry(
                tenant_id=tenant_id,
                scope_id=scope_id,
                value=normalized_value,
                value_fingerprint=value_fingerprint,
                now=now,
            )
            self.db.add(entry)
            self.db.flush()

        existing_link = self.db.scalar(
            select(MemoryEvidenceLink).where(
                MemoryEvidenceLink.tenant_id == tenant_id,
                MemoryEvidenceLink.memory_entry_id == entry.id,
                MemoryEvidenceLink.expense_case_id == decision.expense_case_id,
            )
        )
        if existing_link is not None:
            return self._build_receipt(
                entry,
                existing_link,
                activated=False,
            )

        existing_link = MemoryEvidenceLink(
            id=str(uuid.uuid4()),
            tenant_id=tenant_id,
            memory_entry_id=entry.id,
            decision_id=decision.id,
            feedback_id=feedback.id,
            expense_case_id=decision.expense_case_id,
            outcome_id=outcome.id,
        )
        self.db.add(existing_link)
        self.db.flush()

        self._suppress_opposite_active_entries(
            tenant_id=tenant_id,
            scope_id=scope_id,
            value_fingerprint=value_fingerprint,
            now=now,
        )
        previous_status = str(entry.status or "")
        self._refresh_entry_metrics(entry, now=now, refresh_expiry=True)
        self.db.flush()
        return self._build_receipt(
            entry,
            existing_link,
            activated=previous_status != "active" and entry.status == "active",
        )

    def apply_active_transport_memory(
        self,
        facts: dict[str, Any],
        current_user: CurrentUserContext,
    ) -> list[ExpenseApplicationMemoryApplication]:
        # 当前输入只要显式给出了出行方式就必须优先，即使值暂不在可学习白名单内。
        if str(facts.get(MEMORY_FIELD_KEY) or "").strip():
            return []

        try:
            with self.db.begin_nested():
                resolution = self._resolve_active_entry(current_user)
                if resolution is None:
                    return []
                application = resolution.to_application(current_user)
                if resolution.winner is None:
                    return [application]
                facts[MEMORY_FIELD_KEY] = resolution.value
                return [application]
        except Exception:
            logger.warning("出行方式分层记忆读取失败，本轮预览不应用记忆。", exc_info=True)
            return []

    def learning_receipts_for_preview_decision(
        self,
        preview_decision_id: str,
        current_user: CurrentUserContext,
    ) -> list[ExpenseApplicationLearningReceipt]:
        normalized_id = str(preview_decision_id or "").strip()
        if not normalized_id:
            return []
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        scope_id = self._scope_id(current_user)
        stmt = (
            select(MemoryEvidenceLink, MemoryEntry)
            .join(AIDecision, AIDecision.id == MemoryEvidenceLink.decision_id)
            .join(MemoryEntry, MemoryEntry.id == MemoryEvidenceLink.memory_entry_id)
            .where(
                MemoryEvidenceLink.tenant_id == tenant_id,
                MemoryEntry.tenant_id == tenant_id,
                MemoryEntry.scope_type == MEMORY_SCOPE_TYPE,
                MemoryEntry.scope_id == scope_id,
                AIDecision.preview_decision_id == normalized_id,
            )
            .order_by(MemoryEvidenceLink.created_at.asc())
        )
        return [
            self._build_receipt(entry, evidence, activated=entry.status == "active")
            for evidence, entry in self.db.execute(stmt).all()
        ]

    def list_current_user_memories(
        self,
        current_user: CurrentUserContext,
    ) -> ExpenseApplicationMemoryListRead:
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        scope_id = self._scope_id(current_user)
        now = datetime.now(UTC)
        entries = list(
            self.db.scalars(
                select(MemoryEntry)
                .where(
                    MemoryEntry.tenant_id == tenant_id,
                    MemoryEntry.scope_type == MEMORY_SCOPE_TYPE,
                    MemoryEntry.scope_id == scope_id,
                    MemoryEntry.scene == MEMORY_SCENE,
                )
                .order_by(MemoryEntry.generation.desc(), MemoryEntry.created_at.desc())
            ).all()
        )
        for entry in entries:
            if entry.status in {"candidate", "active"}:
                self._refresh_entry_metrics(entry, now=now, allow_activation=False)
        self.db.commit()
        return ExpenseApplicationMemoryListRead(
            items=[self._serialize_entry(entry, current_user) for entry in entries]
        )

    def revoke_current_user_memory(
        self,
        memory_id: str,
        current_user: CurrentUserContext,
    ) -> ExpenseApplicationMemoryRevokedRead | None:
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        scope_id = self._scope_id(current_user)
        entry = self.db.scalar(
            select(MemoryEntry)
            .where(
                MemoryEntry.id == str(memory_id or "").strip(),
                MemoryEntry.tenant_id == tenant_id,
                MemoryEntry.scope_type == MEMORY_SCOPE_TYPE,
                MemoryEntry.scope_id == scope_id,
                MemoryEntry.scene == MEMORY_SCENE,
            )
            .with_for_update()
        )
        if entry is None:
            return None
        now = datetime.now(UTC)
        entry.status = "revoked"
        entry.value_json = {}
        entry.value_fingerprint = ""
        entry.revoked_at = now
        entry.revoked_reason = "user_requested"
        self.db.commit()
        return ExpenseApplicationMemoryRevokedRead(
            memory_id=entry.id,
            revoked_at=now,
        )

    def _resolve_active_entry(
        self,
        current_user: CurrentUserContext,
    ) -> ExpenseApplicationMemoryResolution | None:
        return ExpenseApplicationMemoryResolver(self.db).resolve(
            current_user,
            user_scope_id=self._scope_id(current_user),
            refresh_user_entry=self._refresh_entry_metrics,
        )

    def _refresh_entry_metrics(
        self,
        entry: MemoryEntry,
        *,
        now: datetime,
        refresh_expiry: bool = False,
        allow_activation: bool = True,
    ) -> None:
        if entry.status not in {"candidate", "active"}:
            return
        if self._is_expired(entry, now):
            entry.status = "expired"
            entry.expired_at = now
            return

        links = ExpenseApplicationMemoryEvidenceValidator(
            self.db
        ).list_currently_valid_links(
            tenant_id=entry.tenant_id,
            memory_entry_id=entry.id,
        )
        case_ids = {link.expense_case_id for link in links}
        approved_case_ids = self._approved_case_ids(
            tenant_id=entry.tenant_id,
            case_ids=case_ids,
        )
        entry.evidence_count = len({link.expense_case_id for link in links})
        entry.approved_evidence_count = len(approved_case_ids)
        entry.confidence = Decimal(
            str(min(1.0, entry.evidence_count / MEMORY_ACTIVATION_THRESHOLD))
        ).quantize(Decimal("0.0001"))
        if links:
            entry.last_evidence_at = max(link.created_at for link in links)
        if refresh_expiry:
            if entry.status == "active":
                entry.active_expires_at = now + MEMORY_ACTIVE_TTL
            else:
                entry.candidate_expires_at = now + MEMORY_CANDIDATE_TTL

        if entry.status == "active" and not self._qualifies_for_activation(entry, links):
            # 审批退回、证据 invalidated 或 outcome reversed 后不得继续预填。
            entry.status = "suppressed"
            entry.suppressed_at = now
            return

        if (
            not allow_activation
            or entry.status != "candidate"
            or not self._qualifies_for_activation(entry, links)
        ):
            return
        self._suppress_other_active_entries(entry, now=now)
        entry.status = "active"
        entry.activated_at = now
        entry.active_expires_at = now + MEMORY_ACTIVE_TTL

    @staticmethod
    def _qualifies_for_activation(
        entry: MemoryEntry,
        links: list[MemoryEvidenceLink],
    ) -> bool:
        if (
            entry.evidence_count < MEMORY_ACTIVATION_THRESHOLD
            or entry.approved_evidence_count < MEMORY_APPROVED_THRESHOLD
            or len(links) < 2
        ):
            return False
        first_at = min(link.created_at for link in links)
        last_at = max(link.created_at for link in links)
        return last_at - first_at >= MEMORY_EVIDENCE_SPAN

    def _suppress_other_active_entries(
        self,
        activated_entry: MemoryEntry,
        *,
        now: datetime,
    ) -> None:
        entries = list(
            self.db.scalars(
                select(MemoryEntry).where(
                    MemoryEntry.tenant_id == activated_entry.tenant_id,
                    MemoryEntry.scope_type == activated_entry.scope_type,
                    MemoryEntry.scope_id == activated_entry.scope_id,
                    MemoryEntry.scene == activated_entry.scene,
                    MemoryEntry.field_key == activated_entry.field_key,
                    MemoryEntry.status == "active",
                    MemoryEntry.id != activated_entry.id,
                )
            ).all()
        )
        for entry in entries:
            entry.status = "suppressed"
            entry.suppressed_at = now

    def _suppress_opposite_active_entries(
        self,
        *,
        tenant_id: str,
        scope_id: str,
        value_fingerprint: str,
        now: datetime,
    ) -> None:
        entries = list(
            self.db.scalars(
                select(MemoryEntry)
                .where(
                    MemoryEntry.tenant_id == tenant_id,
                    MemoryEntry.scope_type == MEMORY_SCOPE_TYPE,
                    MemoryEntry.scope_id == scope_id,
                    MemoryEntry.scene == MEMORY_SCENE,
                    MemoryEntry.field_key == MEMORY_FIELD_KEY,
                    MemoryEntry.status == "active",
                    MemoryEntry.value_fingerprint != value_fingerprint,
                )
                .with_for_update()
            ).all()
        )
        for entry in entries:
            entry.status = "suppressed"
            entry.suppressed_at = now

    def _suppress_active_entries(
        self,
        *,
        tenant_id: str,
        scope_id: str,
        now: datetime,
    ) -> None:
        entries = list(
            self.db.scalars(
                select(MemoryEntry)
                .where(
                    MemoryEntry.tenant_id == tenant_id,
                    MemoryEntry.scope_type == MEMORY_SCOPE_TYPE,
                    MemoryEntry.scope_id == scope_id,
                    MemoryEntry.scene == MEMORY_SCENE,
                    MemoryEntry.field_key == MEMORY_FIELD_KEY,
                    MemoryEntry.status == "active",
                )
                .with_for_update()
            ).all()
        )
        for entry in entries:
            entry.status = "suppressed"
            entry.suppressed_at = now

    def _find_open_entry(
        self,
        *,
        tenant_id: str,
        scope_id: str,
        value_fingerprint: str,
    ) -> MemoryEntry | None:
        return self.db.scalar(
            select(MemoryEntry)
            .where(
                MemoryEntry.tenant_id == tenant_id,
                MemoryEntry.scope_type == MEMORY_SCOPE_TYPE,
                MemoryEntry.scope_id == scope_id,
                MemoryEntry.scene == MEMORY_SCENE,
                MemoryEntry.field_key == MEMORY_FIELD_KEY,
                MemoryEntry.value_fingerprint == value_fingerprint,
                MemoryEntry.status.in_(["candidate", "active"]),
            )
            .order_by(MemoryEntry.generation.desc())
            .with_for_update()
        )

    def _create_candidate_entry(
        self,
        *,
        tenant_id: str,
        scope_id: str,
        value: str,
        value_fingerprint: str,
        now: datetime,
    ) -> MemoryEntry:
        generation = int(
            self.db.scalar(
                select(func.coalesce(func.max(MemoryEntry.generation), 0)).where(
                    MemoryEntry.tenant_id == tenant_id,
                    MemoryEntry.scope_type == MEMORY_SCOPE_TYPE,
                    MemoryEntry.scope_id == scope_id,
                    MemoryEntry.scene == MEMORY_SCENE,
                    MemoryEntry.field_key == MEMORY_FIELD_KEY,
                )
            )
            or 0
        ) + 1
        return MemoryEntry(
            id=str(uuid.uuid4()),
            tenant_id=tenant_id,
            scope_type=MEMORY_SCOPE_TYPE,
            scope_id=scope_id,
            scene=MEMORY_SCENE,
            field_key=MEMORY_FIELD_KEY,
            origin_type="learned",
            generation=generation,
            value_json={"value": value},
            value_fingerprint=value_fingerprint,
            status="candidate",
            evidence_count=0,
            approved_evidence_count=0,
            confidence=Decimal("0"),
            candidate_expires_at=now + MEMORY_CANDIDATE_TTL,
            last_evidence_at=now,
            policy_version=MEMORY_POLICY_VERSION,
        )

    def _approved_case_ids(
        self,
        *,
        tenant_id: str,
        case_ids: set[str],
    ) -> set[str]:
        if not case_ids:
            return set()
        events = list(
            self.db.scalars(
                select(BusinessEvent)
                .where(
                    BusinessEvent.tenant_id == tenant_id,
                    BusinessEvent.expense_case_id.in_(case_ids),
                    BusinessEvent.event_type.in_(MEMORY_OUTCOME_EVENT_TYPES),
                )
                .order_by(
                    BusinessEvent.occurred_at.asc(),
                    case(
                        (BusinessEvent.event_type == "application_returned", 1),
                        else_=0,
                    ).asc(),
                    BusinessEvent.id.asc(),
                )
            ).all()
        )
        latest_by_case: dict[str, BusinessEvent] = {}
        for event in events:
            latest_by_case[event.expense_case_id] = event
        return {
            case_id
            for case_id, event in latest_by_case.items()
            if event.event_type == "application_approved"
        }

    @staticmethod
    def _is_eligible_feedback(
        feedback: AIDecisionFeedback,
        outcome: WorkflowOutcome,
    ) -> bool:
        return (
            feedback.verification_status in {"server_verified", "human_verified"}
            and feedback.feedback_type == "edited"
            and feedback.action_type == "submit"
            and outcome.outcome_type == "application_submitted"
            and outcome.outcome_status in {"recorded", "verified"}
        )

    @staticmethod
    def _changed_transport_mode(feedback: AIDecisionFeedback) -> bool:
        return any(
            isinstance(item, dict) and item.get("field_key") == MEMORY_FIELD_KEY
            for item in list(feedback.changed_fields_json or [])
        )

    @staticmethod
    def _scope_id(current_user: CurrentUserContext) -> str:
        value = str(current_user.employee_id or current_user.username or "").strip()[:120]
        if not value:
            raise ValueError("当前登录用户缺少可用于个人记忆的主体标识。")
        return value

    @staticmethod
    def _normalize_transport_value(value: object) -> str:
        normalized = str(value or "").strip()
        return normalized if normalized in SUPPORTED_TRANSPORT_VALUES else ""

    @staticmethod
    def _value_fingerprint(value: str) -> str:
        return hmac_fingerprint({"field_key": MEMORY_FIELD_KEY, "value": value})

    @staticmethod
    def _entry_value(entry: MemoryEntry) -> str:
        value_json = entry.value_json if isinstance(entry.value_json, dict) else {}
        return ExpenseApplicationMemoryService._normalize_transport_value(
            value_json.get("value")
        )

    @staticmethod
    def _is_expired(entry: MemoryEntry, now: datetime) -> bool:
        expires_at = (
            entry.active_expires_at
            if entry.status == "active"
            else entry.candidate_expires_at
        )
        if expires_at is None:
            return False
        normalized = expires_at if expires_at.tzinfo is not None else expires_at.replace(tzinfo=UTC)
        return normalized <= now

    @staticmethod
    def _build_receipt(
        entry: MemoryEntry,
        evidence: MemoryEvidenceLink,
        *,
        activated: bool,
    ) -> ExpenseApplicationLearningReceipt:
        external_status = "applied" if entry.status == "active" else entry.status
        if external_status == "applied":
            message = "已形成常用出行方式记忆，后续申请可自动预填。"
        else:
            remaining = max(0, MEMORY_ACTIVATION_THRESHOLD - int(entry.evidence_count or 0))
            message = (
                f"已记录本次出行方式纠正，再积累 {remaining} 个不同申请证据后可参与预填。"
                if remaining
                else "已记录本次出行方式纠正，待审批通过证据满足后可参与预填。"
            )
        return ExpenseApplicationLearningReceipt(
            memory_id=entry.id,
            evidence_id=evidence.id,
            value=ExpenseApplicationMemoryService._entry_value(entry),
            status=external_status,
            evidence_count=int(entry.evidence_count or 0),
            approved_evidence_count=int(entry.approved_evidence_count or 0),
            activated=activated,
            message=message,
        )

    @staticmethod
    def _serialize_entry(
        entry: MemoryEntry,
        current_user: CurrentUserContext,
    ) -> ExpenseApplicationMemoryRead:
        scope_type = str(entry.scope_type or "user")
        value = (
            ""
            if entry.status == "revoked"
            else ExpenseApplicationMemoryService._entry_value(entry)
        )
        return ExpenseApplicationMemoryRead(
            id=entry.id,
            scene=entry.scene,
            field_key=entry.field_key,
            value=value,
            status=entry.status,
            scope_type=scope_type,
            scope_id=str(entry.scope_id or ""),
            scope_label=memory_scope_label(entry, current_user),
            source=memory_source(scope_type),
            origin_type=str(getattr(entry, "origin_type", "learned") or "learned"),
            generation=int(entry.generation or 1),
            evidence_count=int(entry.evidence_count or 0),
            approved_evidence_count=int(entry.approved_evidence_count or 0),
            confidence=float(entry.confidence or 0),
            activation_threshold=MEMORY_ACTIVATION_THRESHOLD,
            policy_version=str(
                getattr(entry, "policy_version", MEMORY_POLICY_VERSION)
                or MEMORY_POLICY_VERSION
            ),
            valid_from=entry.activated_at or entry.created_at,
            expires_at=(
                entry.active_expires_at
                if entry.status == "active"
                else entry.candidate_expires_at
            ),
            last_evidence_at=entry.last_evidence_at,
            activated_at=entry.activated_at,
            suppressed_at=entry.suppressed_at,
            revoked_at=entry.revoked_at,
            revoked_reason=str(entry.revoked_reason or ""),
            managed_by=str(getattr(entry, "managed_by", "") or ""),
            managed_at=getattr(entry, "managed_at", None),
            management_reason=str(getattr(entry, "management_reason", "") or ""),
            superseded_by_id=str(entry.superseded_by_id or ""),
            can_revoke=scope_type == "user",
            created_at=entry.created_at,
            updated_at=entry.updated_at,
        )
