use std::time::Duration;

use codex_analytics::GuardianApprovalRequestSource;
use codex_analytics::GuardianReviewAnalyticsResult;
use codex_analytics::GuardianReviewDecision;
use codex_analytics::GuardianReviewFailureReason;
use codex_analytics::GuardianReviewSessionKind;
use codex_analytics::GuardianReviewTerminalStatus;
use codex_analytics::GuardianReviewedAction;
use codex_otel::GUARDIAN_REVIEW_COUNT_METRIC;
use codex_otel::GUARDIAN_REVIEW_DURATION_METRIC;
use codex_otel::GUARDIAN_REVIEW_TOKEN_USAGE_METRIC;
use codex_otel::GUARDIAN_REVIEW_TTFT_DURATION_METRIC;
use codex_otel::SessionTelemetry;
use codex_otel::sanitize_metric_tag_value;
use codex_protocol::protocol::GuardianAssessmentOutcome;
use codex_protocol::protocol::GuardianRiskLevel;
use codex_protocol::protocol::GuardianUserAuthorization;
use codex_protocol::protocol::TokenUsage;

pub(crate) fn emit_guardian_review_metrics(
    session_telemetry: &SessionTelemetry,
    result: &GuardianReviewAnalyticsResult,
    approval_request_source: GuardianApprovalRequestSource,
    reviewed_action: &GuardianReviewedAction,
    completion_latency_ms: u64,
) {
    let tags = guardian_review_metric_tags(result, approval_request_source, reviewed_action);
    let tag_refs: Vec<(&str, &str)> = tags
        .iter()
        .map(|(key, value)| (*key, value.as_str()))
        .collect();

    session_telemetry.counter(GUARDIAN_REVIEW_COUNT_METRIC, /*inc*/ 1, &tag_refs);
    session_telemetry.record_duration(
        GUARDIAN_REVIEW_DURATION_METRIC,
        Duration::from_millis(completion_latency_ms),
        &tag_refs,
    );

    if let Some(time_to_first_token_ms) = result.time_to_first_token_ms {
        session_telemetry.record_duration(
            GUARDIAN_REVIEW_TTFT_DURATION_METRIC,
            Duration::from_millis(time_to_first_token_ms),
            &tag_refs,
        );
    }

    if let Some(token_usage) = result.token_usage.as_ref() {
        emit_guardian_token_usage_histograms(session_telemetry, token_usage, tags);
    }
}

fn emit_guardian_token_usage_histograms(
    session_telemetry: &SessionTelemetry,
    token_usage: &TokenUsage,
    base_tags: Vec<(&'static str, String)>,
) {
    for (token_type, value) in [
        ("total", token_usage.total_tokens.max(0)),
        ("input", token_usage.input_tokens.max(0)),
        ("cached_input", token_usage.cached_input()),
        ("non_cached_input", token_usage.non_cached_input()),
        ("output", token_usage.output_tokens.max(0)),
        (
            "reasoning_output",
            token_usage.reasoning_output_tokens.max(0),
        ),
    ] {
        let mut tags = base_tags.clone();
        tags.push(("token_type", token_type.to_string()));
        let tag_refs: Vec<(&str, &str)> = tags
            .iter()
            .map(|(key, value)| (*key, value.as_str()))
            .collect();
        session_telemetry.histogram(GUARDIAN_REVIEW_TOKEN_USAGE_METRIC, value, &tag_refs);
    }
}

fn guardian_review_metric_tags(
    result: &GuardianReviewAnalyticsResult,
    approval_request_source: GuardianApprovalRequestSource,
    reviewed_action: &GuardianReviewedAction,
) -> Vec<(&'static str, String)> {
    vec![
        ("decision", decision_tag(result.decision).to_string()),
        (
            "terminal_status",
            terminal_status_tag(result.terminal_status).to_string(),
        ),
        (
            "failure_reason",
            failure_reason_tag(result.failure_reason).to_string(),
        ),
        (
            "approval_request_source",
            approval_request_source_tag(approval_request_source).to_string(),
        ),
        ("action", reviewed_action_tag(reviewed_action).to_string()),
        (
            "session_kind",
            session_kind_tag(result.guardian_session_kind).to_string(),
        ),
        (
            "had_prior_review_context",
            optional_bool_tag(result.had_prior_review_context).to_string(),
        ),
        (
            "reviewed_action_truncated",
            bool_tag(result.reviewed_action_truncated).to_string(),
        ),
        ("risk_level", risk_level_tag(result.risk_level).to_string()),
        (
            "user_authorization",
            user_authorization_tag(result.user_authorization).to_string(),
        ),
        ("outcome", outcome_tag(result.outcome).to_string()),
        (
            "guardian_model",
            result
                .guardian_model
                .as_deref()
                .map(sanitize_metric_tag_value)
                .unwrap_or_else(|| "none".to_string()),
        ),
        (
            "guardian_reasoning_effort",
            result
                .guardian_reasoning_effort
                .as_deref()
                .map(sanitize_metric_tag_value)
                .unwrap_or_else(|| "none".to_string()),
        ),
    ]
}

fn decision_tag(decision: GuardianReviewDecision) -> &'static str {
    match decision {
        GuardianReviewDecision::Approved => "approved",
        GuardianReviewDecision::Denied => "denied",
        GuardianReviewDecision::Aborted => "aborted",
    }
}

fn terminal_status_tag(status: GuardianReviewTerminalStatus) -> &'static str {
    match status {
        GuardianReviewTerminalStatus::Approved => "approved",
        GuardianReviewTerminalStatus::Denied => "denied",
        GuardianReviewTerminalStatus::Aborted => "aborted",
        GuardianReviewTerminalStatus::TimedOut => "timed_out",
        GuardianReviewTerminalStatus::FailedClosed => "failed_closed",
    }
}

fn failure_reason_tag(reason: Option<GuardianReviewFailureReason>) -> &'static str {
    match reason {
        Some(GuardianReviewFailureReason::Timeout) => "timeout",
        Some(GuardianReviewFailureReason::Cancelled) => "cancelled",
        Some(GuardianReviewFailureReason::PromptBuildError) => "prompt_build_error",
        Some(GuardianReviewFailureReason::SessionError) => "session_error",
        Some(GuardianReviewFailureReason::ParseError) => "parse_error",
        None => "none",
    }
}

fn approval_request_source_tag(source: GuardianApprovalRequestSource) -> &'static str {
    match source {
        GuardianApprovalRequestSource::MainTurn => "main_turn",
        GuardianApprovalRequestSource::DelegatedSubagent => "delegated_subagent",
    }
}

fn reviewed_action_tag(action: &GuardianReviewedAction) -> &'static str {
    match action {
        GuardianReviewedAction::Shell { .. } => "shell",
        GuardianReviewedAction::UnifiedExec { .. } => "unified_exec",
        GuardianReviewedAction::Execve { .. } => "execve",
        GuardianReviewedAction::ApplyPatch {} => "apply_patch",
        GuardianReviewedAction::NetworkAccess { .. } => "network_access",
        GuardianReviewedAction::McpToolCall { .. } => "mcp_tool_call",
        GuardianReviewedAction::RequestPermissions {} => "request_permissions",
    }
}

fn session_kind_tag(kind: Option<GuardianReviewSessionKind>) -> &'static str {
    match kind {
        Some(GuardianReviewSessionKind::TrunkNew) => "trunk_new",
        Some(GuardianReviewSessionKind::TrunkReused) => "trunk_reused",
        Some(GuardianReviewSessionKind::EphemeralForked) => "ephemeral_forked",
        None => "none",
    }
}

fn optional_bool_tag(value: Option<bool>) -> &'static str {
    match value {
        Some(true) => "true",
        Some(false) => "false",
        None => "unknown",
    }
}

fn bool_tag(value: bool) -> &'static str {
    if value { "true" } else { "false" }
}

fn risk_level_tag(risk_level: Option<GuardianRiskLevel>) -> &'static str {
    match risk_level {
        Some(GuardianRiskLevel::Low) => "low",
        Some(GuardianRiskLevel::Medium) => "medium",
        Some(GuardianRiskLevel::High) => "high",
        Some(GuardianRiskLevel::Critical) => "critical",
        None => "none",
    }
}

fn user_authorization_tag(user_authorization: Option<GuardianUserAuthorization>) -> &'static str {
    match user_authorization {
        Some(GuardianUserAuthorization::Unknown) => "unknown",
        Some(GuardianUserAuthorization::Low) => "low",
        Some(GuardianUserAuthorization::Medium) => "medium",
        Some(GuardianUserAuthorization::High) => "high",
        None => "none",
    }
}

fn outcome_tag(outcome: Option<GuardianAssessmentOutcome>) -> &'static str {
    match outcome {
        Some(GuardianAssessmentOutcome::Allow) => "allow",
        Some(GuardianAssessmentOutcome::Deny) => "deny",
        None => "none",
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use codex_otel::MetricsClient;
    use codex_otel::MetricsConfig;
    use codex_protocol::ThreadId;
    use codex_protocol::protocol::SessionSource;
    use opentelemetry::KeyValue;
    use opentelemetry_sdk::metrics::InMemoryMetricExporter;
    use opentelemetry_sdk::metrics::data::AggregatedMetrics;
    use opentelemetry_sdk::metrics::data::Metric;
    use opentelemetry_sdk::metrics::data::MetricData;
    use opentelemetry_sdk::metrics::data::ResourceMetrics;
    use pretty_assertions::assert_eq;
    use std::collections::BTreeMap;

    fn test_session_telemetry() -> SessionTelemetry {
        let exporter = InMemoryMetricExporter::default();
        let metrics = MetricsClient::new(
            MetricsConfig::in_memory("test", "codex-core", env!("CARGO_PKG_VERSION"), exporter)
                .with_runtime_reader(),
        )
        .expect("in-memory metrics client");
        SessionTelemetry::new(
            ThreadId::new(),
            "gpt-5.4",
            "gpt-5.4",
            /*account_id*/ None,
            /*account_email*/ None,
            /*auth_mode*/ None,
            "test_originator".to_string(),
            /*log_user_prompts*/ false,
            "tty".to_string(),
            SessionSource::Cli,
        )
        .with_metrics_without_metadata_tags(metrics)
    }

    fn find_metric<'a>(resource_metrics: &'a ResourceMetrics, name: &str) -> &'a Metric {
        for scope_metrics in resource_metrics.scope_metrics() {
            for metric in scope_metrics.metrics() {
                if metric.name() == name {
                    return metric;
                }
            }
        }
        panic!("metric {name} missing");
    }

    fn attributes_to_map<'a>(
        attributes: impl Iterator<Item = &'a KeyValue>,
    ) -> BTreeMap<String, String> {
        attributes
            .map(|kv| (kv.key.as_str().to_string(), kv.value.as_str().to_string()))
            .collect()
    }

    fn counter_point(
        resource_metrics: &ResourceMetrics,
        name: &str,
    ) -> (BTreeMap<String, String>, u64) {
        let metric = find_metric(resource_metrics, name);
        match metric.data() {
            AggregatedMetrics::U64(data) => match data {
                MetricData::Sum(sum) => {
                    let points: Vec<_> = sum.data_points().collect();
                    assert_eq!(points.len(), 1);
                    let point = points[0];
                    (attributes_to_map(point.attributes()), point.value())
                }
                _ => panic!("unexpected counter aggregation"),
            },
            _ => panic!("unexpected counter data type"),
        }
    }

    fn histogram_sums(resource_metrics: &ResourceMetrics, name: &str) -> BTreeMap<String, u64> {
        let metric = find_metric(resource_metrics, name);
        match metric.data() {
            AggregatedMetrics::F64(data) => match data {
                MetricData::Histogram(histogram) => histogram
                    .data_points()
                    .map(|point| {
                        let attrs = attributes_to_map(point.attributes());
                        (
                            attrs
                                .get("token_type")
                                .cloned()
                                .unwrap_or_else(|| "sample".to_string()),
                            point.sum() as u64,
                        )
                    })
                    .collect(),
                _ => panic!("unexpected histogram aggregation"),
            },
            _ => panic!("unexpected histogram data type"),
        }
    }

    #[test]
    fn guardian_review_metrics_record_counts_durations_and_token_usage() {
        let session_telemetry = test_session_telemetry();
        let result = GuardianReviewAnalyticsResult {
            decision: GuardianReviewDecision::Approved,
            terminal_status: GuardianReviewTerminalStatus::Approved,
            risk_level: Some(GuardianRiskLevel::Low),
            user_authorization: Some(GuardianUserAuthorization::High),
            outcome: Some(GuardianAssessmentOutcome::Allow),
            guardian_session_kind: Some(GuardianReviewSessionKind::TrunkReused),
            guardian_model: Some("gpt-5.4 guardian".to_string()),
            guardian_reasoning_effort: Some("low".to_string()),
            had_prior_review_context: Some(true),
            reviewed_action_truncated: true,
            token_usage: Some(TokenUsage {
                input_tokens: 10,
                cached_input_tokens: 4,
                output_tokens: 3,
                reasoning_output_tokens: 2,
                total_tokens: 15,
            }),
            time_to_first_token_ms: Some(123),
            ..GuardianReviewAnalyticsResult::without_session()
        };

        emit_guardian_review_metrics(
            &session_telemetry,
            &result,
            GuardianApprovalRequestSource::DelegatedSubagent,
            &GuardianReviewedAction::NetworkAccess {
                protocol: codex_protocol::approvals::NetworkApprovalProtocol::Https,
                port: 443,
            },
            /*completion_latency_ms*/ 456,
        );

        let snapshot = session_telemetry
            .snapshot_metrics()
            .expect("runtime metrics snapshot");
        let (attrs, value) = counter_point(&snapshot, GUARDIAN_REVIEW_COUNT_METRIC);

        assert_eq!(value, 1);
        assert_eq!(
            attrs,
            BTreeMap::from([
                ("action".to_string(), "network_access".to_string()),
                (
                    "approval_request_source".to_string(),
                    "delegated_subagent".to_string()
                ),
                ("decision".to_string(), "approved".to_string()),
                ("failure_reason".to_string(), "none".to_string()),
                ("guardian_model".to_string(), "gpt-5.4_guardian".to_string()),
                ("guardian_reasoning_effort".to_string(), "low".to_string()),
                ("had_prior_review_context".to_string(), "true".to_string()),
                ("outcome".to_string(), "allow".to_string()),
                ("reviewed_action_truncated".to_string(), "true".to_string()),
                ("risk_level".to_string(), "low".to_string()),
                ("session_kind".to_string(), "trunk_reused".to_string()),
                ("terminal_status".to_string(), "approved".to_string()),
                ("user_authorization".to_string(), "high".to_string()),
            ])
        );

        assert_eq!(
            histogram_sums(&snapshot, GUARDIAN_REVIEW_TOKEN_USAGE_METRIC),
            BTreeMap::from([
                ("cached_input".to_string(), 4),
                ("input".to_string(), 10),
                ("non_cached_input".to_string(), 6),
                ("output".to_string(), 3),
                ("reasoning_output".to_string(), 2),
                ("total".to_string(), 15),
            ])
        );
        assert_eq!(
            histogram_sums(&snapshot, GUARDIAN_REVIEW_DURATION_METRIC),
            BTreeMap::from([("sample".to_string(), 456)])
        );
        assert_eq!(
            histogram_sums(&snapshot, GUARDIAN_REVIEW_TTFT_DURATION_METRIC),
            BTreeMap::from([("sample".to_string(), 123)])
        );
    }
}
