from __future__ import annotations

import uuid
from datetime import UTC, datetime, timedelta
from decimal import Decimal

from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.ai_memory import MemoryEntry
from app.models.organization import OrganizationUnit
from app.schemas.expense_application_memory import (
    ExpenseApplicationMemoryListRead,
    ExpenseApplicationMemoryRead,
    ExpenseApplicationMemoryRevokedRead,
    ExpenseApplicationOrganizationMemoryCreate,
    ExpenseApplicationOrganizationMemoryRevoke,
    ExpenseApplicationOrganizationMemoryUpdate,
)
from app.services.expense_application_memory_resolution import memory_source
from app.services.expense_application_snapshot import hmac_fingerprint
from app.services.expense_cases import ExpenseCaseService
from app.services.organization_memory_locks import (
    organization_memory_operation_locks,
    organization_memory_request_lock_key,
    organization_memory_scope_lock_key,
)

MEMORY_SCENE = "travel_application"
MEMORY_FIELD_KEY = "transport_mode"
ORGANIZATION_SCOPE_TYPES = {"department", "enterprise"}
ORGANIZATION_MEMORY_POLICY_VERSION = "expense_application_transport_org_memory.v1"
SUPPORTED_TRANSPORT_VALUES = {"飞机", "火车", "轮船"}


class OrganizationMemoryNotFoundError(LookupError):
    pass


class OrganizationMemoryConflictError(ValueError):
    pass


class ExpenseApplicationOrganizationMemoryService:
    """由平台管理员显式维护企业/部门出行方式记忆。"""

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

    def list_organization_memories(
        self,
        current_user: CurrentUserContext,
    ) -> ExpenseApplicationMemoryListRead:
        self._require_admin(current_user)
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        now = datetime.now(UTC)
        entries = list(
            self.db.scalars(
                select(MemoryEntry)
                .where(
                    MemoryEntry.tenant_id == tenant_id,
                    MemoryEntry.scope_type.in_(ORGANIZATION_SCOPE_TYPES),
                    MemoryEntry.scene == MEMORY_SCENE,
                    MemoryEntry.field_key == MEMORY_FIELD_KEY,
                )
                .order_by(
                    MemoryEntry.scope_type.asc(),
                    MemoryEntry.scope_id.asc(),
                    MemoryEntry.generation.desc(),
                )
            ).all()
        )
        for entry in entries:
            if entry.status == "active" and self._is_expired(entry, now):
                entry.status = "expired"
                entry.expired_at = now
        department_names = self._department_names(entries)
        self.db.commit()
        return ExpenseApplicationMemoryListRead(
            items=[self._serialize_entry(entry, department_names) for entry in entries]
        )
    def create_organization_memory(
        self,
        payload: ExpenseApplicationOrganizationMemoryCreate,
        current_user: CurrentUserContext,
    ) -> ExpenseApplicationMemoryRead:
        self._require_admin(current_user)
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        scope_id = self._validate_scope(
            tenant_id=tenant_id,
            scope_type=payload.scope_type,
            scope_id=payload.scope_id,
        )
        request_id = self._normalize_request_id(payload.request_id)
        reason = self._normalize_reason(payload.reason)
        request_fingerprint = self._request_fingerprint(
            operation="create",
            target=f"{payload.scope_type}:{scope_id}",
            payload={
                "scope_type": payload.scope_type,
                "scope_id": scope_id,
                "value": payload.value,
                "expires_in_days": payload.expires_in_days,
                "reason": reason,
            },
        )
        now = datetime.now(UTC)
        entry_id = self._request_entry_id(
            tenant_id=tenant_id,
            operation="create",
            target=f"{payload.scope_type}:{scope_id}",
            request_id=request_id,
        )
        expires_at = now + timedelta(days=payload.expires_in_days)
        try:
            with organization_memory_operation_locks(
                self.db,
                self._scope_lock_key(
                    tenant_id=tenant_id,
                    scope_type=payload.scope_type,
                    scope_id=scope_id,
                ),
                self._request_lock_key(tenant_id, request_id),
            ):
                replay = self._management_request_replay(
                    tenant_id=tenant_id,
                    request_id=request_id,
                    request_fingerprint=request_fingerprint,
                )
                if replay is not None:
                    result = self._serialize_entry(
                        replay,
                        self._department_names([replay]),
                    )
                    self.db.commit()
                    return result

                active = self._get_scope_active_entry(
                    tenant_id=tenant_id,
                    scope_type=payload.scope_type,
                    scope_id=scope_id,
                )
                if active is not None:
                    if self._is_expired(active, now):
                        active.status = "expired"
                        active.expired_at = now
                        self.db.flush()
                    else:
                        raise OrganizationMemoryConflictError(
                            "该组织范围已有生效记忆，请使用更新操作换代。"
                        )
                entry = self._new_active_generation(
                    entry_id=entry_id,
                    tenant_id=tenant_id,
                    scope_type=payload.scope_type,
                    scope_id=scope_id,
                    value=payload.value,
                    expires_at=expires_at,
                    reason=reason,
                    request_id=request_id,
                    request_fingerprint=request_fingerprint,
                    expected_active_ids=set(),
                    current_user=current_user,
                    now=now,
                )
                self.db.commit()
        except OrganizationMemoryConflictError:
            self.db.rollback()
            raise
        except IntegrityError as error:
            self.db.rollback()
            replay = self._management_request_replay(
                tenant_id=tenant_id,
                request_id=request_id,
                request_fingerprint=request_fingerprint,
            )
            if replay is None:
                raise OrganizationMemoryConflictError(
                    "该组织记忆已被其他管理员创建，请刷新后重试。"
                ) from error
            entry = replay
        self.db.refresh(entry)
        return self._serialize_entry(entry, self._department_names([entry]))
    def update_organization_memory(
        self,
        memory_id: str,
        payload: ExpenseApplicationOrganizationMemoryUpdate,
        current_user: CurrentUserContext,
    ) -> ExpenseApplicationMemoryRead:
        self._require_admin(current_user)
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        previous_snapshot = self._get_entry(memory_id, tenant_id, for_update=False)
        request_id = self._normalize_request_id(payload.request_id)
        reason = self._normalize_reason(payload.reason)
        request_fingerprint = self._request_fingerprint(
            operation="update",
            target=previous_snapshot.id,
            payload={
                "value": payload.value,
                "expires_in_days": payload.expires_in_days,
                "expected_generation": payload.expected_generation,
                "reason": reason,
            },
        )
        entry_id = self._request_entry_id(
            tenant_id=tenant_id,
            operation="update",
            target=previous_snapshot.id,
            request_id=request_id,
        )
        now = datetime.now(UTC)
        try:
            with organization_memory_operation_locks(
                self.db,
                self._scope_lock_key_for_entry(tenant_id, previous_snapshot),
                self._request_lock_key(tenant_id, request_id),
            ):
                replay = self._management_request_replay(
                    tenant_id=tenant_id,
                    request_id=request_id,
                    request_fingerprint=request_fingerprint,
                )
                if replay is not None:
                    result = self._serialize_entry(
                        replay,
                        self._department_names([replay]),
                    )
                    self.db.commit()
                    return result
                previous = self._get_entry(memory_id, tenant_id)
                self._require_active(previous)
                self._validate_expected_generation(previous, payload.expected_generation)
                if self._is_expired(previous, now):
                    previous.status = "expired"
                    previous.expired_at = now
                    self.db.commit()
                    raise OrganizationMemoryConflictError(
                        "组织记忆已过期，不能继续更新。"
                    )

                expires_at = (
                    now + timedelta(days=payload.expires_in_days)
                    if payload.expires_in_days is not None
                    else self._aware(previous.active_expires_at)
                )
                entry = self._new_active_generation(
                    entry_id=entry_id,
                    tenant_id=tenant_id,
                    scope_type=str(previous.scope_type),
                    scope_id=str(previous.scope_id),
                    value=payload.value or self._entry_value(previous),
                    expires_at=expires_at,
                    reason=reason,
                    request_id=request_id,
                    request_fingerprint=request_fingerprint,
                    expected_active_ids={previous.id},
                    current_user=current_user,
                    now=now,
                )
                self.db.commit()
        except OrganizationMemoryConflictError:
            self.db.rollback()
            raise
        except IntegrityError as error:
            self.db.rollback()
            replay = self._management_request_replay(
                tenant_id=tenant_id,
                request_id=request_id,
                request_fingerprint=request_fingerprint,
            )
            if replay is None:
                raise OrganizationMemoryConflictError(
                    "组织记忆已被其他管理员更新，请刷新后重试。"
                ) from error
            entry = replay
        self.db.refresh(entry)
        return self._serialize_entry(entry, self._department_names([entry]))
    def revoke_organization_memory(
        self,
        memory_id: str,
        payload: ExpenseApplicationOrganizationMemoryRevoke,
        current_user: CurrentUserContext,
    ) -> ExpenseApplicationMemoryRevokedRead:
        self._require_admin(current_user)
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        entry_snapshot = self._get_entry(memory_id, tenant_id, for_update=False)
        request_id = self._normalize_request_id(payload.request_id)
        reason = self._normalize_reason(payload.reason)
        request_fingerprint = self._request_fingerprint(
            operation="revoke",
            target=entry_snapshot.id,
            payload={
                "expected_generation": payload.expected_generation,
                "reason": reason,
            },
        )
        now = datetime.now(UTC)
        try:
            with organization_memory_operation_locks(
                self.db,
                self._scope_lock_key_for_entry(tenant_id, entry_snapshot),
                self._request_lock_key(tenant_id, request_id),
            ):
                replay = self._revoke_request_replay(
                    tenant_id=tenant_id,
                    request_id=request_id,
                    request_fingerprint=request_fingerprint,
                )
                if replay is not None:
                    result = self._revoked_response(replay)
                    self.db.commit()
                    return result
                entry = self._get_entry(memory_id, tenant_id)
                self._require_active(entry)
                self._validate_expected_generation(entry, payload.expected_generation)
                if self._is_expired(entry, now):
                    entry.status = "expired"
                    entry.expired_at = now
                    self.db.commit()
                    raise OrganizationMemoryConflictError(
                        "组织记忆已过期，不能继续撤销。"
                    )
                entry.status = "revoked"
                entry.revoked_at = now
                entry.revoked_reason = reason
                entry.managed_by = self._actor_id(current_user)
                entry.managed_at = now
                entry.management_reason = reason
                entry.revoke_request_id = request_id
                entry.revoke_payload_fingerprint = request_fingerprint
                self.db.commit()
        except OrganizationMemoryConflictError:
            self.db.rollback()
            raise
        except IntegrityError as error:
            self.db.rollback()
            replay = self._revoke_request_replay(
                tenant_id=tenant_id,
                request_id=request_id,
                request_fingerprint=request_fingerprint,
            )
            if replay is None:
                raise OrganizationMemoryConflictError(
                    "组织记忆已被其他管理员撤销，请刷新后重试。"
                ) from error
            entry = replay
        return self._revoked_response(entry)
    def _new_active_generation(
        self,
        *,
        entry_id: str,
        tenant_id: str,
        scope_type: str,
        scope_id: str,
        value: str,
        expires_at: datetime,
        reason: str,
        request_id: str,
        request_fingerprint: str,
        expected_active_ids: set[str],
        current_user: CurrentUserContext,
        now: datetime,
    ) -> MemoryEntry:
        if value not in SUPPORTED_TRANSPORT_VALUES:
            raise ValueError("组织记忆只允许飞机、火车或轮船三种低敏枚举值。")
        existing = list(
            self.db.scalars(
                select(MemoryEntry)
                .where(
                    MemoryEntry.tenant_id == tenant_id,
                    MemoryEntry.scope_type == scope_type,
                    MemoryEntry.scope_id == scope_id,
                    MemoryEntry.scene == MEMORY_SCENE,
                    MemoryEntry.field_key == MEMORY_FIELD_KEY,
                    MemoryEntry.status == "active",
                )
                .with_for_update()
            ).all()
        )
        actual_active_ids = {entry.id for entry in existing}
        if actual_active_ids != expected_active_ids:
            raise OrganizationMemoryConflictError(
                "组织记忆生效版本已变化，请刷新后重试。"
            )
        generation = int(
            self.db.scalar(
                select(func.coalesce(func.max(MemoryEntry.generation), 0)).where(
                    MemoryEntry.tenant_id == tenant_id,
                    MemoryEntry.scope_type == scope_type,
                    MemoryEntry.scope_id == scope_id,
                    MemoryEntry.scene == MEMORY_SCENE,
                    MemoryEntry.field_key == MEMORY_FIELD_KEY,
                )
            )
            or 0
        ) + 1
        for old_entry in existing:
            old_entry.status = "suppressed"
            old_entry.suppressed_at = now
        if existing:
            self.db.flush()
        entry = MemoryEntry(
            id=entry_id,
            tenant_id=tenant_id,
            scope_type=scope_type,
            scope_id=scope_id,
            scene=MEMORY_SCENE,
            field_key=MEMORY_FIELD_KEY,
            origin_type="admin_managed",
            generation=generation,
            value_json={"value": value},
            value_fingerprint=hmac_fingerprint(
                {"field_key": MEMORY_FIELD_KEY, "value": value}
            ),
            status="active",
            evidence_count=0,
            approved_evidence_count=0,
            confidence=Decimal("1.0000"),
            last_evidence_at=now,
            candidate_expires_at=expires_at,
            activated_at=now,
            active_expires_at=expires_at,
            managed_by=self._actor_id(current_user),
            managed_at=now,
            management_reason=reason,
            policy_version=ORGANIZATION_MEMORY_POLICY_VERSION,
            management_request_id=request_id,
            management_payload_fingerprint=request_fingerprint,
        )
        self.db.add(entry)
        self.db.flush()
        for old_entry in existing:
            old_entry.superseded_by_id = entry.id
        self.db.flush()
        return entry

    @staticmethod
    def _scope_lock_key(
        *,
        tenant_id: str,
        scope_type: str,
        scope_id: str,
    ) -> str:
        return organization_memory_scope_lock_key(
            tenant_id=tenant_id,
            scope_type=scope_type,
            scope_id=scope_id,
            scene=MEMORY_SCENE,
            field_key=MEMORY_FIELD_KEY,
        )

    @classmethod
    def _scope_lock_key_for_entry(
        cls,
        tenant_id: str,
        entry: MemoryEntry,
    ) -> str:
        return cls._scope_lock_key(
            tenant_id=tenant_id,
            scope_type=str(entry.scope_type),
            scope_id=str(entry.scope_id),
        )

    @staticmethod
    def _request_lock_key(tenant_id: str, request_id: str) -> str:
        return organization_memory_request_lock_key(tenant_id, request_id)

    def _management_request_replay(
        self,
        *,
        tenant_id: str,
        request_id: str,
        request_fingerprint: str,
    ) -> MemoryEntry | None:
        if self._get_revoke_request_entry(tenant_id, request_id) is not None:
            raise OrganizationMemoryConflictError(
                "该幂等请求标识已用于其他组织记忆操作。"
            )
        entry = self.db.scalar(
            self._organization_entry_query().where(
                MemoryEntry.tenant_id == tenant_id,
                MemoryEntry.management_request_id == request_id,
            )
        )
        if entry is None:
            return None
        if entry.management_payload_fingerprint != request_fingerprint:
            raise OrganizationMemoryConflictError(
                "同一幂等请求标识对应的请求内容不一致。"
            )
        return entry

    def _revoke_request_replay(
        self,
        *,
        tenant_id: str,
        request_id: str,
        request_fingerprint: str,
    ) -> MemoryEntry | None:
        management_entry = self.db.scalar(
            self._organization_entry_query().where(
                MemoryEntry.tenant_id == tenant_id,
                MemoryEntry.management_request_id == request_id,
            )
        )
        if management_entry is not None:
            raise OrganizationMemoryConflictError(
                "该幂等请求标识已用于其他组织记忆操作。"
            )
        entry = self._get_revoke_request_entry(tenant_id, request_id)
        if entry is None:
            return None
        if entry.revoke_payload_fingerprint != request_fingerprint:
            raise OrganizationMemoryConflictError(
                "同一幂等请求标识对应的请求内容不一致。"
            )
        if entry.status != "revoked" or entry.revoked_at is None:
            raise OrganizationMemoryConflictError("撤销操作审计状态不完整，请人工复核。")
        return entry

    def _get_revoke_request_entry(
        self,
        tenant_id: str,
        request_id: str,
    ) -> MemoryEntry | None:
        return self.db.scalar(
            self._organization_entry_query().where(
                MemoryEntry.tenant_id == tenant_id,
                MemoryEntry.revoke_request_id == request_id,
            )
        )

    @staticmethod
    def _request_fingerprint(
        *,
        operation: str,
        target: str,
        payload: dict[str, object],
    ) -> str:
        return hmac_fingerprint(
            {
                "protocol": "organization_memory_mutation.v1",
                "operation": operation,
                "target": target,
                "payload": payload,
            }
        )

    @staticmethod
    def _normalize_request_id(value: str) -> str:
        normalized = str(value or "").strip()
        if len(normalized) < 8:
            raise ValueError("组织记忆管理操作缺少有效的幂等请求标识。")
        return normalized[:120]

    @staticmethod
    def _revoked_response(entry: MemoryEntry) -> ExpenseApplicationMemoryRevokedRead:
        if entry.revoked_at is None:
            raise OrganizationMemoryConflictError("撤销操作缺少审计时间，请人工复核。")
        return ExpenseApplicationMemoryRevokedRead(
            memory_id=entry.id,
            revoked_at=entry.revoked_at,
        )

    @staticmethod
    def _organization_entry_query():
        return select(MemoryEntry).where(
            MemoryEntry.scope_type.in_(ORGANIZATION_SCOPE_TYPES),
            MemoryEntry.scene == MEMORY_SCENE,
            MemoryEntry.field_key == MEMORY_FIELD_KEY,
        )

    def _get_entry(
        self,
        memory_id: str,
        tenant_id: str,
        *,
        for_update: bool = True,
    ) -> MemoryEntry:
        statement = self._organization_entry_query().where(
            MemoryEntry.id == str(memory_id or "").strip(),
            MemoryEntry.tenant_id == tenant_id,
        )
        if for_update:
            statement = statement.with_for_update()
        entry = self.db.scalar(statement)
        if entry is None:
            raise OrganizationMemoryNotFoundError("未找到当前租户内可管理的组织记忆。")
        return entry

    def _get_scope_active_entry(
        self,
        *,
        tenant_id: str,
        scope_type: str,
        scope_id: str,
    ) -> MemoryEntry | None:
        return self.db.scalar(
            select(MemoryEntry)
            .where(
                MemoryEntry.tenant_id == tenant_id,
                MemoryEntry.scope_type == scope_type,
                MemoryEntry.scope_id == scope_id,
                MemoryEntry.scene == MEMORY_SCENE,
                MemoryEntry.field_key == MEMORY_FIELD_KEY,
                MemoryEntry.status == "active",
            )
            .with_for_update()
        )

    @staticmethod
    def _require_active(entry: MemoryEntry) -> None:
        if entry.status != "active":
            raise OrganizationMemoryConflictError(
                "该组织记忆已被换代、撤销或失效，请刷新后重试。"
            )

    @staticmethod
    def _request_entry_id(
        *,
        tenant_id: str,
        operation: str,
        target: str,
        request_id: str,
    ) -> str:
        normalized_request_id = str(request_id or "").strip()
        if not normalized_request_id:
            raise ValueError("组织记忆管理操作缺少幂等请求标识。")
        material = "|".join(
            (tenant_id, operation, target, normalized_request_id)
        )
        return str(uuid.uuid5(uuid.NAMESPACE_URL, f"x-financial:memory:{material}"))

    def _validate_scope(
        self,
        *,
        tenant_id: str,
        scope_type: str,
        scope_id: str | None,
    ) -> str:
        normalized_scope_id = str(scope_id or "").strip()
        if scope_type == "enterprise":
            if normalized_scope_id and normalized_scope_id != tenant_id:
                raise ValueError("企业记忆的 scope_id 必须等于当前租户 ID。")
            return tenant_id
        if scope_type != "department" or not normalized_scope_id:
            raise ValueError("部门记忆必须提供稳定的 OrganizationUnit.id。")
        department = self.db.get(OrganizationUnit, normalized_scope_id)
        if department is None or str(department.unit_type or "") != "department":
            raise ValueError("部门记忆只能绑定已存在的 department 类型组织单元。")
        return normalized_scope_id

    @staticmethod
    def _require_admin(current_user: CurrentUserContext) -> None:
        if not current_user.is_admin:
            raise PermissionError("只有平台管理员可以维护企业或部门记忆。")

    @staticmethod
    def _validate_expected_generation(entry: MemoryEntry, expected: int) -> None:
        if int(entry.generation or 0) != expected:
            raise OrganizationMemoryConflictError(
                "组织记忆已被其他管理员更新，请刷新后重试。"
            )

    def _department_names(self, entries: list[MemoryEntry]) -> dict[str, str]:
        department_ids = {
            str(entry.scope_id)
            for entry in entries
            if entry.scope_type == "department" and str(entry.scope_id or "")
        }
        if not department_ids:
            return {}
        return {
            unit.id: str(unit.name or "").strip()
            for unit in self.db.scalars(
                select(OrganizationUnit).where(OrganizationUnit.id.in_(department_ids))
            ).all()
        }

    @staticmethod
    def _serialize_entry(
        entry: MemoryEntry,
        department_names: dict[str, str],
    ) -> ExpenseApplicationMemoryRead:
        scope_type = str(entry.scope_type)
        if scope_type == "enterprise":
            scope_label = "企业统一规则"
        else:
            name = department_names.get(str(entry.scope_id), "")
            scope_label = f"部门规则（{name}）" if name else "部门规则"
        return ExpenseApplicationMemoryRead(
            id=entry.id,
            scene=entry.scene,
            field_key=entry.field_key,
            value=ExpenseApplicationOrganizationMemoryService._entry_value(entry),
            status=entry.status,
            scope_type=scope_type,
            scope_id=str(entry.scope_id),
            scope_label=scope_label,
            source=memory_source(scope_type),
            origin_type=str(getattr(entry, "origin_type", "admin_managed")),
            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=0,
            policy_version=str(
                getattr(entry, "policy_version", ORGANIZATION_MEMORY_POLICY_VERSION)
            ),
            valid_from=entry.activated_at or entry.created_at,
            expires_at=entry.active_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=entry.status == "active",
            created_at=entry.created_at,
            updated_at=entry.updated_at,
        )

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

    @staticmethod
    def _actor_id(current_user: CurrentUserContext) -> str:
        value = str(current_user.employee_id or current_user.username or "").strip()
        if not value:
            raise ValueError("当前管理员缺少稳定的操作人标识。")
        return value[:120]

    @staticmethod
    def _normalize_reason(value: str) -> str:
        normalized = str(value or "").strip()
        if not normalized:
            raise ValueError("组织记忆管理操作必须填写原因。")
        return normalized[:255]

    @staticmethod
    def _is_expired(entry: MemoryEntry, now: datetime) -> bool:
        return (
            entry.active_expires_at is not None
            and ExpenseApplicationOrganizationMemoryService._aware(
                entry.active_expires_at
            )
            <= now
        )

    @staticmethod
    def _aware(value: datetime | None) -> datetime:
        if value is None:
            raise ValueError("组织记忆缺少有效期。")
        return value if value.tzinfo is not None else value.replace(tzinfo=UTC)
