"""发布遥测的精度、负样本证据与召回率聚合。"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.agent_asset_release_telemetry import (
    AgentAssetReleaseAuditSample,
    AgentAssetReleaseLabel,
    AgentAssetReleaseObservation,
)
from app.services.agent_asset_release_label_votes import (
    release_reviewer_quorum,
    resolve_release_label_votes,
)
from app.services.agent_asset_release_recall import (
    ReleaseRecallEstimate,
    estimate_release_recall,
)
from app.services.agent_asset_release_telemetry_values import precision


@dataclass(frozen=True, slots=True)
class _RecallEvidence:
    status: str
    estimate: ReleaseRecallEstimate
    false_negative_count: int | None
    negative_sample_count: int
    negative_labeled_count: int
    negative_pending_count: int
    random_population_count: int
    random_sample_count: int
    random_labeled_count: int
    random_false_negative_count: int


def build_release_telemetry_aggregate(
    *,
    db: Session,
    tenant_id: str,
    asset_id: str,
    release_id: str,
    stage: str,
    version: str,
    release_state: dict[str, Any],
) -> Any:
    from app.services.agent_asset_release_telemetry import ReleaseTelemetryAggregate

    observations = _observations(db, tenant_id, asset_id, release_id, stage, version)
    labels = _labels(db, tenant_id, asset_id, release_id, stage, version)
    samples = _samples(db, tenant_id, asset_id, release_id, stage, version)
    vote_states = resolve_release_label_votes(
        labels,
        required_reviewers=release_reviewer_quorum(release_state),
        required_reviewers_by_observation={
            item.observation_id: _sample_quorum(item, release_state)
            for item in samples
        },
    )
    latest_labels = {
        observation_id: state.label
        for observation_id, state in vote_states.items()
        if state.label is not None
    }
    completed = [item for item in observations if item.runtime_status == "completed"]
    failures = [item for item in observations if item.runtime_status == "failed"]
    candidate_hits = [item for item in completed if item.candidate_hit]
    baseline_hits = [item for item in completed if item.baseline_hit is True]
    candidate_values = _values(candidate_hits, latest_labels)
    baseline_values = _values(baseline_hits, latest_labels)
    candidate_confirmed = candidate_values.count("confirmed")
    candidate_false_positive = candidate_values.count("false_positive")
    baseline_confirmed = baseline_values.count("confirmed")
    baseline_false_positive = baseline_values.count("false_positive")
    candidate_pending = len(candidate_hits) - len(candidate_values)
    baseline_pending = len(baseline_hits) - len(baseline_values)
    candidate_precision = precision(candidate_confirmed, candidate_false_positive)
    baseline_precision = (
        precision(baseline_confirmed, baseline_false_positive)
        if not baseline_pending
        else None
    )
    recall = _recall_evidence(
        completed=completed,
        samples=samples,
        labels=latest_labels,
        true_positive_count=candidate_confirmed,
        candidate_pending_count=candidate_pending,
        release_state=release_state,
    )
    reasons = _reasons(
        observations=observations,
        candidate_hits=candidate_hits,
        candidate_values=candidate_values,
        candidate_pending=candidate_pending,
        baseline_precision=baseline_precision,
        recall=recall,
        release_state=release_state,
    )
    precision_ready = bool(candidate_hits and candidate_values and not candidate_pending)
    recall_gate = _recall_gate_enabled(release_state)
    recall_ready = recall.status.startswith("available")
    min_precision = _policy_float(release_state, "min_precision", 0.98)
    hard_precision_failure = (
        candidate_precision is not None and candidate_precision < min_precision
    )
    ready = bool(
        observations
        and (
            failures
            or (
                precision_ready
                and (not recall_gate or recall_ready or hard_precision_failure)
            )
        )
    )
    estimate = recall.estimate
    return ReleaseTelemetryAggregate(
        tenant_id=tenant_id,
        asset_id=asset_id,
        release_id=release_id,
        stage=stage,
        version=version,
        status="ready" if ready else "collecting",
        reasons=tuple(reasons),
        observed_count=len(observations),
        completed_count=len(completed),
        runtime_failure_count=len(failures),
        candidate_hit_count=len(candidate_hits),
        candidate_labeled_count=len(candidate_values),
        candidate_pending_label_count=candidate_pending,
        candidate_confirmed_count=candidate_confirmed,
        candidate_false_positive_count=candidate_false_positive,
        precision=candidate_precision,
        baseline_hit_count=len(baseline_hits),
        baseline_labeled_count=len(baseline_values),
        baseline_pending_label_count=baseline_pending,
        baseline_confirmed_count=baseline_confirmed,
        baseline_false_positive_count=baseline_false_positive,
        baseline_precision=baseline_precision,
        false_negative_count=recall.false_negative_count,
        estimated_false_negative_count=estimate.estimated_false_negative_count,
        false_negative_upper_bound=estimate.false_negative_upper_bound,
        negative_sample_count=recall.negative_sample_count,
        negative_labeled_count=recall.negative_labeled_count,
        negative_pending_label_count=recall.negative_pending_count,
        random_negative_population_count=recall.random_population_count,
        random_negative_sample_count=recall.random_sample_count,
        random_negative_labeled_count=recall.random_labeled_count,
        random_negative_false_negative_count=recall.random_false_negative_count,
        recall=estimate.recall,
        recall_lower_bound=estimate.recall_lower_bound,
        recall_confidence_level=estimate.confidence_level,
        recall_method=estimate.method,
        negative_ground_truth_status=recall.status,
    )


def _recall_evidence(
    *,
    completed: list[AgentAssetReleaseObservation],
    samples: list[AgentAssetReleaseAuditSample],
    labels: dict[str, str],
    true_positive_count: int,
    candidate_pending_count: int,
    release_state: dict[str, Any],
) -> _RecallEvidence:
    by_id = {item.id: item for item in completed}
    negative_samples = [
        item
        for item in samples
        if item.observation_id in by_id and not by_id[item.observation_id].candidate_hit
    ]
    negative_labeled = [item for item in negative_samples if item.observation_id in labels]
    disagreement = [
        item
        for item in completed
        if not item.candidate_hit and item.baseline_hit is True
    ]
    random_population = [
        item
        for item in completed
        if not item.candidate_hit and item.baseline_hit is not True
    ]
    random_samples = [
        item for item in negative_samples if item.stratum == "candidate_negative_random"
    ]
    random_labeled = [item for item in random_samples if item.observation_id in labels]
    disagreement_false_negatives = sum(
        1 for item in disagreement if labels.get(item.id) == "confirmed"
    )
    random_false_negatives = sum(
        1 for item in random_labeled if labels[item.observation_id] == "confirmed"
    )
    observed_false_negatives = sum(
        1 for item in negative_labeled if labels[item.observation_id] == "confirmed"
    )
    status = _negative_status(
        candidate_pending_count=candidate_pending_count,
        disagreement=disagreement,
        random_population=random_population,
        random_samples=random_samples,
        random_labeled=random_labeled,
        labels=labels,
        minimum_random_reviews=_policy_int(
            release_state,
            "negative_min_reviewed",
            5,
            low=1,
            high=10_000,
        ),
    )
    confidence = _policy_confidence(release_state)
    estimate = _estimate_if_ready(
        status=status,
        true_positive_count=true_positive_count,
        disagreement_false_negatives=disagreement_false_negatives,
        random_population_count=len(random_population),
        random_reviewed_count=len(random_labeled),
        random_false_negative_count=random_false_negatives,
        confidence=confidence,
    )
    return _RecallEvidence(
        status=status,
        estimate=estimate,
        false_negative_count=(
            observed_false_negatives if status.startswith("available") else None
        ),
        negative_sample_count=len(negative_samples),
        negative_labeled_count=len(negative_labeled),
        negative_pending_count=len(negative_samples) - len(negative_labeled),
        random_population_count=len(random_population),
        random_sample_count=len(random_samples),
        random_labeled_count=len(random_labeled),
        random_false_negative_count=random_false_negatives,
    )


def _negative_status(
    *,
    candidate_pending_count: int,
    disagreement: list[AgentAssetReleaseObservation],
    random_population: list[AgentAssetReleaseObservation],
    random_samples: list[AgentAssetReleaseAuditSample],
    random_labeled: list[AgentAssetReleaseAuditSample],
    labels: dict[str, str],
    minimum_random_reviews: int,
) -> str:
    if candidate_pending_count:
        return "collecting_candidate_labels"
    if any(item.id not in labels for item in disagreement):
        return "collecting_disagreement_labels"
    if not random_population:
        return "available_census"
    if not random_samples:
        return "unavailable_no_random_negative_samples"
    if len(random_labeled) < len(random_samples):
        return "collecting_random_negative_labels"
    if len(random_labeled) < minimum_random_reviews:
        return "insufficient_random_negative_reviews"
    return "available_stratified_random_audit"


def _estimate_if_ready(
    *,
    status: str,
    true_positive_count: int,
    disagreement_false_negatives: int,
    random_population_count: int,
    random_reviewed_count: int,
    random_false_negative_count: int,
    confidence: float,
) -> ReleaseRecallEstimate:
    if not status.startswith("available"):
        return estimate_release_recall(
            true_positive_count=true_positive_count,
            disagreement_false_negative_count=disagreement_false_negatives,
            random_negative_population_count=max(1, random_population_count),
            random_reviewed_count=0,
            random_false_negative_count=0,
            confidence_level=confidence,
        )
    return estimate_release_recall(
        true_positive_count=true_positive_count,
        disagreement_false_negative_count=disagreement_false_negatives,
        random_negative_population_count=random_population_count,
        random_reviewed_count=random_reviewed_count,
        random_false_negative_count=random_false_negative_count,
        confidence_level=confidence,
    )


def _reasons(
    *,
    observations: list[AgentAssetReleaseObservation],
    candidate_hits: list[AgentAssetReleaseObservation],
    candidate_values: list[str],
    candidate_pending: int,
    baseline_precision: float | None,
    recall: _RecallEvidence,
    release_state: dict[str, Any],
) -> list[str]:
    reasons: list[str] = []
    if not observations:
        reasons.append("no_runtime_observations")
    if not candidate_hits:
        reasons.append("no_candidate_positive_samples")
    if not candidate_values:
        reasons.append("no_candidate_labels")
    if candidate_pending:
        reasons.append("candidate_labels_pending")
    if baseline_precision is None:
        reasons.append("baseline_precision_unavailable")
    if _recall_gate_enabled(release_state) and not recall.status.startswith("available"):
        reasons.append(recall.status)
    return reasons


def _observations(
    db: Session,
    tenant_id: str,
    asset_id: str,
    release_id: str,
    stage: str,
    version: str,
) -> list[AgentAssetReleaseObservation]:
    return list(
        db.scalars(
            select(AgentAssetReleaseObservation)
            .where(
                AgentAssetReleaseObservation.tenant_id == tenant_id,
                AgentAssetReleaseObservation.asset_id == asset_id,
                AgentAssetReleaseObservation.release_id == release_id,
                AgentAssetReleaseObservation.stage == stage,
                AgentAssetReleaseObservation.version == version,
            )
            .order_by(
                AgentAssetReleaseObservation.created_at.asc(),
                AgentAssetReleaseObservation.id.asc(),
            )
        ).all()
    )


def _labels(
    db: Session,
    tenant_id: str,
    asset_id: str,
    release_id: str,
    stage: str,
    version: str,
) -> list[AgentAssetReleaseLabel]:
    return list(
        db.scalars(
            select(AgentAssetReleaseLabel)
            .where(
                AgentAssetReleaseLabel.tenant_id == tenant_id,
                AgentAssetReleaseLabel.asset_id == asset_id,
                AgentAssetReleaseLabel.release_id == release_id,
                AgentAssetReleaseLabel.stage == stage,
                AgentAssetReleaseLabel.version == version,
            )
            .order_by(
                AgentAssetReleaseLabel.created_at.asc(),
                AgentAssetReleaseLabel.id.asc(),
            )
        ).all()
    )


def _samples(
    db: Session,
    tenant_id: str,
    asset_id: str,
    release_id: str,
    stage: str,
    version: str,
) -> list[AgentAssetReleaseAuditSample]:
    return list(
        db.scalars(
            select(AgentAssetReleaseAuditSample)
            .where(
                AgentAssetReleaseAuditSample.tenant_id == tenant_id,
                AgentAssetReleaseAuditSample.asset_id == asset_id,
                AgentAssetReleaseAuditSample.release_id == release_id,
                AgentAssetReleaseAuditSample.stage == stage,
                AgentAssetReleaseAuditSample.version == version,
            )
            .order_by(
                AgentAssetReleaseAuditSample.created_at.asc(),
                AgentAssetReleaseAuditSample.id.asc(),
            )
        ).all()
    )


def _values(
    observations: list[AgentAssetReleaseObservation],
    labels: dict[str, str],
) -> list[str]:
    return [labels[item.id] for item in observations if item.id in labels]


def _recall_gate_enabled(release_state: dict[str, Any]) -> bool:
    policy = release_state.get("policy")
    return isinstance(policy, dict) and policy.get("recall_gate_enabled") is True


def _sample_quorum(
    sample: AgentAssetReleaseAuditSample,
    release_state: dict[str, Any],
) -> int:
    if sample.stratum != "candidate_positive_census":
        return 2
    return release_reviewer_quorum(release_state)


def _policy_int(
    release_state: dict[str, Any],
    key: str,
    default: int,
    *,
    low: int,
    high: int,
) -> int:
    policy = release_state.get("policy")
    source = policy if isinstance(policy, dict) else {}
    try:
        parsed = int(source.get(key, default))
    except (TypeError, ValueError, OverflowError):
        return default
    return max(low, min(high, parsed))


def _policy_float(release_state: dict[str, Any], key: str, default: float) -> float:
    policy = release_state.get("policy")
    source = policy if isinstance(policy, dict) else {}
    try:
        parsed = float(source.get(key, default))
    except (TypeError, ValueError, OverflowError):
        return default
    return max(0.0, min(1.0, parsed))


def _policy_confidence(release_state: dict[str, Any]) -> float:
    value = _policy_float(release_state, "recall_confidence_level", 0.95)
    return value if value in {0.9, 0.95, 0.99} else 0.95
