from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime

from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.risk_disposition import RiskDisposition
from app.models.risk_observation import RiskObservation
from app.schemas.risk_disposition import RiskDispositionRead
from app.schemas.risk_observation import RiskObservationRead
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin
from app.services.risk_observation_access_policy import RiskObservationAccessPolicy
from app.services.risk_waiver_decision_policy import RiskWaiverDecisionPolicy


@dataclass(frozen=True, slots=True)
class RiskDispositionActionPermission:
    available_actions: tuple[str, ...]
    read_only_reason: str = ""


class RiskDispositionActionPolicy:
    """根据当前用户与实时处置状态计算服务端权威动作集合。"""

    def __init__(self, db: Session) -> None:
        self.db = db
        self.access_policy = RiskObservationAccessPolicy(db)
        self.waiver_policy = RiskWaiverDecisionPolicy(db)

    def evaluate(
        self,
        observation: RiskObservation,
        disposition: RiskDisposition | RiskDispositionRead | None,
        current_user: CurrentUserContext | None,
        *,
        now: datetime | None = None,
    ) -> RiskDispositionActionPermission:
        current_at = _as_utc(now) or datetime.now(UTC)
        tenant_id = ExpenseClaimTenantScopeMixin.normalize_tenant_id(observation.tenant_id)
        if current_user is None:
            return RiskDispositionActionPermission((), "请先登录后再处理风险观察。")
        if ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id) != tenant_id:
            return RiskDispositionActionPermission((), "风险观察不在当前租户范围内。")

        adjudication = _adjudication(observation, disposition)
        lifecycle = _lifecycle(observation, disposition)
        can_manage = self.access_policy.can_manage_disposition(observation, current_user)
        actions: list[str] = []
        if can_manage and lifecycle != "resolved":
            if adjudication != "confirmed":
                actions.append("confirm")
            if adjudication != "false_positive":
                actions.append("false_positive")
            if lifecycle != "supplement_requested":
                actions.append("request_supplement")
            if adjudication == "confirmed":
                if lifecycle != "remediation_in_progress":
                    actions.append("start_remediation")
                if lifecycle != "waiver_requested" or not _text(
                    disposition.waiver_requester_id if disposition is not None else None
                ):
                    actions.append("request_waiver")
                actions.append("resolve")

        decision_reason = ""
        if disposition is not None and lifecycle == "waiver_requested":
            eligibility = self.waiver_policy.evaluate_decider(
                disposition,
                current_user=current_user,
                tenant_id=tenant_id,
            )
            decision_reason = eligibility.reason
            if eligibility.allowed:
                expires_at = _as_utc(disposition.waiver_expires_at)
                if expires_at is not None and expires_at > current_at:
                    actions.append("approve_waiver")
                actions.append("reject_waiver")

        available_actions = tuple(dict.fromkeys(actions))
        if available_actions:
            return RiskDispositionActionPermission(available_actions)
        if lifecycle == "resolved":
            reason = "该风险已经解决，当前仅可查看审计记录。"
        elif lifecycle == "waived" and _is_unexpired(disposition, now=current_at):
            reason = "该风险处于有效豁免期，当前仅可查看审计记录。"
        elif lifecycle == "waiver_requested" and decision_reason:
            reason = decision_reason
        elif not can_manage:
            reason = "当前用户只有查看权限，无风险处置权限。"
        else:
            reason = "当前状态没有可执行的风险处置动作。"
        return RiskDispositionActionPermission((), reason)


class RiskDispositionReadProjection:
    """把同一权限投影注入风险列表、详情和处置详情响应。"""

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

    def observation_read(
        self,
        observation: RiskObservation,
        current_user: CurrentUserContext,
    ) -> RiskObservationRead:
        disposition = observation.disposition
        permission = self.policy.evaluate(observation, disposition, current_user)
        disposition_read = (
            self._with_permission(
                RiskDispositionRead.model_validate(disposition),
                permission,
            )
            if disposition is not None
            else None
        )
        return RiskObservationRead.model_validate(observation).model_copy(
            update={
                "disposition": disposition_read,
                "available_actions": list(permission.available_actions),
                "read_only_reason": permission.read_only_reason,
            }
        )

    def observation_list_read(
        self,
        observations: list[RiskObservation],
        current_user: CurrentUserContext,
    ) -> list[RiskObservationRead]:
        return [self.observation_read(item, current_user) for item in observations]

    def disposition_read(
        self,
        disposition: RiskDisposition,
        observation: RiskObservation,
        current_user: CurrentUserContext,
    ) -> RiskDispositionRead:
        return self.disposition_snapshot_read(
            RiskDispositionRead.model_validate(disposition),
            observation,
            current_user,
        )

    def disposition_snapshot_read(
        self,
        disposition: RiskDispositionRead,
        observation: RiskObservation,
        current_user: CurrentUserContext,
    ) -> RiskDispositionRead:
        """为幂等响应快照补权限字段，不改写快照中的历史业务状态。"""

        permission = self.policy.evaluate(observation, disposition, current_user)
        return self._with_permission(disposition, permission)

    @staticmethod
    def _with_permission(
        disposition: RiskDispositionRead,
        permission: RiskDispositionActionPermission,
    ) -> RiskDispositionRead:
        return disposition.model_copy(
            update={
                "available_actions": list(permission.available_actions),
                "read_only_reason": permission.read_only_reason,
            }
        )


def _adjudication(
    observation: RiskObservation,
    disposition: RiskDisposition | RiskDispositionRead | None,
) -> str:
    if disposition is not None:
        return _text(disposition.adjudication).lower() or "unreviewed"
    values = {
        _text(observation.feedback_status).lower(),
        _text(observation.status).lower(),
    }
    if "false_positive" in values:
        return "false_positive"
    if "confirmed" in values:
        return "confirmed"
    return "unreviewed"


def _lifecycle(
    observation: RiskObservation,
    disposition: RiskDisposition | RiskDispositionRead | None,
) -> str:
    if disposition is not None:
        return _text(disposition.lifecycle_status).lower() or "open"
    return "resolved" if _text(observation.status).lower() == "resolved" else "open"


def _is_unexpired(
    disposition: RiskDisposition | RiskDispositionRead | None,
    *,
    now: datetime | None,
) -> bool:
    expires_at = _as_utc(disposition.waiver_expires_at if disposition is not None else None)
    current_at = _as_utc(now) or datetime.now(UTC)
    return expires_at is not None and expires_at > current_at


def _as_utc(value: datetime | None) -> datetime | None:
    if value is None:
        return None
    if value.tzinfo is None:
        return value.replace(tzinfo=UTC)
    return value.astimezone(UTC)


def _text(value: object) -> str:
    return str(value or "").strip()
