"""基于 AgentAsset 配置与测试记录的最小安全发布状态机。"""

from __future__ import annotations

import uuid
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.agent_enums import AgentAssetStatus, AgentAssetType, AgentReviewStatus
from app.models.agent_asset import (
    AgentAsset,
    AgentAssetReview,
    AgentAssetTestRun,
    AgentAssetVersion,
)
from app.services.agent_asset_release_artifacts import AgentAssetReleaseArtifactService
from app.services.agent_asset_release_policy import (
    ReleaseEvaluationInput,
    ReleaseGuardPolicy,
    ReleaseStage,
    evaluate_release,
    normalize_release_policy,
)
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.audit import AuditLogService


class AgentAssetReleaseGuardService:
    """管理 shadow→canary→active，并在质量越界时自动回滚。"""

    CONFIG_KEY = "release_guard"

    def __init__(
        self,
        db: Session,
        *,
        rule_library_manager: AgentAssetRuleLibraryManager | None = None,
    ) -> None:
        self.db = db
        self.artifacts = AgentAssetReleaseArtifactService(rule_library_manager)
        self.audit_service = AuditLogService(db)

    def start_shadow(
        self,
        asset_id: str,
        candidate_version: str,
        *,
        actor: str,
        policy: ReleaseGuardPolicy | None = None,
        tenant_id: str | None = None,
        allow_global_management: bool = False,
        request_id: str | None = None,
    ) -> dict[str, Any]:
        asset = self._asset(
            asset_id,
            tenant_id=tenant_id,
            allow_global_management=allow_global_management,
            lock=True,
        )
        version = str(candidate_version or "").strip()
        if not version or self._version(asset.id, version) is None:
            raise ValueError("候选版本不存在，不能进入影子发布。")
        if version == str(asset.published_version or "").strip():
            raise ValueError("候选版本已是当前发布版本，无需重复发布。")
        current = self._state(asset)
        if current.get("stage") in {"shadow", "canary"}:
            if current.get("candidate_version") == version:
                return current
            raise ValueError("当前已有进行中的发布，请先完成或回滚。")
        if current.get("stage") == "active" and current.get("candidate_version") == version:
            return current

        if asset.asset_type == AgentAssetType.RULE.value:
            self._require_rule_start_lifecycle(asset, version)
        bundle = self.artifacts.capture(asset, version, previous_state=current)
        if asset.asset_type == AgentAssetType.RULE.value:
            self._require_rule_release_preconditions(asset, version, bundle.artifacts, actor=actor)
            asset = self._asset(
                asset_id,
                tenant_id=tenant_id,
                allow_global_management=allow_global_management,
                lock=True,
            )
            latest_state = self._state(asset)
            if latest_state != current:
                if (
                    latest_state.get("stage") == "shadow"
                    and latest_state.get("candidate_version") == version
                ):
                    return latest_state
                raise ValueError("发布状态已被其他操作更新，请刷新后重试。")

        now = _now()
        release_id = uuid.uuid4().hex
        history = list(current.get("history") if isinstance(current.get("history"), list) else [])
        history.append(
            {
                "from": current.get("stage") or "none",
                "to": "shadow",
                "actor": actor,
                "at": now,
                "reason": "release_started",
            }
        )
        state = {
            "stage": "shadow",
            "release_id": release_id,
            "candidate_version": version,
            "previous_version": str(asset.published_version or ""),
            "policy": (policy or ReleaseGuardPolicy()).to_dict(),
            "artifacts": bundle.artifacts,
            "previous_config": bundle.previous_config,
            "started_at": now,
            "started_by": actor,
            "updated_at": now,
            "history": history[-20:],
        }
        self._save_state(asset, state, commit=False)
        self._audit(
            asset,
            actor=actor,
            action="start_agent_asset_shadow_release",
            after={
                "stage": "shadow",
                "release_id": release_id,
                "candidate_version": version,
            },
            request_id=request_id,
        )
        self.db.commit()
        return state

    def record_evaluation(
        self,
        asset_id: str,
        evaluation: ReleaseEvaluationInput,
        *,
        actor: str,
        tenant_id: str | None = None,
        allow_global_management: bool = False,
        request_id: str | None = None,
    ) -> dict[str, Any]:
        asset = self._asset(
            asset_id,
            tenant_id=tenant_id,
            allow_global_management=allow_global_management,
            lock=True,
        )
        state = self._state(asset)
        stage = str(state.get("stage") or "")
        if asset.status == AgentAssetStatus.DISABLED.value:
            raise ValueError("当前资产已停用，不能继续写入发布评测。")
        if stage not in {"shadow", "canary", "active"}:
            raise ValueError("当前资产不在可评测的发布阶段。")
        candidate_version = str(state.get("candidate_version") or "")
        release_id = str(state.get("release_id") or "")
        policy = self._normalized_policy(state.get("policy"))
        result = self._evaluate(stage, evaluation, policy)
        requested_tenant = str(tenant_id or "").strip()
        run = AgentAssetTestRun(
            id=str(uuid.uuid4()),
            tenant_id=requested_tenant or asset.tenant_id,
            scope="tenant" if requested_tenant else asset.scope,
            asset_id=asset.id,
            version=candidate_version,
            test_type=f"release_{stage}",
            status=result["status"],
            passed=result["status"] == "passed",
            summary=result["summary"],
            input_json={
                "total": evaluation.total,
                "failure_count": evaluation.failure_count,
                "precision": evaluation.precision,
                "baseline_precision": evaluation.baseline_precision,
                "release_id": release_id,
            },
            result_json={**result, "details": dict(evaluation.details or {})},
            created_by=actor,
            created_at=datetime.now(UTC),
        )
        self.db.add(run)
        if result["status"] == "failed":
            self._apply_rollback(
                asset,
                state,
                actor=actor,
                reason=";".join(result["reasons"]),
                automatic=True,
            )
        self._audit(
            asset,
            actor=actor,
            action="evaluate_agent_asset_release",
            after={
                "stage": stage,
                "status": result["status"],
                "reasons": result["reasons"],
                "automatic_rollback": result["status"] == "failed",
            },
            request_id=request_id,
        )
        self.db.commit()
        self.db.refresh(run)
        return {
            "test_run_id": run.id,
            "stage": stage,
            **result,
            "release_stage": self._state(asset).get("stage"),
        }

    def promote(
        self,
        asset_id: str,
        *,
        actor: str,
        tenant_id: str | None = None,
        allow_global_management: bool = False,
        request_id: str | None = None,
    ) -> dict[str, Any]:
        asset = self._asset(
            asset_id,
            tenant_id=tenant_id,
            allow_global_management=allow_global_management,
            lock=True,
        )
        state = self._state(asset)
        stage = str(state.get("stage") or "")
        if stage not in {"shadow", "canary"}:
            raise ValueError("只有 shadow 或 canary 阶段可以晋级。")
        candidate_version = str(state.get("candidate_version") or "")
        if asset.asset_type == AgentAssetType.RULE.value:
            self._require_release_state_integrity(asset, state)
        latest = self._latest_stage_run(
            asset.id,
            candidate_version,
            stage,
            release_id=str(state.get("release_id") or ""),
        )
        if latest is None or latest.status != "passed" or not latest.passed:
            raise PermissionError(f"{stage} 阶段尚无通过的质量评测，不能晋级。")

        target = "canary" if stage == "shadow" else "active"
        if asset.asset_type == AgentAssetType.RULE.value:
            self._require_approved_rule_version(asset, candidate_version)
        if target == "active":
            if asset.asset_type == AgentAssetType.RULE.value:
                self.artifacts.apply_candidate(asset, state, actor=actor)
            asset.published_version = candidate_version
            asset.current_version = candidate_version
            asset.working_version = candidate_version
            asset.status = AgentAssetStatus.ACTIVE.value
        self._transition(state, stage, target, actor=actor, reason="quality_gate_passed")
        self._save_state(asset, state, commit=False)
        self._audit(
            asset,
            actor=actor,
            action="promote_agent_asset_release",
            after={"from": stage, "to": target, "candidate_version": candidate_version},
            request_id=request_id,
        )
        self.db.commit()
        return state

    def rollback(
        self,
        asset_id: str,
        *,
        actor: str,
        reason: str,
        tenant_id: str | None = None,
        allow_global_management: bool = False,
        request_id: str | None = None,
    ) -> dict[str, Any]:
        asset = self._asset(
            asset_id,
            tenant_id=tenant_id,
            allow_global_management=allow_global_management,
            lock=True,
        )
        state = self._state(asset)
        if state.get("stage") not in {"shadow", "canary", "active"}:
            raise ValueError("当前发布状态不能回滚。")
        self._apply_rollback(asset, state, actor=actor, reason=reason, automatic=False)
        self._audit(
            asset,
            actor=actor,
            action="rollback_agent_asset_release",
            after={"stage": "rolled_back", "reason": reason, "automatic": False},
            request_id=request_id,
        )
        self.db.commit()
        return state

    def get_state(
        self,
        asset_id: str,
        *,
        tenant_id: str | None = None,
        allow_global_management: bool = False,
    ) -> dict[str, Any]:
        return self._state(
            self._asset(
                asset_id,
                tenant_id=tenant_id,
                allow_global_management=allow_global_management,
            )
        )

    def get_serving_plan(
        self,
        asset_id: str,
        *,
        tenant_id: str | None = None,
        allow_global_management: bool = False,
    ) -> dict[str, Any]:
        """返回运行时可直接消费的候选/主版本路由计划。"""

        asset = self._asset(
            asset_id,
            tenant_id=tenant_id,
            allow_global_management=allow_global_management,
        )
        state = self._state(asset)
        stage = str(state.get("stage") or "")
        candidate = str(state.get("candidate_version") or "")
        previous = str(state.get("previous_version") or asset.published_version or "")
        policy = self._normalized_policy(state.get("policy"))
        if stage == "shadow":
            return {
                "stage": stage,
                "primary_version": previous,
                "candidate_version": candidate,
                "candidate_traffic_percent": 0,
                "shadow_evaluation": True,
            }
        if stage == "canary":
            return {
                "stage": stage,
                "primary_version": previous,
                "candidate_version": candidate,
                "candidate_traffic_percent": policy["canary_traffic_percent"],
                "shadow_evaluation": False,
            }
        if stage == "active":
            return {
                "stage": stage,
                "primary_version": candidate,
                "candidate_version": candidate,
                "candidate_traffic_percent": 100,
                "shadow_evaluation": False,
            }
        return {
            "stage": stage or "unmanaged",
            "primary_version": previous or str(asset.published_version or ""),
            "candidate_version": candidate,
            "candidate_traffic_percent": 0,
            "shadow_evaluation": False,
        }

    def _evaluate(
        self,
        stage: str,
        evaluation: ReleaseEvaluationInput,
        policy: dict[str, Any],
    ) -> dict[str, Any]:
        return evaluate_release(stage, evaluation, policy)

    def _apply_rollback(
        self,
        asset: AgentAsset,
        state: dict[str, Any],
        *,
        actor: str,
        reason: str,
        automatic: bool,
    ) -> None:
        previous = str(state.get("previous_version") or "")
        candidate = str(state.get("candidate_version") or "")
        asset.published_version = previous or None
        if previous:
            asset.current_version = previous
            asset.working_version = previous
            asset.status = AgentAssetStatus.ACTIVE.value
        else:
            asset.current_version = candidate or asset.current_version
            asset.working_version = candidate or asset.working_version
            asset.status = AgentAssetStatus.REVIEW.value
        state["rollback"] = {
            "automatic": automatic,
            "reason": str(reason or "release_guard_triggered"),
            "actor": actor,
            "at": _now(),
            "restored_version": previous,
        }
        self._transition(
            state,
            str(state.get("stage") or "unknown"),
            "rolled_back",
            actor=actor,
            reason=str(reason or "release_guard_triggered"),
        )
        if asset.asset_type == AgentAssetType.RULE.value:
            self.artifacts.restore_previous(asset, state)
        self._save_state(asset, state, commit=False)

    @staticmethod
    def _transition(
        state: dict[str, Any],
        source: str,
        target: ReleaseStage,
        *,
        actor: str,
        reason: str,
    ) -> None:
        now = _now()
        history = list(state.get("history") if isinstance(state.get("history"), list) else [])
        history.append({"from": source, "to": target, "actor": actor, "at": now, "reason": reason})
        state["history"] = history[-20:]
        state["stage"] = target
        state["updated_at"] = now

    def _save_state(
        self,
        asset: AgentAsset,
        state: dict[str, Any],
        *,
        commit: bool = True,
    ) -> None:
        config = dict(asset.config_json or {})
        config[self.CONFIG_KEY] = state
        asset.config_json = config
        self.db.add(asset)
        if commit:
            self.db.commit()

    @staticmethod
    def _state(asset: AgentAsset) -> dict[str, Any]:
        config = asset.config_json if isinstance(asset.config_json, dict) else {}
        value = config.get(AgentAssetReleaseGuardService.CONFIG_KEY)
        return dict(value) if isinstance(value, dict) else {}

    def _asset(
        self,
        asset_id: str,
        *,
        tenant_id: str | None = None,
        allow_global_management: bool = False,
        lock: bool = False,
    ) -> AgentAsset:
        requested_tenant = str(tenant_id or "").strip()
        stmt = select(AgentAsset).where(AgentAsset.id == asset_id)
        if requested_tenant:
            stmt = stmt.where(
                (
                    (AgentAsset.scope == "tenant")
                    & (AgentAsset.tenant_id == requested_tenant)
                )
                | (
                    (AgentAsset.scope == "platform")
                    & (AgentAsset.tenant_id == "platform")
                )
            )
        else:
            stmt = stmt.where(
                AgentAsset.scope == "platform",
                AgentAsset.tenant_id == "platform",
            )
        if lock:
            stmt = stmt.with_for_update()
        asset = self.db.scalar(stmt)
        if asset is None:
            raise LookupError("Agent asset not found")
        configured_tenant = str((asset.config_json or {}).get("tenant_id") or "").strip()
        if requested_tenant:
            if asset.scope == "tenant" and configured_tenant not in {
                "",
                requested_tenant,
            }:
                raise LookupError("Agent asset not found")
            if asset.scope == "platform" and not allow_global_management:
                # 未绑定租户的规则是平台共享资产，租户管理员不得修改其发布状态。
                raise LookupError("Agent asset not found")
        return asset

    def _version(self, asset_id: str, version: str) -> AgentAssetVersion | None:
        return self.db.scalar(
            select(AgentAssetVersion).where(
                AgentAssetVersion.asset_id == asset_id,
                AgentAssetVersion.version == version,
            )
        )

    def _latest_stage_run(
        self,
        asset_id: str,
        version: str,
        stage: str,
        *,
        release_id: str,
    ) -> AgentAssetTestRun | None:
        runs = list(
            self.db.scalars(
                select(AgentAssetTestRun)
                .where(
                    AgentAssetTestRun.asset_id == asset_id,
                    AgentAssetTestRun.version == version,
                    AgentAssetTestRun.test_type == f"release_{stage}",
                )
                .order_by(AgentAssetTestRun.created_at.desc(), AgentAssetTestRun.id.desc())
                .limit(50)
            ).all()
        )
        return next(
            (
                run
                for run in runs
                if str((run.input_json or {}).get("release_id") or "") == release_id
            ),
            None,
        )

    def _require_approved_rule_version(self, asset: AgentAsset, version: str) -> None:
        if asset.asset_type != AgentAssetType.RULE.value:
            return
        review = self.db.scalar(
            select(AgentAssetReview)
            .where(
                AgentAssetReview.asset_id == asset.id,
                AgentAssetReview.version == version,
            )
            .order_by(AgentAssetReview.created_at.desc(), AgentAssetReview.id.desc())
        )
        if review is None or review.review_status != AgentReviewStatus.APPROVED.value:
            raise PermissionError("风险规则候选版本未经审核批准，不能进入 Canary 或 active。")

    def _require_release_state_integrity(
        self,
        asset: AgentAsset,
        state: dict[str, Any],
    ) -> None:
        candidate = str(state.get("candidate_version") or "").strip()
        previous = str(state.get("previous_version") or "").strip()
        AgentAssetReleaseArtifactService.artifact(state, candidate)
        if previous:
            AgentAssetReleaseArtifactService.artifact(state, previous)
            if str(asset.published_version or "").strip() != previous:
                raise PermissionError("发布期间基线版本发生变化，已停止晋级。")
            if asset.status != AgentAssetStatus.ACTIVE.value:
                raise PermissionError("发布期间基线规则已不再生效，已停止晋级。")
        else:
            if str(asset.published_version or "").strip():
                raise PermissionError("发布期间资产发布版本发生变化，已停止晋级。")
            if asset.status != AgentAssetStatus.REVIEW.value:
                raise PermissionError("候选规则已不在待审核状态，已停止晋级。")
        config = asset.config_json if isinstance(asset.config_json, dict) else {}
        if config.get("enabled") is False:
            raise PermissionError("发布期间风险规则已被停用，已停止晋级。")

    def _require_rule_release_preconditions(
        self,
        asset: AgentAsset,
        version: str,
        artifacts: dict[str, dict[str, Any]],
        *,
        actor: str,
    ) -> None:
        report = self.db.scalar(
            select(AgentAssetTestRun)
            .where(
                AgentAssetTestRun.asset_id == asset.id,
                AgentAssetTestRun.version == version,
                AgentAssetTestRun.test_type == "report",
            )
            .order_by(AgentAssetTestRun.created_at.desc(), AgentAssetTestRun.id.desc())
        )
        if report is None or not report.passed or report.status != "passed":
            raise PermissionError("候选风险规则尚无通过的测试报告，不能进入影子发布。")
        artifact = artifacts.get(version)
        manifest = artifact.get("manifest") if isinstance(artifact, dict) else None
        if not isinstance(manifest, dict):
            raise PermissionError("候选风险规则缺少可信运行快照。")
        rule_code = str(manifest.get("rule_code") or "").strip()
        from app.services.risk_rule_golden_evaluator import RiskRuleGoldenEvaluator

        RiskRuleGoldenEvaluator().require_pass(
            self.db,
            asset,
            version,
            manifest,
            rule_code,
            actor=actor,
        )
        review = self.db.scalar(
            select(AgentAssetReview)
            .where(
                AgentAssetReview.asset_id == asset.id,
                AgentAssetReview.version == version,
            )
            .order_by(AgentAssetReview.created_at.desc(), AgentAssetReview.id.desc())
        )
        if review is None or review.review_status != AgentReviewStatus.APPROVED.value:
            self.db.add(
                AgentAssetReview(
                    asset_id=asset.id,
                    version=version,
                    reviewer=actor,
                    review_status=AgentReviewStatus.APPROVED.value,
                    review_note="批准候选版本进入受控分阶段发布。",
                    reviewed_at=datetime.now(UTC),
                    created_at=datetime.now(UTC),
                )
            )
        asset.reviewer = actor

    @staticmethod
    def _require_rule_start_lifecycle(asset: AgentAsset, version: str) -> None:
        working = str(asset.working_version or asset.current_version or "").strip()
        published = str(asset.published_version or "").strip()
        if version != working:
            raise PermissionError("只能发布当前工作版本，历史版本不能直接进入分阶段发布。")
        if published:
            if asset.status != AgentAssetStatus.ACTIVE.value:
                raise PermissionError("只有当前基线仍在线的规则才能发布修订候选。")
            if version == published:
                raise ValueError("候选版本已是当前发布版本，无需重复发布。")
            return
        if asset.status != AgentAssetStatus.REVIEW.value:
            raise PermissionError("首次发布前必须先完成测试并提交审核。")

    def _audit(
        self,
        asset: AgentAsset,
        *,
        actor: str,
        action: str,
        after: dict[str, Any],
        request_id: str | None,
    ) -> None:
        self.audit_service.log_action(
            actor=actor,
            action=action,
            resource_type=asset.asset_type,
            resource_id=asset.id,
            after_json=after,
            request_id=request_id,
            commit=False,
        )

    @staticmethod
    def _normalized_policy(value: Any) -> dict[str, Any]:
        return normalize_release_policy(value)


def _now() -> str:
    return datetime.now(UTC).isoformat()
