"""Agent 分阶段发布的人工复核队列。"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.agent_asset import AgentAsset
from app.models.agent_asset_release_telemetry import (
    AgentAssetReleaseAuditSample,
    AgentAssetReleaseLabel,
    AgentAssetReleaseObservation,
)
from app.services.agent_asset_release_alerts import build_release_alerts
from app.services.agent_asset_release_label_votes import (
    release_reviewer_quorum,
    resolve_release_label_votes,
)
from app.services.agent_asset_release_sampling import AgentAssetReleaseSamplingService
from app.services.agent_asset_release_telemetry import AgentAssetReleaseTelemetryService

_REVIEWABLE_STAGES = {"shadow", "canary", "active"}


class AgentAssetReleaseReviewService:
    """混排正负样本且隐藏模型结论，禁止发布发起人自审。"""

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

    def list_pending(
        self,
        *,
        tenant_id: str,
        asset_id: str,
        limit: int = 50,
    ) -> dict[str, Any]:
        tenant, asset, state = self._current_release(tenant_id, asset_id)
        normalized_limit = _limit(limit)
        samples = list(
            self.db.scalars(
                select(AgentAssetReleaseAuditSample)
                .where(
                    AgentAssetReleaseAuditSample.tenant_id == tenant,
                    AgentAssetReleaseAuditSample.asset_id == asset.id,
                    AgentAssetReleaseAuditSample.release_id == state["release_id"],
                    AgentAssetReleaseAuditSample.stage == state["stage"],
                    AgentAssetReleaseAuditSample.version == state["candidate_version"],
                )
                .order_by(
                    AgentAssetReleaseAuditSample.selection_score_ppm.asc(),
                    AgentAssetReleaseAuditSample.created_at.asc(),
                    AgentAssetReleaseAuditSample.id.asc(),
                )
            ).all()
        )
        observation_ids = [item.observation_id for item in samples]
        observations = {
            item.id: item
            for item in self.db.scalars(
                select(AgentAssetReleaseObservation).where(
                    AgentAssetReleaseObservation.tenant_id == tenant,
                    AgentAssetReleaseObservation.id.in_(observation_ids),
                )
            ).all()
        } if observation_ids else {}
        labels: list[AgentAssetReleaseLabel] = []
        if observation_ids:
            labels = list(
                self.db.scalars(
                    select(AgentAssetReleaseLabel).where(
                        AgentAssetReleaseLabel.tenant_id == tenant,
                        AgentAssetReleaseLabel.observation_id.in_(observation_ids),
                    )
                    .order_by(
                        AgentAssetReleaseLabel.created_at.asc(),
                        AgentAssetReleaseLabel.id.asc(),
                    )
                ).all()
            )
        quorum = release_reviewer_quorum(state)
        vote_states = resolve_release_label_votes(
            labels,
            required_reviewers=quorum,
            required_reviewers_by_observation={
                item.observation_id: _sample_quorum(item, quorum) for item in samples
            },
        )
        pending = [
            item
            for item in samples
            if vote_states.get(item.observation_id) is None
            or vote_states[item.observation_id].label is None
        ]
        items = [
            self._item(
                item,
                observations[item.observation_id],
                vote_states.get(item.observation_id),
                _sample_quorum(item, quorum),
                tenant,
            )
            for item in pending[:normalized_limit]
            if item.observation_id in observations
        ]
        aggregate = AgentAssetReleaseTelemetryService(self.db).aggregate(
            tenant_id=tenant,
            asset_id=asset.id,
            release_id=state["release_id"],
            stage=state["stage"],
            version=state["candidate_version"],
        )
        pending_observations = [
            observations[item.observation_id]
            for item in pending
            if item.observation_id in observations
        ]
        metrics = _metrics(aggregate, pending_observations)
        alerts = build_release_alerts(
            status=aggregate.status,
            rolled_back=False,
            reasons=aggregate.reasons,
            metrics=metrics,
        )
        return {
            "asset_id": asset.id,
            "release_id": state["release_id"],
            "stage": state["stage"],
            "version": state["candidate_version"],
            "pending_total": len(pending),
            "telemetry_status": aggregate.status,
            "reasons": list(aggregate.reasons),
            "metrics": metrics,
            "alerts": alerts,
            "items": items,
        }

    def record_label(
        self,
        *,
        tenant_id: str,
        asset_id: str,
        observation_id: str,
        label: str,
        actor_id: str,
        request_id: str,
    ) -> AgentAssetReleaseLabel:
        tenant, asset, state = self._current_release(tenant_id, asset_id)
        actor = _required(actor_id, "actor_id", 160)
        if actor == str(state.get("started_by") or "").strip():
            raise PermissionError("发布发起人不能复核自己的候选版本。")
        sample = self.db.scalar(
            select(AgentAssetReleaseAuditSample).where(
                AgentAssetReleaseAuditSample.tenant_id == tenant,
                AgentAssetReleaseAuditSample.observation_id == _required(
                    observation_id,
                    "observation_id",
                    36,
                ),
                AgentAssetReleaseAuditSample.asset_id == asset.id,
                AgentAssetReleaseAuditSample.release_id == state["release_id"],
                AgentAssetReleaseAuditSample.stage == state["stage"],
                AgentAssetReleaseAuditSample.version == state["candidate_version"],
            )
        )
        if sample is None:
            raise LookupError("Release audit sample not found.")
        ground_truth = {
            "confirmed": "risk_present",
            "false_positive": "risk_absent",
            "risk_present": "risk_present",
            "risk_absent": "risk_absent",
        }.get(str(label or "").strip().lower())
        if ground_truth is None:
            raise ValueError("Blind review requires risk_present or risk_absent.")
        return AgentAssetReleaseTelemetryService(self.db).record_blind_review_label(
            tenant_id=tenant,
            observation_id=sample.observation_id,
            ground_truth=ground_truth,  # type: ignore[arg-type]
            request_id=_required(request_id, "request_id", 160),
            actor_id=actor,
        )

    def _current_release(
        self,
        tenant_id: str,
        asset_id: str,
    ) -> tuple[str, AgentAsset, dict[str, Any]]:
        tenant = _required(tenant_id, "tenant_id", 64)
        asset = self.db.scalar(
            select(AgentAsset).where(
                AgentAsset.id == _required(asset_id, "asset_id", 36),
                (
                    ((AgentAsset.scope == "tenant") & (AgentAsset.tenant_id == tenant))
                    | ((AgentAsset.scope == "platform") & (AgentAsset.tenant_id == "platform"))
                ),
            )
        )
        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" and configured_tenant not in {"", tenant}:
            raise LookupError("Agent asset not found.")
        if asset.scope == "platform" and configured_tenant != tenant:
            raise LookupError("Agent asset not found.")
        state = config.get("release_guard")
        if not isinstance(state, dict):
            raise ValueError("Asset has no active release guard state.")
        normalized = {
            **state,
            "release_id": _required(state.get("release_id"), "release_id", 64),
            "stage": str(state.get("stage") or "").strip().lower(),
            "candidate_version": _required(
                state.get("candidate_version"),
                "candidate_version",
                30,
            ),
        }
        if normalized["stage"] not in _REVIEWABLE_STAGES:
            raise ValueError("Asset is not in a reviewable release stage.")
        return tenant, asset, normalized

    def _item(
        self,
        sample: AgentAssetReleaseAuditSample,
        observation: AgentAssetReleaseObservation,
        votes: Any,
        quorum: int,
        tenant_id: str,
    ) -> dict[str, Any]:
        return {
            "sample_id": sample.id,
            "observation_id": observation.id,
            "source_document_id": AgentAssetReleaseSamplingService(
                self.db
            ).source_reference(sample=sample, tenant_id=tenant_id),
            "rule_code": observation.rule_code,
            "business_stage": observation.business_stage,
            "prediction_blinded": True,
            "reviewer_count": int(votes.reviewer_count if votes is not None else 0),
            "required_reviewers": quorum,
            "conflicted": bool(votes.conflicted if votes is not None else False),
            "created_at": observation.created_at,
        }


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 _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 > 100:
        raise ValueError("limit must be between 1 and 100.")
    return parsed


def _sample_quorum(sample: AgentAssetReleaseAuditSample, default: int) -> int:
    return 2 if sample.stratum != "candidate_positive_census" else default


def _metrics(aggregate: Any, pending: list[AgentAssetReleaseObservation]) -> dict[str, Any]:
    observed_count = aggregate.observed_count
    oldest_pending = min((item.created_at for item in pending), default=None)
    return {
        "observed_count": observed_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,
        "candidate_oldest_pending_at": (
            oldest_pending.isoformat() if oldest_pending is not None else None
        ),
        "candidate_oldest_pending_age_seconds": _age_seconds(oldest_pending),
        "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,
    }


def _age_seconds(value: datetime | None) -> int | None:
    if value is None:
        return None
    normalized = value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
    return max(0, int((datetime.now(UTC) - normalized).total_seconds()))
