"""动作级自动化资格计算。

该模块只计算一个动作在当前证据下允许达到的最高自动化级别，不执行动作。
默认策略保持在人审/影子模式；只有系统硬白名单与企业白名单的交集才可能进入
自动执行，资金支付和高风险制度动作始终需要人工确认。
"""

from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal, InvalidOperation
from enum import IntEnum
from math import isfinite
from typing import Any, Literal


class AutomationLevel(IntEnum):
    """与费用闭环契约一致的动作级别；L5 不是更高自治。"""

    L0_EXPLAIN = 0
    L1_RECOMMEND = 1
    L2_PREFILL = 2
    L3_REVERSIBLE_AUTO = 3
    L4_LOW_RISK_STRAIGHT_THROUGH = 4
    L5_HUMAN_CONTROLLED = 5


ActionRisk = Literal["low", "medium", "high", "critical"]


SYSTEM_AUTO_WHITELIST = frozenset(
    {
        "expense.prefill_fields",
        "expense.classify_receipt",
        "expense.deduplicate_attachment",
        "expense.request_missing_information",
        "expense.route_for_review",
        "expense.apply_user_preference",
    }
)

NEVER_AUTO_ACTIONS = frozenset(
    {
        "expense.execute_payment",
        "expense.release_funds",
        "expense.change_policy",
        "expense.publish_policy",
        "expense.disable_control",
        "expense.waive_high_risk_control",
    }
)


@dataclass(frozen=True)
class AutomationPolicy:
    """企业自动化上限；企业白名单不能扩张系统硬白名单。"""

    enabled: bool = False
    enterprise_max_level: AutomationLevel = AutomationLevel.L1_RECOMMEND
    enterprise_whitelist: frozenset[str] = field(default_factory=frozenset)
    amount_cap: Decimal = Decimal("0")
    min_confidence: float = 0.95
    min_evidence_completeness: float = 0.95
    min_historical_precision: float = 0.98
    min_historical_samples: int = 100
    minimum_sampling_rate: float = 0.10
    release_stage: Literal["shadow", "canary", "active"] = "shadow"


@dataclass(frozen=True)
class AutomationEligibilityInput:
    action_key: str
    action_risk: ActionRisk = "medium"
    amount: Decimal = Decimal("0")
    confidence: float = 0.0
    evidence_completeness: float = 0.0
    reversible: bool = False
    historical_precision: float | None = None
    historical_samples: int = 0
    sampling_rate: float = 0.0
    moves_money: bool = False
    changes_policy: bool = False


@dataclass(frozen=True)
class AutomationEligibilityDecision:
    eligible_level: AutomationLevel
    mode: str
    eligible_for_auto_execution: bool
    requires_human_confirmation: bool
    sampling_rate: float
    reasons: tuple[str, ...]
    checks: dict[str, Any]

    def to_dict(self) -> dict[str, Any]:
        return {
            "eligible_level": int(self.eligible_level),
            "level_name": self.eligible_level.name,
            "mode": self.mode,
            "eligible_for_auto_execution": self.eligible_for_auto_execution,
            "requires_human_confirmation": self.requires_human_confirmation,
            "sampling_rate": self.sampling_rate,
            "reasons": list(self.reasons),
            "checks": self.checks,
        }


class AutomationEligibilityCalculator:
    """按最弱控制项计算动作的最高资格，所有异常输入均收紧而非放宽。"""

    def calculate(
        self,
        candidate: AutomationEligibilityInput,
        policy: AutomationPolicy | None = None,
    ) -> AutomationEligibilityDecision:
        policy = policy or AutomationPolicy()
        action_key = str(candidate.action_key or "").strip()
        amount, amount_valid = _validated_amount(candidate.amount)
        confidence = _clamp01(candidate.confidence)
        evidence = _clamp01(candidate.evidence_completeness)
        precision = _optional_clamp01(candidate.historical_precision)
        sampling_rate = _clamp01(candidate.sampling_rate)
        historical_samples, historical_samples_valid = _safe_count(
            candidate.historical_samples,
            default=0,
        )
        minimum_samples, minimum_samples_valid = _safe_count(
            policy.min_historical_samples,
            default=100,
            minimum=1,
        )
        enterprise_level = _safe_level(policy.enterprise_max_level)
        enterprise_whitelist = (
            policy.enterprise_whitelist
            if isinstance(policy.enterprise_whitelist, (set, frozenset, list, tuple))
            else frozenset()
        )
        release_stage = str(policy.release_stage or "").strip().lower()
        risk = (
            candidate.action_risk
            if candidate.action_risk in {"low", "medium", "high", "critical"}
            else "critical"
        )
        system_whitelisted = action_key in SYSTEM_AUTO_WHITELIST
        enterprise_whitelisted = action_key in enterprise_whitelist
        explicitly_forbidden = (
            action_key in NEVER_AUTO_ACTIONS
            or candidate.moves_money
            or candidate.changes_policy
            or risk in {"high", "critical"}
        )
        checks = {
            "policy_enabled": bool(policy.enabled),
            "system_whitelisted": system_whitelisted,
            "enterprise_whitelisted": enterprise_whitelisted,
            "action_risk": risk,
            "amount": str(amount),
            "amount_valid": amount_valid,
            "amount_cap": str(_non_negative_decimal(policy.amount_cap)),
            "confidence": confidence,
            "evidence_completeness": evidence,
            "reversible": bool(candidate.reversible),
            "historical_precision": precision,
            "historical_samples": historical_samples,
            "historical_samples_valid": historical_samples_valid,
            "minimum_historical_samples": minimum_samples,
            "minimum_historical_samples_valid": minimum_samples_valid,
            "sampling_rate": sampling_rate,
            "enterprise_max_level": int(enterprise_level),
            "release_stage": release_stage
            if release_stage in {"shadow", "canary", "active"}
            else "shadow",
            "money_or_policy_forbidden": explicitly_forbidden,
        }

        if enterprise_level == AutomationLevel.L5_HUMAN_CONTROLLED:
            enterprise_level = AutomationLevel.L0_EXPLAIN
        if not policy.enabled:
            return self._decision(
                min(AutomationLevel.L1_RECOMMEND, enterprise_level),
                sampling_rate,
                ("enterprise_automation_disabled",),
                checks,
                release_stage="shadow",
            )
        if explicitly_forbidden:
            return self._decision(
                AutomationLevel.L5_HUMAN_CONTROLLED,
                sampling_rate,
                ("money_policy_or_high_risk_action_requires_human",),
                checks,
                release_stage=release_stage,
            )
        if not system_whitelisted or not enterprise_whitelisted:
            return self._decision(
                min(AutomationLevel.L1_RECOMMEND, enterprise_level),
                sampling_rate,
                ("action_not_in_hard_whitelist",),
                checks,
                release_stage="shadow",
            )

        reasons: list[str] = []
        level = AutomationLevel.L4_LOW_RISK_STRAIGHT_THROUGH
        amount_cap = _non_negative_decimal(policy.amount_cap)
        if not amount_valid:
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("amount_invalid")
        elif amount > amount_cap:
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("amount_exceeds_enterprise_cap")
        if risk != "low":
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("action_risk_not_low")
        if not candidate.reversible:
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("action_not_reversible")
        if confidence < _clamp01(policy.min_confidence):
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("confidence_below_threshold")
        if evidence < _clamp01(policy.min_evidence_completeness):
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("evidence_incomplete")
        if precision is None or precision < _clamp01(policy.min_historical_precision):
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("historical_precision_insufficient")
        if not historical_samples_valid:
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("historical_samples_invalid")
        if not minimum_samples_valid:
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("historical_sample_policy_invalid")
        if historical_samples < minimum_samples:
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("historical_sample_insufficient")
        if sampling_rate < _clamp01(policy.minimum_sampling_rate):
            level = min(level, AutomationLevel.L2_PREFILL)
            reasons.append("sampling_rate_below_threshold")

        if level > enterprise_level:
            level = enterprise_level
            reasons.append("clamped_by_enterprise_level")
        if (
            level
            in {
                AutomationLevel.L3_REVERSIBLE_AUTO,
                AutomationLevel.L4_LOW_RISK_STRAIGHT_THROUGH,
            }
            and not reasons
        ):
            reasons.append("all_automation_controls_passed")
        return self._decision(
            level,
            sampling_rate,
            tuple(reasons),
            checks,
            release_stage=release_stage,
        )

    @staticmethod
    def _decision(
        level: AutomationLevel,
        sampling_rate: float,
        reasons: tuple[str, ...],
        checks: dict[str, Any],
        *,
        release_stage: str,
    ) -> AutomationEligibilityDecision:
        action_mode = {
            AutomationLevel.L0_EXPLAIN: "explain_only",
            AutomationLevel.L1_RECOMMEND: "recommend",
            AutomationLevel.L2_PREFILL: "prefill",
            AutomationLevel.L3_REVERSIBLE_AUTO: "reversible_auto",
            AutomationLevel.L4_LOW_RISK_STRAIGHT_THROUGH: "low_risk_straight_through",
            AutomationLevel.L5_HUMAN_CONTROLLED: "human_controlled",
        }[level]
        normalized_stage = (
            release_stage if release_stage in {"shadow", "canary", "active"} else "shadow"
        )
        automatic = level in {
            AutomationLevel.L3_REVERSIBLE_AUTO,
            AutomationLevel.L4_LOW_RISK_STRAIGHT_THROUGH,
        } and normalized_stage in {"canary", "active"}
        return AutomationEligibilityDecision(
            eligible_level=level,
            mode=(f"shadow_{action_mode}" if normalized_stage == "shadow" else action_mode),
            eligible_for_auto_execution=automatic,
            requires_human_confirmation=not automatic,
            sampling_rate=sampling_rate,
            reasons=reasons,
            checks=checks,
        )


def _safe_level(value: AutomationLevel | int) -> AutomationLevel:
    try:
        numeric = int(value)
    except (TypeError, ValueError, OverflowError):
        return AutomationLevel.L1_RECOMMEND
    return AutomationLevel(max(0, min(5, numeric)))


def _non_negative_decimal(value: Decimal | int | float | str) -> Decimal:
    try:
        parsed = Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        return Decimal("0")
    if not parsed.is_finite():
        return Decimal("0")
    return max(Decimal("0"), parsed)


def _validated_amount(value: Decimal | int | float | str) -> tuple[Decimal, bool]:
    try:
        parsed = Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        return Decimal("0"), False
    if not parsed.is_finite() or parsed < 0:
        return Decimal("0"), False
    return parsed, True


def _clamp01(value: float | int | Decimal | None) -> float:
    try:
        parsed = float(value or 0)
    except (TypeError, ValueError):
        return 0.0
    if not isfinite(parsed):
        return 0.0
    return max(0.0, min(1.0, parsed))


def _optional_clamp01(value: float | int | Decimal | None) -> float | None:
    return None if value is None else _clamp01(value)


def _safe_count(
    value: Any,
    *,
    default: int,
    minimum: int = 0,
) -> tuple[int, bool]:
    try:
        parsed = int(value)
    except (TypeError, ValueError, OverflowError):
        return max(minimum, default), False
    if parsed < minimum:
        return max(minimum, default), False
    return parsed, True
