// API state - Shared application state for Axum handlers

use std::sync::Arc;

use vapora_channels::ChannelRegistry;
use vapora_rlm::storage::SurrealDBStorage;
use vapora_rlm::RLMEngine;
use vapora_workflow_engine::{ScheduleStore, WorkflowOrchestrator};

use crate::config::NotificationConfig;
use crate::services::{
    AgentService, ProjectService, ProposalService, ProviderAnalyticsService, TaskService,
    WorkflowService,
};

/// Application state shared across all API handlers
#[derive(Clone)]
pub struct AppState {
    pub project_service: Arc<ProjectService>,
    pub task_service: Arc<TaskService>,
    pub agent_service: Arc<AgentService>,
    pub proposal_service: Arc<ProposalService>,
    pub provider_analytics_service: Arc<ProviderAnalyticsService>,
    pub workflow_orchestrator: Option<Arc<WorkflowOrchestrator>>,
    pub workflow_service: Option<Arc<WorkflowService>>,
    pub rlm_engine: Option<Arc<RLMEngine<SurrealDBStorage>>>,
    pub schedule_store: Option<Arc<ScheduleStore>>,
    /// Outbound notification channels; `None` when `[channels]` is absent from
    /// config.
    pub channel_registry: Option<Arc<ChannelRegistry>>,
    /// Backend-level event → channel-name mappings.
    pub notification_config: Arc<NotificationConfig>,
}

impl AppState {
    /// Create a new AppState instance
    pub fn new(
        project_service: ProjectService,
        task_service: TaskService,
        agent_service: AgentService,
        proposal_service: ProposalService,
        provider_analytics_service: ProviderAnalyticsService,
    ) -> Self {
        Self {
            project_service: Arc::new(project_service),
            task_service: Arc::new(task_service),
            agent_service: Arc::new(agent_service),
            proposal_service: Arc::new(proposal_service),
            provider_analytics_service: Arc::new(provider_analytics_service),
            workflow_orchestrator: None,
            workflow_service: None,
            rlm_engine: None,
            schedule_store: None,
            channel_registry: None,
            notification_config: Arc::new(NotificationConfig::default()),
        }
    }

    /// Attach the workflow service for `/api/v1/workflows/*` handlers.
    pub fn with_workflow_service(mut self, svc: Arc<WorkflowService>) -> Self {
        self.workflow_service = Some(svc);
        self
    }

    /// Add workflow orchestrator to state
    #[allow(dead_code)]
    pub fn with_workflow_orchestrator(mut self, orchestrator: Arc<WorkflowOrchestrator>) -> Self {
        self.workflow_orchestrator = Some(orchestrator);
        self
    }

    /// Add RLM engine to state
    pub fn with_rlm_engine(mut self, rlm_engine: Arc<RLMEngine<SurrealDBStorage>>) -> Self {
        self.rlm_engine = Some(rlm_engine);
        self
    }

    /// Add schedule store to state
    pub fn with_schedule_store(mut self, store: Arc<ScheduleStore>) -> Self {
        self.schedule_store = Some(store);
        self
    }

    /// Attach the notification channel registry built from `[channels]` config.
    pub fn with_channel_registry(mut self, registry: Arc<ChannelRegistry>) -> Self {
        self.channel_registry = Some(registry);
        self
    }

    /// Attach the per-event notification targets.
    pub fn with_notification_config(mut self, cfg: NotificationConfig) -> Self {
        self.notification_config = Arc::new(cfg);
        self
    }

    /// Fire-and-forget: send `msg` to each channel in `targets`.
    ///
    /// Spawns a background task; delivery failures are logged as `warn!` and
    /// never surface to the caller.
    pub fn notify(&self, targets: &[String], msg: vapora_channels::Message) {
        if targets.is_empty() {
            return;
        }
        let registry = self.channel_registry.clone();
        let targets = targets.to_vec();
        tokio::spawn(dispatch_notifications(registry, targets, msg));
    }
}

/// Deliver `msg` to every channel name in `targets` using `registry`.
///
/// A `None` registry or an unknown channel name is silent (warn-logged for
/// unknown names). Failures in one channel do not abort delivery to others.
///
/// Extracted from [`AppState::notify`] to be directly callable in tests
/// without needing a fully-constructed [`AppState`].
pub(crate) async fn dispatch_notifications(
    registry: Option<Arc<ChannelRegistry>>,
    targets: Vec<String>,
    msg: vapora_channels::Message,
) {
    let Some(registry) = registry else {
        return;
    };
    for name in &targets {
        if let Err(e) = registry.send(name, msg.clone()).await {
            tracing::warn!(channel = %name, error = %e, "Notification delivery failed");
        }
    }
}

#[cfg(test)]
mod tests {
    use std::sync::{Arc, Mutex};

    use async_trait::async_trait;
    use vapora_channels::Result as ChannelResult;
    use vapora_channels::{ChannelError, ChannelRegistry, Message, NotificationChannel};

    use super::dispatch_notifications;

    struct RecordingChannel {
        name: String,
        captured: Arc<Mutex<Vec<Message>>>,
    }

    impl RecordingChannel {
        fn new(name: &str) -> (Self, Arc<Mutex<Vec<Message>>>) {
            let captured = Arc::new(Mutex::new(vec![]));
            (
                Self {
                    name: name.to_string(),
                    captured: Arc::clone(&captured),
                },
                captured,
            )
        }
    }

    #[async_trait]
    impl NotificationChannel for RecordingChannel {
        fn name(&self) -> &str {
            &self.name
        }
        async fn send(&self, msg: &Message) -> ChannelResult<()> {
            self.captured.lock().unwrap().push(msg.clone());
            Ok(())
        }
    }

    struct FailingChannel {
        name: String,
    }

    #[async_trait]
    impl NotificationChannel for FailingChannel {
        fn name(&self) -> &str {
            &self.name
        }
        async fn send(&self, _msg: &Message) -> ChannelResult<()> {
            Err(ChannelError::ApiError {
                channel: self.name.clone(),
                status: 503,
                body: "unavailable".to_string(),
            })
        }
    }

    #[tokio::test]
    async fn dispatch_is_noop_when_registry_is_none() {
        // Must not panic; targets are non-empty so the only short-circuit is None
        // registry.
        dispatch_notifications(None, vec!["ch".to_string()], Message::info("Test", "body")).await;
    }

    #[tokio::test]
    async fn dispatch_delivers_to_named_channel() {
        let (recording, captured) = RecordingChannel::new("team-slack");
        let mut registry = ChannelRegistry::new();
        registry.register(Arc::new(recording));

        dispatch_notifications(
            Some(Arc::new(registry)),
            vec!["team-slack".to_string()],
            Message::success("Deploy done", "v1.0 → prod"),
        )
        .await;

        let msgs = captured.lock().unwrap();
        assert_eq!(msgs.len(), 1);
        assert_eq!(msgs[0].title, "Deploy done");
    }

    #[tokio::test]
    async fn dispatch_delivers_to_multiple_targets() {
        let (ch_a, cap_a) = RecordingChannel::new("ch-a");
        let (ch_b, cap_b) = RecordingChannel::new("ch-b");
        let mut registry = ChannelRegistry::new();
        registry.register(Arc::new(ch_a));
        registry.register(Arc::new(ch_b));

        dispatch_notifications(
            Some(Arc::new(registry)),
            vec!["ch-a".to_string(), "ch-b".to_string()],
            Message::info("Test", "broadcast"),
        )
        .await;

        assert_eq!(cap_a.lock().unwrap().len(), 1);
        assert_eq!(cap_b.lock().unwrap().len(), 1);
    }

    #[tokio::test]
    async fn dispatch_continues_after_channel_failure() {
        let bad = FailingChannel {
            name: "bad".to_string(),
        };
        let (good, cap_good) = RecordingChannel::new("good");
        let mut registry = ChannelRegistry::new();
        registry.register(Arc::new(bad));
        registry.register(Arc::new(good));

        // Must not panic; "good" receives despite "bad" returning an error.
        dispatch_notifications(
            Some(Arc::new(registry)),
            vec!["bad".to_string(), "good".to_string()],
            Message::error("Alert", "system down"),
        )
        .await;

        assert_eq!(cap_good.lock().unwrap().len(), 1);
    }

    #[tokio::test]
    async fn dispatch_logs_warn_on_unknown_channel_but_continues() {
        let (present, cap) = RecordingChannel::new("present");
        let mut registry = ChannelRegistry::new();
        registry.register(Arc::new(present));

        // "missing" → ChannelError::NotFound logged as warn; "present" still
        // receives its message.
        dispatch_notifications(
            Some(Arc::new(registry)),
            vec!["missing".to_string(), "present".to_string()],
            Message::info("Test", "body"),
        )
        .await;

        assert_eq!(cap.lock().unwrap().len(), 1);
    }
}
