"""从普通发布或 release guard 快照解析实际运行的风险规则。"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentAssetType
from app.models.agent_asset import AgentAsset
from app.services.agent_asset_release_artifacts import AgentAssetReleaseArtifactService
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
from app.services.risk_rule_manifest_classifier import is_budget_risk_manifest
from app.services.risk_rule_manifest_normalizer import normalize_risk_rule_manifest


@dataclass(frozen=True)
class _ReleaseRoute:
    version: str
    mode: str


class ExpenseClaimRiskRuleLoader:
    """按租户、业务阶段与稳定路由键返回强制/影子规则。"""

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

    def load(
        self,
        *,
        rule_codes: list[str] | None,
        business_stage: str | None,
        routing_key: str,
        tenant_id: str,
        stage_matcher: Any,
    ) -> list[dict[str, Any]]:
        code_filter = {
            str(code or "").strip() for code in list(rule_codes or []) if str(code or "").strip()
        }
        assets = list(
            self.db.scalars(
                select(AgentAsset)
                .where(
                    (
                        ((AgentAsset.scope == "tenant") & (AgentAsset.tenant_id == tenant_id))
                        | (
                            (AgentAsset.scope == "platform")
                            & (AgentAsset.tenant_id == "platform")
                        )
                    ),
                    AgentAsset.asset_type == AgentAssetType.RULE.value,
                )
                .where(AgentAsset.domain == AgentAssetDomain.EXPENSE.value)
                .order_by(AgentAsset.updated_at.desc(), AgentAsset.created_at.desc())
            ).all()
        )
        assets.sort(key=lambda item: self._tenant_priority(item, tenant_id), reverse=True)
        manifests_by_code: dict[str, dict[str, Any]] = {}
        shadow_manifests: list[dict[str, Any]] = []
        reserved_codes: set[str] = set()
        for asset in assets:
            config = asset.config_json if isinstance(asset.config_json, dict) else {}
            rule_code = str(asset.code or "").strip()
            if rule_code and str(config.get("detail_mode") or "").strip().lower() == "json_risk":
                reserved_codes.add(rule_code)
            if not self._asset_is_visible(asset, config, tenant_id=tenant_id):
                continue
            if code_filter and rule_code not in code_filter:
                continue
            state = config.get("release_guard")
            release_state = dict(state) if isinstance(state, dict) else {}
            routes = self._release_routes(asset, release_state, routing_key=routing_key)
            if not routes:
                if asset.status != AgentAssetStatus.ACTIVE.value:
                    continue
                manifest = self._load_document_manifest(asset, config)
                if manifest is not None:
                    self._accept_manifest(
                        manifests_by_code,
                        manifest,
                        code_filter=code_filter,
                        business_stage=business_stage,
                        stage_matcher=stage_matcher,
                    )
                continue

            for route in routes:
                manifest = self._load_release_manifest(asset, release_state, route)
                if manifest is None and route.mode == "enforced":
                    manifest = self._safe_previous_manifest(asset, release_state)
                    if manifest is None and route.version:
                        # 受控发布的候选和稳定快照都不可用时不能静默跳过规则，
                        # 否则损坏配置会把高风险规则变成“未命中”。
                        manifest = self._release_integrity_failure_manifest(
                            asset,
                            release_state,
                            failed_version=route.version,
                        )
                if manifest is None or not self._manifest_is_eligible(
                    manifest,
                    code_filter=code_filter,
                    business_stage=business_stage,
                    stage_matcher=stage_matcher,
                ):
                    continue
                if route.mode == "shadow":
                    shadow_manifests.append(manifest)
                else:
                    manifests_by_code.setdefault(str(manifest["rule_code"]), manifest)

        self._load_library_fallbacks(
            manifests_by_code,
            reserved_codes=reserved_codes,
            code_filter=code_filter,
            business_stage=business_stage,
            stage_matcher=stage_matcher,
        )
        return [*manifests_by_code.values(), *shadow_manifests]

    @staticmethod
    def _tenant_priority(asset: AgentAsset, tenant_id: str) -> tuple[int, Any, Any]:
        return (
            2 if asset.scope == "tenant" and asset.tenant_id == tenant_id else 1,
            asset.updated_at,
            asset.created_at,
        )

    @staticmethod
    def _asset_is_visible(
        asset: AgentAsset,
        config: dict[str, Any],
        *,
        tenant_id: str,
    ) -> bool:
        if str(config.get("detail_mode") or "").strip().lower() != "json_risk":
            return False
        if asset.scope == "tenant" and asset.tenant_id != tenant_id:
            return False
        if asset.scope == "platform" and asset.tenant_id != "platform":
            return False
        if asset.status == AgentAssetStatus.DISABLED.value or config.get("enabled") is False:
            return False
        state = config.get("release_guard")
        stage = str(state.get("stage") or "") if isinstance(state, dict) else ""
        return asset.status == AgentAssetStatus.ACTIVE.value or stage in {
            "shadow",
            "canary",
            "active",
            "rolled_back",
        }

    def _load_document_manifest(
        self,
        asset: AgentAsset,
        config: dict[str, Any],
    ) -> dict[str, Any] | None:
        document = config.get("rule_document")
        if not isinstance(document, dict):
            return None
        file_name = str(document.get("file_name") or "").strip()
        library = str(config.get("rule_library") or RISK_RULES_LIBRARY).strip()
        if not file_name:
            return None
        try:
            payload = self.rule_library_manager.read_rule_library_json(
                library=library,
                file_name=file_name,
            )
        except (FileNotFoundError, ValueError):
            return None
        payload = normalize_risk_rule_manifest(payload)
        if payload.get("enabled") is False:
            return None
        return self._decorate(
            payload,
            asset=asset,
            version=str(asset.published_version or asset.current_version or "v1.0.0"),
            stage="unmanaged",
            mode="enforced",
        )

    def _load_release_manifest(
        self,
        asset: AgentAsset,
        state: dict[str, Any],
        route: _ReleaseRoute,
    ) -> dict[str, Any] | None:
        try:
            artifact = AgentAssetReleaseArtifactService.artifact(state, route.version)
        except ValueError:
            return None
        manifest = dict(artifact["manifest"])
        manifest["enabled"] = True
        return self._decorate(
            manifest,
            asset=asset,
            version=route.version,
            stage=str(state.get("stage") or "unmanaged"),
            mode=route.mode,
        )

    def _safe_previous_manifest(
        self,
        asset: AgentAsset,
        state: dict[str, Any],
    ) -> dict[str, Any] | None:
        previous = str(state.get("previous_version") or "").strip()
        if not previous:
            return None
        return self._load_release_manifest(asset, state, _ReleaseRoute(previous, "enforced"))

    @staticmethod
    def _release_integrity_failure_manifest(
        asset: AgentAsset,
        state: dict[str, Any],
        *,
        failed_version: str,
    ) -> dict[str, Any]:
        """把不可恢复的发布快照故障转换为强制人工阻断信号。"""

        stage = str(state.get("stage") or "unmanaged").strip() or "unmanaged"
        return {
            "rule_code": str(asset.code or "release_guard_integrity_failure").strip(),
            "name": "风险规则发布快照完整性异常",
            "description": "受控发布快照无法校验，已停止该申请自动流转。",
            "evaluator": "release_guard_integrity_failure",
            "enabled": True,
            "applies_to": {
                "domains": ["expense"],
                "business_stages": ["expense_application", "reimbursement"],
            },
            "outcomes": {
                "fail": {
                    "severity": "critical",
                    "action": "block",
                }
            },
            "metadata": {
                "risk_domain": "policy",
                "visibility_scope": "leader",
                "actionability": "review_decision",
            },
            "_rule_version": failed_version,
            "_rule_asset_id": asset.id,
            "_release_stage": stage,
            "_release_mode": "enforced",
            "_release_integrity_error": {
                "asset_id": asset.id,
                "release_id": str(state.get("release_id") or ""),
                "failed_version": failed_version,
                "candidate_version": str(state.get("candidate_version") or ""),
                "previous_version": str(state.get("previous_version") or ""),
            },
        }

    @staticmethod
    def _release_routes(
        asset: AgentAsset,
        state: dict[str, Any],
        *,
        routing_key: str,
    ) -> list[_ReleaseRoute]:
        stage = str(state.get("stage") or "").strip()
        candidate = str(state.get("candidate_version") or "").strip()
        previous = str(state.get("previous_version") or asset.published_version or "").strip()
        if stage == "shadow":
            routes = [_ReleaseRoute(previous, "enforced")] if previous else []
            if candidate:
                routes.append(_ReleaseRoute(candidate, "shadow"))
            return routes
        if stage == "canary":
            policy = state.get("policy") if isinstance(state.get("policy"), dict) else {}
            traffic = _safe_percent(policy.get("canary_traffic_percent"), 5)
            if candidate and _is_candidate_route(asset.id, routing_key, traffic):
                return [_ReleaseRoute(candidate, "enforced")]
            return [_ReleaseRoute(previous, "enforced")] if previous else []
        if stage == "active" and candidate:
            return [_ReleaseRoute(candidate, "enforced")]
        if stage == "rolled_back" and previous:
            return [_ReleaseRoute(previous, "enforced")]
        return []

    @staticmethod
    def _decorate(
        manifest: dict[str, Any],
        *,
        asset: AgentAsset,
        version: str,
        stage: str,
        mode: str,
    ) -> dict[str, Any]:
        payload = dict(manifest)
        payload.setdefault("rule_code", str(asset.code or "").strip())
        payload["_rule_version"] = version
        payload["_rule_asset_id"] = asset.id
        payload["_release_stage"] = stage
        payload["_release_mode"] = mode
        return payload

    def _accept_manifest(
        self,
        target: dict[str, dict[str, Any]],
        manifest: dict[str, Any],
        *,
        code_filter: set[str],
        business_stage: str | None,
        stage_matcher: Any,
    ) -> None:
        if self._manifest_is_eligible(
            manifest,
            code_filter=code_filter,
            business_stage=business_stage,
            stage_matcher=stage_matcher,
        ):
            target.setdefault(str(manifest["rule_code"]), manifest)

    @staticmethod
    def _manifest_is_eligible(
        manifest: dict[str, Any],
        *,
        code_filter: set[str],
        business_stage: str | None,
        stage_matcher: Any,
    ) -> bool:
        rule_code = str(manifest.get("rule_code") or "").strip()
        return bool(
            rule_code
            and (not code_filter or rule_code in code_filter)
            and not is_budget_risk_manifest(manifest)
            and manifest.get("enabled") is not False
            and stage_matcher(manifest, business_stage=business_stage)
        )

    def _load_library_fallbacks(
        self,
        target: dict[str, dict[str, Any]],
        *,
        reserved_codes: set[str],
        code_filter: set[str],
        business_stage: str | None,
        stage_matcher: Any,
    ) -> None:
        try:
            files = self.rule_library_manager.list_rule_library_json_files(
                library=RISK_RULES_LIBRARY
            )
        except ValueError:
            return
        for file_name in files:
            try:
                payload = normalize_risk_rule_manifest(
                    self.rule_library_manager.read_rule_library_json(
                        library=RISK_RULES_LIBRARY,
                        file_name=file_name,
                    )
                )
            except (FileNotFoundError, ValueError):
                continue
            payload = dict(payload)
            rule_code = str(payload.get("rule_code") or "").strip()
            if rule_code in reserved_codes:
                continue
            payload["_rule_version"] = "v1.0.0"
            payload["_release_stage"] = "library_fallback"
            payload["_release_mode"] = "enforced"
            self._accept_manifest(
                target,
                payload,
                code_filter=code_filter,
                business_stage=business_stage,
                stage_matcher=stage_matcher,
            )


def _safe_percent(value: Any, default: int) -> int:
    try:
        parsed = int(value)
    except (TypeError, ValueError, OverflowError):
        parsed = default
    return max(0, min(100, parsed))


def _is_candidate_route(asset_id: str, routing_key: str, traffic_percent: int) -> bool:
    if not routing_key or traffic_percent <= 0:
        return False
    digest = hashlib.sha256(f"{asset_id}:{routing_key}".encode()).digest()
    return int.from_bytes(digest[:8], "big") % 100 < traffic_percent
