"""风险规则分阶段发布使用的不可变运行快照。"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any

from app.core.agent_enums import AgentAssetType
from app.models.agent_asset import AgentAsset
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_normalizer import normalize_risk_rule_manifest


@dataclass(frozen=True)
class ReleaseArtifactBundle:
    artifacts: dict[str, dict[str, Any]]
    previous_config: dict[str, Any]


class AgentAssetReleaseArtifactService:
    """读取并冻结候选与基线规则，避免发布期间文件被原地改写。"""

    _PROHIBITED_AUTOMATION_ACTIONS = {
        "approve",
        "auto_approve",
        "auto_reject",
        "execute_payment",
        "pay",
        "reject",
        "transfer",
    }

    def __init__(
        self,
        rule_library_manager: AgentAssetRuleLibraryManager | None = None,
    ) -> None:
        self.rule_library_manager = rule_library_manager or AgentAssetRuleLibraryManager()

    def capture(
        self,
        asset: AgentAsset,
        candidate_version: str,
        *,
        previous_state: dict[str, Any] | None = None,
    ) -> ReleaseArtifactBundle:
        config = dict(asset.config_json or {})
        config.pop("release_guard", None)
        if asset.asset_type != AgentAssetType.RULE.value:
            return ReleaseArtifactBundle(artifacts={}, previous_config=config)
        if str(config.get("detail_mode") or "").strip().lower() != "json_risk":
            raise ValueError("分阶段发布当前只支持 JSON 风险规则资产。")

        library = str(config.get("rule_library") or RISK_RULES_LIBRARY).strip()
        previous_version = str(asset.published_version or "").strip()
        artifacts: dict[str, dict[str, Any]] = {}
        if previous_version:
            previous_document = config.get("rule_document")
            artifact = self._capture_document(
                asset,
                version=previous_version,
                library=library,
                document=previous_document,
                allow_disabled=False,
            )
            artifacts[previous_version] = artifact

        candidate_document = self._candidate_document(
            config,
            candidate_version,
            has_published_version=bool(previous_version),
        )
        if candidate_document is None:
            prior_artifacts = (
                previous_state.get("artifacts")
                if isinstance(previous_state, dict)
                and isinstance(previous_state.get("artifacts"), dict)
                else {}
            )
            prior_candidate = prior_artifacts.get(candidate_version)
            if isinstance(prior_candidate, dict):
                artifacts[candidate_version] = dict(prior_candidate)
            else:
                raise ValueError("候选版本缺少可执行规则快照，不能进入影子发布。")
        else:
            artifacts[candidate_version] = self._capture_document(
                asset,
                version=candidate_version,
                library=library,
                document=candidate_document,
                allow_disabled=True,
            )
        return ReleaseArtifactBundle(artifacts=artifacts, previous_config=config)

    def apply_candidate(
        self,
        asset: AgentAsset,
        state: dict[str, Any],
        *,
        actor: str,
    ) -> None:
        candidate = str(state.get("candidate_version") or "").strip()
        artifact = self.artifact(state, candidate)
        manifest = dict(artifact["manifest"])
        config = dict(asset.config_json or {})
        config.update(self._runtime_config(manifest, artifact["rule_document"]))
        revision = config.get("revision_draft")
        if isinstance(revision, dict):
            previous_config = state.get("previous_config")
            previous_document = (
                previous_config.get("rule_document")
                if isinstance(previous_config, dict)
                and isinstance(previous_config.get("rule_document"), dict)
                else {}
            )
            history = list(
                config.get("revision_history")
                if isinstance(config.get("revision_history"), list)
                else []
            )
            history.insert(
                0,
                {
                    "version": candidate,
                    "base_version": revision.get("base_version"),
                    "change_reason": revision.get("change_reason"),
                    "published_by": actor,
                    "published_at": datetime.now(UTC).isoformat(),
                    "previous_rule_document": previous_document,
                    "rule_document": dict(artifact["rule_document"]),
                },
            )
            config["revision_history"] = history[:20]
        config.pop("revision_draft", None)
        config["last_operation"] = {
            "action": "activate_staged_release",
            "actor": actor,
            "at": datetime.now(UTC).isoformat(),
            "target_version": candidate,
        }
        asset.name = str(manifest.get("name") or asset.name)
        asset.description = str(manifest.get("description") or asset.description)
        risk_category = str(manifest.get("risk_category") or "").strip()
        if risk_category:
            asset.scenario_json = [risk_category]
        asset.config_json = config

    @staticmethod
    def restore_previous(asset: AgentAsset, state: dict[str, Any]) -> None:
        previous_config = state.get("previous_config")
        if not isinstance(previous_config, dict):
            raise ValueError("发布状态缺少基线配置快照，不能安全回滚。")
        release_state = dict(state)
        restored = dict(previous_config)
        restored["release_guard"] = release_state
        asset.config_json = restored

    @staticmethod
    def artifact(state: dict[str, Any], version: str) -> dict[str, Any]:
        artifacts = state.get("artifacts")
        artifact = artifacts.get(version) if isinstance(artifacts, dict) else None
        if not isinstance(artifact, dict) or not isinstance(artifact.get("manifest"), dict):
            raise ValueError(f"发布版本 {version or '<empty>'} 缺少可信运行快照。")
        manifest = artifact["manifest"]
        expected = str(artifact.get("sha256") or "").strip()
        if not expected or expected != _manifest_hash(manifest):
            raise ValueError(f"发布版本 {version} 的运行快照完整性校验失败。")
        return dict(artifact)

    def _capture_document(
        self,
        asset: AgentAsset,
        *,
        version: str,
        library: str,
        document: Any,
        allow_disabled: bool,
    ) -> dict[str, Any]:
        if not isinstance(document, dict):
            raise ValueError(f"规则版本 {version} 缺少 rule_document。")
        file_name = str(document.get("file_name") or "").strip()
        if not file_name:
            raise ValueError(f"规则版本 {version} 缺少 JSON 文件名。")
        manifest = normalize_risk_rule_manifest(
            self.rule_library_manager.read_rule_library_json(
                library=library,
                file_name=file_name,
            )
        )
        rule_code = str(manifest.get("rule_code") or "").strip()
        if not rule_code or rule_code != str(asset.code or "").strip():
            raise ValueError("规则快照的 rule_code 与资产编码不一致。")
        if manifest.get("enabled") is False and not allow_disabled:
            raise PermissionError("当前基线规则已停用，不能作为分阶段发布的主版本。")
        self._assert_no_prohibited_automation(manifest)
        frozen = json.loads(json.dumps(manifest, ensure_ascii=False, sort_keys=True, default=str))
        return {
            "version": version,
            "rule_library": library,
            "rule_document": dict(document),
            "manifest": frozen,
            "sha256": _manifest_hash(frozen),
        }

    @staticmethod
    def _candidate_document(
        config: dict[str, Any],
        version: str,
        *,
        has_published_version: bool,
    ) -> dict[str, Any] | None:
        revision = config.get("revision_draft")
        if isinstance(revision, dict) and str(revision.get("version") or "").strip() == version:
            document = revision.get("rule_document")
            if isinstance(document, dict):
                return dict(document)
        document = config.get("rule_document")
        if isinstance(document, dict) and not has_published_version:
            return dict(document)
        return None

    def _assert_no_prohibited_automation(self, manifest: dict[str, Any]) -> None:
        outcomes = manifest.get("outcomes")
        if not isinstance(outcomes, dict):
            return
        actions = {
            str(outcome.get("action") or "").strip().lower()
            for outcome in outcomes.values()
            if isinstance(outcome, dict)
        }
        prohibited = sorted(actions & self._PROHIBITED_AUTOMATION_ACTIONS)
        if prohibited:
            raise PermissionError(
                "风险规则包含禁止无人值守执行的高风险动作：" + ", ".join(prohibited)
            )

    @staticmethod
    def _runtime_config(
        manifest: dict[str, Any],
        rule_document: dict[str, Any],
    ) -> dict[str, Any]:
        metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
        outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {}
        fail = outcomes.get("fail") if isinstance(outcomes.get("fail"), dict) else {}
        risk_level = str(metadata.get("risk_level") or fail.get("severity") or "medium")
        risk_score = metadata.get("risk_score") or fail.get("risk_score") or 0
        try:
            parsed_score = int(risk_score)
        except (TypeError, ValueError):
            parsed_score = 0
        return {
            "severity": risk_level,
            "risk_score": parsed_score,
            "risk_level": risk_level,
            "risk_level_label": metadata.get("risk_level_label"),
            "risk_score_detail": metadata.get("risk_score_detail") or {},
            "enabled": True,
            "detail_mode": "json_risk",
            "rule_document": dict(rule_document),
            "ontology_signal": manifest.get("ontology_signal"),
            "evaluator": manifest.get("evaluator"),
            "risk_category": manifest.get("risk_category"),
            "flow_diagram_svg": manifest.get("flow_diagram_svg"),
        }


def _manifest_hash(manifest: dict[str, Any]) -> str:
    canonical = json.dumps(
        manifest,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        default=str,
    )
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
