use super::traits::{Observer, ObserverEvent, ObserverMetric};
use prometheus::{
    Encoder, Gauge, GaugeVec, Histogram, HistogramOpts, HistogramVec, IntCounterVec, Registry,
    TextEncoder,
};

/// Prometheus-backed observer — exposes metrics for scraping via `/metrics`.
pub struct PrometheusObserver {
    registry: Registry,

    // Counters
    agent_starts: IntCounterVec,
    tool_calls: IntCounterVec,
    channel_messages: IntCounterVec,
    heartbeat_ticks: prometheus::IntCounter,
    errors: IntCounterVec,
    budget_warnings: IntCounterVec,
    budget_exceeded: IntCounterVec,
    budget_overrides: IntCounterVec,

    // Histograms
    agent_duration: HistogramVec,
    tool_duration: HistogramVec,
    request_latency: Histogram,

    // Gauges
    tokens_used: prometheus::IntGauge,
    cost_usd_last: Gauge,
    active_sessions: GaugeVec,
    queue_depth: GaugeVec,

    // Image ingress
    image_ingress: IntCounterVec,

    // Audio ingress
    audio_ingress: IntCounterVec,
}

impl PrometheusObserver {
    pub fn new() -> Self {
        let registry = Registry::new();

        let agent_starts = IntCounterVec::new(
            prometheus::Opts::new("corvus_agent_starts_total", "Total agent invocations"),
            &["provider", "model"],
        )
        .expect("valid metric");

        let tool_calls = IntCounterVec::new(
            prometheus::Opts::new("corvus_tool_calls_total", "Total tool calls"),
            &["tool", "success"],
        )
        .expect("valid metric");

        let channel_messages = IntCounterVec::new(
            prometheus::Opts::new("corvus_channel_messages_total", "Total channel messages"),
            &["channel", "direction"],
        )
        .expect("valid metric");

        let heartbeat_ticks =
            prometheus::IntCounter::new("corvus_heartbeat_ticks_total", "Total heartbeat ticks")
                .expect("valid metric");

        let errors = IntCounterVec::new(
            prometheus::Opts::new("corvus_errors_total", "Total errors by component"),
            &["component"],
        )
        .expect("valid metric");

        let budget_warnings = IntCounterVec::new(
            prometheus::Opts::new(
                "corvus_budget_warnings_total",
                "Budget warning lifecycle events",
            ),
            &["period", "surface"],
        )
        .expect("valid metric");

        let budget_exceeded = IntCounterVec::new(
            prometheus::Opts::new(
                "corvus_budget_exceeded_total",
                "Budget hard block lifecycle events",
            ),
            &["period", "surface"],
        )
        .expect("valid metric");

        let budget_overrides = IntCounterVec::new(
            prometheus::Opts::new(
                "corvus_budget_overrides_total",
                "Budget override lifecycle events",
            ),
            &["action", "scope", "surface"],
        )
        .expect("valid metric");

        let agent_duration = HistogramVec::new(
            HistogramOpts::new(
                "corvus_agent_duration_seconds",
                "Agent invocation duration in seconds",
            )
            .buckets(vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]),
            &["provider", "model"],
        )
        .expect("valid metric");

        let tool_duration = HistogramVec::new(
            HistogramOpts::new(
                "corvus_tool_duration_seconds",
                "Tool execution duration in seconds",
            )
            .buckets(vec![0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0]),
            &["tool"],
        )
        .expect("valid metric");

        let request_latency = Histogram::with_opts(
            HistogramOpts::new(
                "corvus_request_latency_seconds",
                "Request latency in seconds",
            )
            .buckets(vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]),
        )
        .expect("valid metric");

        let tokens_used =
            prometheus::IntGauge::new("corvus_tokens_used_last", "Tokens used in the last request")
                .expect("valid metric");

        let cost_usd_last = Gauge::new(
            "corvus_cost_usd_last",
            "Cost of the last completed request in USD",
        )
        .expect("valid metric");

        let active_sessions = GaugeVec::new(
            prometheus::Opts::new("corvus_active_sessions", "Number of active sessions"),
            &[],
        )
        .expect("valid metric");

        let queue_depth = GaugeVec::new(
            prometheus::Opts::new("corvus_queue_depth", "Message queue depth"),
            &[],
        )
        .expect("valid metric");

        let image_ingress = IntCounterVec::new(
            prometheus::Opts::new(
                "corvus_image_ingress_total",
                "Image ingress lifecycle events",
            ),
            &["channel", "outcome", "reason"],
        )
        .expect("valid metric");

        let audio_ingress = IntCounterVec::new(
            prometheus::Opts::new(
                "corvus_audio_ingress_total",
                "Audio ingress lifecycle events",
            ),
            &["channel", "outcome", "reason"],
        )
        .expect("valid metric");

        // Register all metrics
        registry.register(Box::new(audio_ingress.clone())).ok();
        registry.register(Box::new(image_ingress.clone())).ok();
        registry.register(Box::new(agent_starts.clone())).ok();
        registry.register(Box::new(tool_calls.clone())).ok();
        registry.register(Box::new(channel_messages.clone())).ok();
        registry.register(Box::new(heartbeat_ticks.clone())).ok();
        registry.register(Box::new(errors.clone())).ok();
        registry.register(Box::new(budget_warnings.clone())).ok();
        registry.register(Box::new(budget_exceeded.clone())).ok();
        registry.register(Box::new(budget_overrides.clone())).ok();
        registry.register(Box::new(agent_duration.clone())).ok();
        registry.register(Box::new(tool_duration.clone())).ok();
        registry.register(Box::new(request_latency.clone())).ok();
        registry.register(Box::new(tokens_used.clone())).ok();
        registry.register(Box::new(cost_usd_last.clone())).ok();
        registry.register(Box::new(active_sessions.clone())).ok();
        registry.register(Box::new(queue_depth.clone())).ok();

        Self {
            registry,
            agent_starts,
            tool_calls,
            channel_messages,
            heartbeat_ticks,
            errors,
            budget_warnings,
            budget_exceeded,
            budget_overrides,
            agent_duration,
            tool_duration,
            request_latency,
            tokens_used,
            cost_usd_last,
            active_sessions,
            queue_depth,
            image_ingress,
            audio_ingress,
        }
    }

    /// Encode all registered metrics into Prometheus text exposition format.
    pub fn encode(&self) -> String {
        let encoder = TextEncoder::new();
        let families = self.registry.gather();
        let mut buf = Vec::new();
        encoder.encode(&families, &mut buf).unwrap_or_default();
        String::from_utf8(buf).unwrap_or_default()
    }
}

impl Observer for PrometheusObserver {
    fn record_event(&self, event: &ObserverEvent) {
        match event {
            ObserverEvent::AgentStart { provider, model } => {
                self.agent_starts
                    .with_label_values(&[provider, model])
                    .inc();
            }
            ObserverEvent::AgentEnd {
                provider,
                model,
                duration,
                tokens_used,
                cost_usd,
            } => {
                // Agent duration is recorded via the histogram with provider/model labels
                self.agent_duration
                    .with_label_values(&[provider, model])
                    .observe(duration.as_secs_f64());
                if let Some(t) = tokens_used {
                    self.tokens_used.set(i64::try_from(*t).unwrap_or(i64::MAX));
                }
                self.cost_usd_last.set(cost_usd.unwrap_or(0.0));
            }
            ObserverEvent::ToolCallStart { tool: _ }
            | ObserverEvent::TurnComplete
            | ObserverEvent::CodeSessionCompleted { .. }
            | ObserverEvent::LlmRequest { .. }
            | ObserverEvent::LlmResponse { .. }
            | ObserverEvent::MissionStarted { .. }
            | ObserverEvent::MissionCheckpointProgress { .. }
            | ObserverEvent::MissionGuardrailViolation { .. }
            | ObserverEvent::MissionCompleted { .. }
            | ObserverEvent::MissionTerminated { .. } => {}
            ObserverEvent::BudgetWarning(event) => {
                self.budget_warnings
                    .with_label_values(&[
                        super::traits::usage_period_label(event.period),
                        event.surface.as_deref().unwrap_or("unknown"),
                    ])
                    .inc();
            }
            ObserverEvent::BudgetExceeded(event) => {
                self.budget_exceeded
                    .with_label_values(&[
                        super::traits::usage_period_label(event.period),
                        event.surface.as_deref().unwrap_or("unknown"),
                    ])
                    .inc();
            }
            ObserverEvent::BudgetOverride(event) => {
                self.budget_overrides
                    .with_label_values(&[
                        event.action.as_str(),
                        super::traits::cost_override_scope_label(event.scope),
                        event.surface.as_deref().unwrap_or("unknown"),
                    ])
                    .inc();
            }
            ObserverEvent::AudioIngress(evt) => {
                let outcome = format!("{:?}", evt.outcome);
                let reason = evt
                    .reason
                    .as_ref()
                    .map(|r| r.to_string())
                    .unwrap_or_default();
                self.audio_ingress
                    .with_label_values(&[&evt.channel, &outcome, &reason])
                    .inc();
            }
            ObserverEvent::ImageIngress(evt) => {
                let outcome = format!("{:?}", evt.outcome);
                let reason = evt
                    .reason
                    .as_ref()
                    .map(|r| r.to_string())
                    .unwrap_or_default();
                self.image_ingress
                    .with_label_values(&[&evt.channel, &outcome, &reason])
                    .inc();
            }
            ObserverEvent::ToolCall {
                tool,
                duration,
                success,
            } => {
                let success_str = if *success { "true" } else { "false" };
                self.tool_calls
                    .with_label_values(&[tool.as_str(), success_str])
                    .inc();
                self.tool_duration
                    .with_label_values(&[tool.as_str()])
                    .observe(duration.as_secs_f64());
            }
            ObserverEvent::ChannelMessage { channel, direction } => {
                self.channel_messages
                    .with_label_values(&[channel, direction])
                    .inc();
            }
            ObserverEvent::HeartbeatTick => {
                self.heartbeat_ticks.inc();
            }
            ObserverEvent::Error {
                component,
                message: _,
            } => {
                self.errors.with_label_values(&[component]).inc();
            }
        }
    }

    fn record_metric(&self, metric: &ObserverMetric) {
        match metric {
            ObserverMetric::RequestLatency(d) => {
                self.request_latency.observe(d.as_secs_f64());
            }
            ObserverMetric::TokensUsed(t) => {
                self.tokens_used.set(i64::try_from(*t).unwrap_or(i64::MAX));
            }
            ObserverMetric::ActiveSessions(s) => {
                self.active_sessions
                    .with_label_values(&[] as &[&str])
                    .set(*s as f64);
            }
            ObserverMetric::QueueDepth(d) => {
                self.queue_depth
                    .with_label_values(&[] as &[&str])
                    .set(*d as f64);
            }
        }
    }

    fn name(&self) -> &str {
        "prometheus"
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cost::{BudgetState, CostOverrideScope, UsagePeriod};
    use crate::observability::{BudgetOverrideAction, BudgetOverrideEvent};
    use std::time::Duration;

    #[test]
    fn prometheus_observer_name() {
        assert_eq!(PrometheusObserver::new().name(), "prometheus");
    }

    #[test]
    fn records_all_events_without_panic() {
        let obs = PrometheusObserver::new();
        obs.record_event(&ObserverEvent::AgentStart {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
        });
        obs.record_event(&ObserverEvent::AgentEnd {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
            duration: Duration::from_millis(500),
            tokens_used: Some(100),
            cost_usd: None,
        });
        obs.record_event(&ObserverEvent::AgentEnd {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
            duration: Duration::ZERO,
            tokens_used: None,
            cost_usd: None,
        });
        obs.record_event(&ObserverEvent::ToolCall {
            tool: "shell".into(),
            duration: Duration::from_millis(10),
            success: true,
        });
        obs.record_event(&ObserverEvent::ToolCall {
            tool: "file_read".into(),
            duration: Duration::from_millis(5),
            success: false,
        });
        obs.record_event(&ObserverEvent::ChannelMessage {
            channel: "telegram".into(),
            direction: "inbound".into(),
        });
        obs.record_event(&ObserverEvent::HeartbeatTick);
        obs.record_event(&ObserverEvent::Error {
            component: "provider".into(),
            message: "timeout".into(),
        });
        obs.record_event(&ObserverEvent::CodeSessionCompleted {
            session_id: "sess-1".into(),
            status: "completed".into(),
            summary: "Updated tests".into(),
            changed_files: vec!["src/lib.rs".into()],
            commands: vec!["cargo test".into()],
            validations: vec!["pass:cargo test".into()],
            blockers: vec![],
            pending_work: vec![],
            delegated: false,
        });
    }

    #[test]
    fn records_all_metrics_without_panic() {
        let obs = PrometheusObserver::new();
        obs.record_metric(&ObserverMetric::RequestLatency(Duration::from_secs(2)));
        obs.record_metric(&ObserverMetric::TokensUsed(500));
        obs.record_metric(&ObserverMetric::TokensUsed(0));
        obs.record_metric(&ObserverMetric::ActiveSessions(3));
        obs.record_metric(&ObserverMetric::QueueDepth(42));
    }

    #[test]
    fn encode_produces_prometheus_text_format() {
        let obs = PrometheusObserver::new();
        obs.record_event(&ObserverEvent::AgentStart {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
        });
        obs.record_event(&ObserverEvent::ToolCall {
            tool: "shell".into(),
            duration: Duration::from_millis(100),
            success: true,
        });
        obs.record_event(&ObserverEvent::HeartbeatTick);
        obs.record_metric(&ObserverMetric::RequestLatency(Duration::from_millis(250)));

        let output = obs.encode();
        assert!(output.contains("corvus_agent_starts_total"));
        assert!(output.contains("corvus_tool_calls_total"));
        assert!(output.contains("corvus_heartbeat_ticks_total"));
        assert!(output.contains("corvus_request_latency_seconds"));
    }

    #[test]
    fn counters_increment_correctly() {
        let obs = PrometheusObserver::new();

        for _ in 0..3 {
            obs.record_event(&ObserverEvent::HeartbeatTick);
        }

        let output = obs.encode();
        assert!(output.contains("corvus_heartbeat_ticks_total 3"));
    }

    #[test]
    fn tool_calls_track_success_and_failure_separately() {
        let obs = PrometheusObserver::new();

        obs.record_event(&ObserverEvent::ToolCall {
            tool: "shell".into(),
            duration: Duration::from_millis(10),
            success: true,
        });
        obs.record_event(&ObserverEvent::ToolCall {
            tool: "shell".into(),
            duration: Duration::from_millis(10),
            success: true,
        });
        obs.record_event(&ObserverEvent::ToolCall {
            tool: "shell".into(),
            duration: Duration::from_millis(10),
            success: false,
        });

        let output = obs.encode();
        assert!(output.contains(r#"corvus_tool_calls_total{success="true",tool="shell"} 2"#));
        assert!(output.contains(r#"corvus_tool_calls_total{success="false",tool="shell"} 1"#));
    }

    #[test]
    fn errors_track_by_component() {
        let obs = PrometheusObserver::new();
        obs.record_event(&ObserverEvent::Error {
            component: "provider".into(),
            message: "timeout".into(),
        });
        obs.record_event(&ObserverEvent::Error {
            component: "provider".into(),
            message: "rate limit".into(),
        });
        obs.record_event(&ObserverEvent::Error {
            component: "channels".into(),
            message: "disconnected".into(),
        });

        let output = obs.encode();
        assert!(output.contains(r#"corvus_errors_total{component="provider"} 2"#));
        assert!(output.contains(r#"corvus_errors_total{component="channels"} 1"#));
    }

    #[test]
    fn gauge_reflects_latest_value() {
        let obs = PrometheusObserver::new();
        obs.record_metric(&ObserverMetric::TokensUsed(100));
        obs.record_metric(&ObserverMetric::TokensUsed(200));

        let output = obs.encode();
        assert!(output.contains("corvus_tokens_used_last 200"));
    }

    #[test]
    fn budget_metrics_do_not_expose_sensitive_override_fields() {
        let obs = PrometheusObserver::new();
        obs.record_event(&ObserverEvent::BudgetOverride(BudgetOverrideEvent {
            action: BudgetOverrideAction::Granted,
            actor: "paired-admin-token".into(),
            scope: CostOverrideScope::NextRequest,
            reason: Some("token=super-secret".into()),
            session_id: Some("sess-123".into()),
            previous_state: BudgetState::Exceeded,
            period: Some(UsagePeriod::Day),
            override_id: Some("ovr-123".into()),
            surface: Some("gateway_admin".into()),
        }));

        let output = obs.encode();
        assert!(output.contains("corvus_budget_overrides_total"));
        assert!(!output.contains("paired-admin-token"));
        assert!(!output.contains("super-secret"));
    }
}
