"""用数据库真实发布遥测驱动 Release Guard 的协调器。"""

from __future__ import annotations

import hashlib
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, AgentAssetTestRun
from app.services.agent_asset_release_alerts import build_release_alerts
from app.services.agent_asset_release_guard import AgentAssetReleaseGuardService
from app.services.agent_asset_release_telemetry import (
    AgentAssetReleaseTelemetryService,
    ReleaseTelemetryAggregate,
    ReleaseTelemetryStaleRelease,
)

_MONITORED_STAGES = {"shadow", "canary", "active"}
_MAX_BATCH_SIZE = 500


class AgentAssetReleaseMonitor:
    """只接受 release 身份，绝不接受调用方提供的质量汇总数字。"""

    def __init__(
        self,
        db: Session,
        *,
        telemetry_service: AgentAssetReleaseTelemetryService | None = None,
        guard_service: AgentAssetReleaseGuardService | None = None,
    ) -> None:
        self.db = db
        self.telemetry = telemetry_service or AgentAssetReleaseTelemetryService(db)
        self.guard = guard_service or AgentAssetReleaseGuardService(db)

    def evaluate_current(
        self,
        *,
        tenant_id: str,
        asset_id: str,
        actor: str,
        allow_global_management: bool = False,
    ) -> dict[str, Any]:
        """聚合当前 release；只有真实标签完备后才调用 Release Guard。"""

        tenant = _required(tenant_id, "tenant_id", 64)
        target_asset = _required(asset_id, "asset_id", 36)
        normalized_actor = _required(actor, "actor", 100)
        _require_bool(allow_global_management, "allow_global_management")
        asset, state = self._load_target(
            tenant_id=tenant,
            asset_id=target_asset,
            allow_global_management=allow_global_management,
            lock=False,
        )
        identity = self._release_identity(asset, state)
        aggregate = self.telemetry.aggregate(
            tenant_id=tenant,
            asset_id=asset.id,
            release_id=identity["release_id"],
            stage=identity["stage"],
            version=identity["version"],
        )
        if aggregate.status == "collecting":
            return self._collecting_result(aggregate)

        # 在把指标交给 Guard 前锁定资产并重验 release 身份，避免把旧阶段
        # 的真实指标写入刚刚晋级或重启的新 release。
        locked_asset, locked_state = self._load_target(
            tenant_id=tenant,
            asset_id=target_asset,
            allow_global_management=allow_global_management,
            lock=True,
        )
        if self._release_identity(locked_asset, locked_state) != identity:
            raise ReleaseTelemetryStaleRelease(
                "Release changed after telemetry aggregation; evaluation was not submitted."
            )

        evaluation = aggregate.to_release_evaluation_input()
        existing_run = self._existing_evaluation(
            aggregate=aggregate,
            evaluation_input={
                "total": evaluation.total,
                "failure_count": evaluation.failure_count,
                "precision": evaluation.precision,
                "baseline_precision": evaluation.baseline_precision,
                "release_id": identity["release_id"],
            },
        )
        if existing_run is not None:
            result = existing_run.result_json if isinstance(existing_run.result_json, dict) else {}
            release_stage = str(locked_state.get("stage") or identity["stage"])
            payload = {
                "asset_id": locked_asset.id,
                "release_id": identity["release_id"],
                "stage": identity["stage"],
                "version": identity["version"],
                "telemetry_status": "ready",
                "status": str(existing_run.status or "unknown"),
                "evaluation_submitted": True,
                "release_stage": release_stage,
                "rolled_back": release_stage == "rolled_back",
                "reasons": list(result.get("reasons") or []),
                "test_run_id": existing_run.id,
                "metrics": self._metrics(aggregate),
            }
            payload["alerts"] = build_release_alerts(
                status=payload["status"],
                rolled_back=payload["rolled_back"],
                reasons=payload["reasons"],
                metrics=payload["metrics"],
            )
            return payload
        guard_result = self.guard.record_evaluation(
            locked_asset.id,
            evaluation,
            actor=normalized_actor,
            tenant_id=tenant,
            allow_global_management=allow_global_management,
            request_id=self._request_id(aggregate),
        )
        release_stage = str(guard_result.get("release_stage") or identity["stage"])
        payload = {
            "asset_id": locked_asset.id,
            "release_id": identity["release_id"],
            "stage": identity["stage"],
            "version": identity["version"],
            "telemetry_status": "ready",
            "status": str(guard_result.get("status") or "unknown"),
            "evaluation_submitted": True,
            "release_stage": release_stage,
            "rolled_back": release_stage == "rolled_back",
            "reasons": list(guard_result.get("reasons") or []),
            "test_run_id": guard_result.get("test_run_id"),
            "metrics": self._metrics(aggregate),
        }
        payload["alerts"] = build_release_alerts(
            status=payload["status"],
            rolled_back=payload["rolled_back"],
            reasons=payload["reasons"],
            metrics=payload["metrics"],
        )
        return payload

    def batch_evaluate(
        self,
        *,
        tenant_id: str,
        actor: str,
        allow_global_management: bool = False,
        limit: int = 100,
        after_asset_id: str | None = None,
    ) -> dict[str, Any]:
        """有界扫描当前受控风险规则，并将每个资产的失败隔离。"""

        tenant = _required(tenant_id, "tenant_id", 64)
        normalized_actor = _required(actor, "actor", 100)
        _require_bool(allow_global_management, "allow_global_management")
        normalized_limit = _batch_limit(limit)
        normalized_cursor = str(after_asset_id or "").strip() or None
        assets = self._batch_targets(
            tenant_id=tenant,
            limit=normalized_limit,
            after_asset_id=normalized_cursor,
        )
        results: list[dict[str, Any]] = []
        errors: list[dict[str, Any]] = []
        evaluated = 0
        collecting = 0
        rolled_back = 0
        for asset in assets:
            try:
                result = self.evaluate_current(
                    tenant_id=tenant,
                    asset_id=asset.id,
                    actor=normalized_actor,
                    allow_global_management=allow_global_management,
                )
            except Exception as error:
                # Guard 在成功路径自行提交；失败路径必须回滚当前 Session，
                # 防止一个资产留下的失败事务污染后续资产。
                self.db.rollback()
                errors.append(
                    {
                        "asset_id": asset.id,
                        "error_type": type(error).__name__,
                        "message": _safe_error(error),
                        "alerts": build_release_alerts(
                            status="collecting",
                            rolled_back=False,
                            reasons=("telemetry_aggregation_failed",),
                            metrics={"aggregation_failed": True},
                        ),
                    }
                )
                continue
            results.append(result)
            if result["evaluation_submitted"]:
                evaluated += 1
            if result["status"] == "collecting":
                collecting += 1
            if result["rolled_back"]:
                rolled_back += 1
        return {
            "tenant_id": tenant,
            "limit": normalized_limit,
            "scanned": len(assets),
            "evaluated": evaluated,
            "collecting": collecting,
            "rolled_back": rolled_back,
            "next_cursor": assets[-1].id if assets else normalized_cursor,
            "errors": errors,
            "results": results,
        }

    def _existing_evaluation(
        self,
        *,
        aggregate: ReleaseTelemetryAggregate,
        evaluation_input: dict[str, Any],
    ) -> AgentAssetTestRun | None:
        """相同 release 与真实聚合快照只产生一次测试运行。"""

        runs = self.db.scalars(
            select(AgentAssetTestRun)
            .where(
                AgentAssetTestRun.tenant_id == aggregate.tenant_id,
                AgentAssetTestRun.scope == "tenant",
                AgentAssetTestRun.asset_id == aggregate.asset_id,
                AgentAssetTestRun.version == aggregate.version,
                AgentAssetTestRun.test_type == f"release_{aggregate.stage}",
            )
            .order_by(AgentAssetTestRun.created_at.desc(), AgentAssetTestRun.id.desc())
        )
        for run in runs:
            if run.input_json == evaluation_input:
                return run
        return None

    def _load_target(
        self,
        *,
        tenant_id: str,
        asset_id: str,
        allow_global_management: bool,
        lock: bool,
    ) -> tuple[AgentAsset, dict[str, Any]]:
        statement = select(AgentAsset).where(
            AgentAsset.id == asset_id,
            (
                ((AgentAsset.scope == "tenant") & (AgentAsset.tenant_id == tenant_id))
                | ((AgentAsset.scope == "platform") & (AgentAsset.tenant_id == "platform"))
            ),
        )
        if lock:
            statement = statement.with_for_update().execution_options(populate_existing=True)
        asset = self.db.scalar(statement)
        if asset is None:
            raise LookupError("Agent asset not found.")
        config = asset.config_json if isinstance(asset.config_json, dict) else {}
        configured_tenant = str(config.get("tenant_id") or "").strip()
        if asset.scope == "tenant":
            if configured_tenant not in {"", tenant_id}:
                raise LookupError("Agent asset not found.")
        else:
            # 全局资产会命中多个租户；用某一个当前用户的 tenant_id 聚合会把
            # 局部样本误当成全局事实。跨租户聚合协议完成前，即使平台管理员
            # 也只能显式回滚，不能触发自动评测。
            if allow_global_management:
                raise ValueError(
                    "Global release telemetry requires a cross-tenant aggregate and "
                    "cannot be evaluated by the tenant monitor."
                )
            raise LookupError("Agent asset not found.")
        if (
            asset.asset_type != AgentAssetType.RULE.value
            or asset.domain != AgentAssetDomain.EXPENSE.value
            or str(config.get("detail_mode") or "").strip().lower() != "json_risk"
        ):
            raise ValueError("Asset is not an expense JSON risk rule.")
        if asset.status == AgentAssetStatus.DISABLED.value or config.get("enabled") is False:
            raise ValueError("Disabled risk rules cannot be evaluated by the release monitor.")
        state = config.get("release_guard")
        if not isinstance(state, dict):
            raise ValueError("Asset has no active release guard state.")
        return asset, dict(state)

    @staticmethod
    def _release_identity(asset: AgentAsset, state: dict[str, Any]) -> dict[str, str]:
        stage = str(state.get("stage") or "").strip().lower()
        if stage not in _MONITORED_STAGES:
            raise ValueError("Asset is not in a monitored release stage.")
        return {
            "asset_id": asset.id,
            "release_id": _required(state.get("release_id"), "release_id", 64),
            "stage": stage,
            "version": _required(state.get("candidate_version"), "candidate_version", 30),
        }

    def _batch_targets(
        self,
        *,
        tenant_id: str,
        limit: int,
        after_asset_id: str | None,
    ) -> list[AgentAsset]:
        config = AgentAsset.config_json
        tenant_value = config["tenant_id"].as_string()
        base = (
            select(AgentAsset)
            .where(
                AgentAsset.asset_type == AgentAssetType.RULE.value,
                AgentAsset.domain == AgentAssetDomain.EXPENSE.value,
                AgentAsset.scope == "tenant",
                AgentAsset.tenant_id == tenant_id,
                config["detail_mode"].as_string() == "json_risk",
                config["release_guard"]["stage"].as_string().in_(sorted(_MONITORED_STAGES)),
                tenant_value == tenant_id,
            )
            .order_by(AgentAsset.id.asc())
        )
        if not after_asset_id:
            return list(self.db.scalars(base.limit(limit)).all())

        # 游标扫描到尾部后从头补足本轮，避免固定 limit 永远只评测 ID 最小的资产。
        after = list(
            self.db.scalars(
                base.where(AgentAsset.id > after_asset_id).limit(limit)
            ).all()
        )
        remaining = limit - len(after)
        if remaining <= 0:
            return after
        wrapped = list(
            self.db.scalars(
                base.where(AgentAsset.id <= after_asset_id).limit(remaining)
            ).all()
        )
        return [*after, *wrapped]

    @staticmethod
    def _collecting_result(aggregate: ReleaseTelemetryAggregate) -> dict[str, Any]:
        payload = {
            "asset_id": aggregate.asset_id,
            "release_id": aggregate.release_id,
            "stage": aggregate.stage,
            "version": aggregate.version,
            "telemetry_status": "collecting",
            "status": "collecting",
            "evaluation_submitted": False,
            "release_stage": aggregate.stage,
            "rolled_back": False,
            "reasons": list(aggregate.reasons),
            "test_run_id": None,
            "metrics": AgentAssetReleaseMonitor._metrics(aggregate),
        }
        payload["alerts"] = build_release_alerts(
            status=payload["status"],
            rolled_back=False,
            reasons=payload["reasons"],
            metrics=payload["metrics"],
        )
        return payload

    @staticmethod
    def _metrics(aggregate: ReleaseTelemetryAggregate) -> dict[str, Any]:
        observed_count = aggregate.observed_count
        return {
            "observed_count": observed_count,
            "completed_count": aggregate.completed_count,
            "runtime_failure_count": aggregate.runtime_failure_count,
            "runtime_failure_rate": (
                aggregate.runtime_failure_count / observed_count if observed_count else None
            ),
            "candidate_hit_count": aggregate.candidate_hit_count,
            "candidate_labeled_count": aggregate.candidate_labeled_count,
            "candidate_pending_label_count": aggregate.candidate_pending_label_count,
            "precision": aggregate.precision,
            "baseline_hit_count": aggregate.baseline_hit_count,
            "baseline_labeled_count": aggregate.baseline_labeled_count,
            "baseline_pending_label_count": aggregate.baseline_pending_label_count,
            "baseline_precision": aggregate.baseline_precision,
            "false_negative_count": aggregate.false_negative_count,
            "estimated_false_negative_count": aggregate.estimated_false_negative_count,
            "false_negative_upper_bound": aggregate.false_negative_upper_bound,
            "negative_sample_count": aggregate.negative_sample_count,
            "negative_labeled_count": aggregate.negative_labeled_count,
            "negative_pending_label_count": aggregate.negative_pending_label_count,
            "random_negative_population_count": aggregate.random_negative_population_count,
            "random_negative_sample_count": aggregate.random_negative_sample_count,
            "random_negative_labeled_count": aggregate.random_negative_labeled_count,
            "recall": aggregate.recall,
            "recall_lower_bound": aggregate.recall_lower_bound,
            "recall_confidence_level": aggregate.recall_confidence_level,
            "recall_method": aggregate.recall_method,
            "negative_ground_truth_status": aggregate.negative_ground_truth_status,
        }

    @staticmethod
    def _request_id(aggregate: ReleaseTelemetryAggregate) -> str:
        source = "\x1f".join(
            str(value)
            for value in (
                aggregate.tenant_id,
                aggregate.asset_id,
                aggregate.release_id,
                aggregate.stage,
                aggregate.version,
                aggregate.observed_count,
                aggregate.runtime_failure_count,
                aggregate.candidate_labeled_count,
                aggregate.candidate_confirmed_count,
                aggregate.candidate_false_positive_count,
                aggregate.baseline_labeled_count,
                aggregate.baseline_confirmed_count,
                aggregate.baseline_false_positive_count,
                aggregate.negative_sample_count,
                aggregate.negative_labeled_count,
                aggregate.false_negative_count,
                aggregate.recall,
                aggregate.recall_lower_bound,
            )
        )
        return f"release-monitor:{hashlib.sha256(source.encode('utf-8')).hexdigest()[:32]}"


def _required(value: Any, field: str, maximum: int) -> str:
    normalized = str(value or "").strip()
    if not normalized or len(normalized) > maximum:
        raise ValueError(f"{field} is required and must be at most {maximum} characters.")
    return normalized


def _require_bool(value: Any, field: str) -> None:
    if not isinstance(value, bool):
        raise ValueError(f"{field} must be a boolean.")


def _batch_limit(value: Any) -> int:
    if isinstance(value, bool):
        raise ValueError("limit must be an integer.")
    try:
        parsed = int(value)
    except (TypeError, ValueError, OverflowError) as error:
        raise ValueError("limit must be an integer.") from error
    if parsed < 1 or parsed > _MAX_BATCH_SIZE:
        raise ValueError(f"limit must be between 1 and {_MAX_BATCH_SIZE}.")
    return parsed


def _safe_error(error: Exception) -> str:
    message = " ".join(str(error).split())
    return message[:240] or type(error).__name__
