//! Two-task, cancellation-safe orchestration skeleton.

use crate::backend::{BackendError, GridBackend, OfflineGridBackend};
use crate::config::{AgentConfig, ConfigError, OperatingMode};
use crate::types::{
    BoundedText, ControlCommand, GridEventKind, MAX_OBSERVABLE_DETAIL_BYTES, ObservableEvent,
};
use libremetaverse_types::compat::CancellationTokenSource;
use std::error::Error;
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;

const OWNED_TASKS: usize = 2;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum ServiceState {
    Starting = 0,
    Running = 1,
    Paused = 2,
    Stopping = 3,
    Stopped = 4,
    Failed = 5,
}

impl ServiceState {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Starting => "starting",
            Self::Running => "running",
            Self::Paused => "paused",
            Self::Stopping => "stopping",
            Self::Stopped => "stopped",
            Self::Failed => "failed",
        }
    }

    fn from_atomic(value: u8) -> Self {
        match value {
            0 => Self::Starting,
            1 => Self::Running,
            2 => Self::Paused,
            3 => Self::Stopping,
            4 => Self::Stopped,
            _ => Self::Failed,
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ServiceError {
    Configuration(ConfigError),
    Backend(BackendError),
    ControlQueueClosed,
    ObservableQueueClosed,
    TaskPanicked,
    ShutdownTimedOut { task: &'static str },
}

impl fmt::Display for ServiceError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Configuration(error) => write!(formatter, "configuration rejected: {error}"),
            Self::Backend(error) => write!(formatter, "grid backend failed: {error}"),
            Self::ControlQueueClosed => formatter.write_str("control queue is closed"),
            Self::ObservableQueueClosed => formatter.write_str("observable queue is closed"),
            Self::TaskPanicked => formatter.write_str("owned task panicked or was cancelled"),
            Self::ShutdownTimedOut { task } => {
                write!(formatter, "timed out while joining owned {task} task")
            }
        }
    }
}

impl Error for ServiceError {}

impl From<ConfigError> for ServiceError {
    fn from(value: ConfigError) -> Self {
        Self::Configuration(value)
    }
}

impl From<BackendError> for ServiceError {
    fn from(value: BackendError) -> Self {
        Self::Backend(value)
    }
}

/// Validated service composition. Construction and validation are inert;
/// `start` is the only task-creation point.
pub struct AgentService {
    config: AgentConfig,
    backend: Arc<dyn GridBackend>,
}

impl AgentService {
    #[must_use]
    pub fn new(config: AgentConfig, backend: Arc<dyn GridBackend>) -> Self {
        Self { config, backend }
    }

    /// Composes the deterministic offline backend.
    ///
    /// # Errors
    ///
    /// Rejects non-offline configuration.
    pub fn offline(config: AgentConfig) -> Result<Self, ServiceError> {
        if config.mode != OperatingMode::OfflineFake {
            return Err(ConfigError::Missing {
                field: "offline mode",
                required_for: "the offline backend",
            }
            .into());
        }
        Ok(Self::new(config, Arc::new(OfflineGridBackend::new())))
    }

    /// Validates all invariants, creates three bounded queues, and starts the
    /// exactly two configured owner tasks.
    ///
    /// # Errors
    ///
    /// Returns before task creation for invalid configuration.
    pub fn start(self) -> Result<ServiceHandle, ServiceError> {
        self.config.validate()?;
        if self.config.limits.max_background_tasks != OWNED_TASKS {
            return Err(ConfigError::UnsafeLimit {
                field: "limits.max_background_tasks",
                value: self.config.limits.max_background_tasks,
                minimum: OWNED_TASKS,
                maximum: OWNED_TASKS,
            }
            .into());
        }

        let (grid_sender, grid_receiver) = mpsc::channel(self.config.limits.grid_event_queue);
        let (control_sender, control_receiver) = mpsc::channel(self.config.limits.control_queue);
        let (observable_sender, observable_receiver) =
            mpsc::channel(self.config.limits.observable_queue);
        let cancellation = CancellationTokenSource::new();
        let state = Arc::new(AtomicU8::new(ServiceState::Starting as u8));
        let active_tasks = Arc::new(AtomicUsize::new(0));

        let backend = Arc::clone(&self.backend);
        let backend_cancellation = cancellation.clone();
        let backend_observable = observable_sender.clone();
        let backend_guard = TaskCountGuard::new(Arc::clone(&active_tasks));
        let backend_state = Arc::clone(&state);
        let backend_task = tokio::spawn(async move {
            let _task_guard = backend_guard;
            let result = backend.run(grid_sender, backend_cancellation.token()).await;
            if let Err(error) = &result {
                if let Ok(diagnostic) = BoundedText::<MAX_OBSERVABLE_DETAIL_BYTES>::new(
                    "observable.backend_failure",
                    "grid backend stopped with a typed error",
                ) {
                    let _ = send_observable(
                        &backend_observable,
                        ObservableEvent::Diagnostic { detail: diagnostic },
                        &backend_cancellation,
                    )
                    .await;
                }
                backend_state.store(ServiceState::Failed as u8, Ordering::Release);
                backend_cancellation.cancel();
                return Err(ServiceError::Backend(error.clone()));
            }
            Ok(())
        });

        let coordinator_cancellation = cancellation.clone();
        let coordinator_state = Arc::clone(&state);
        let coordinator_guard = TaskCountGuard::new(Arc::clone(&active_tasks));
        let coordinator_task = tokio::spawn(async move {
            let _task_guard = coordinator_guard;
            coordinator_loop(
                grid_receiver,
                control_receiver,
                observable_sender,
                coordinator_cancellation,
                coordinator_state,
            )
            .await
        });

        Ok(ServiceHandle {
            control_sender,
            observable_receiver,
            cancellation,
            tasks: [Some(backend_task), Some(coordinator_task)],
            state,
            active_tasks,
            shutdown_timeout: self.config.timeouts.shutdown,
        })
    }
}

/// Exclusive owner of service controls, observations, cancellation, and both tasks.
pub struct ServiceHandle {
    control_sender: mpsc::Sender<ControlCommand>,
    observable_receiver: mpsc::Receiver<ObservableEvent>,
    cancellation: CancellationTokenSource,
    tasks: [Option<JoinHandle<Result<(), ServiceError>>>; OWNED_TASKS],
    state: Arc<AtomicU8>,
    active_tasks: Arc<AtomicUsize>,
    shutdown_timeout: Duration,
}

impl ServiceHandle {
    #[must_use]
    pub fn state(&self) -> ServiceState {
        ServiceState::from_atomic(self.state.load(Ordering::Acquire))
    }

    #[must_use]
    pub fn active_task_count(&self) -> usize {
        self.active_tasks.load(Ordering::Acquire)
    }

    /// Applies backpressure at the bounded coordinator-owned control queue.
    ///
    /// # Errors
    ///
    /// Returns when the service has stopped accepting control commands.
    pub async fn command(&self, command: ControlCommand) -> Result<(), ServiceError> {
        self.control_sender
            .send(command)
            .await
            .map_err(|_| ServiceError::ControlQueueClosed)
    }

    /// Receives the next event from the handle-owned bounded observation queue.
    pub async fn next_event(&mut self) -> Option<ObservableEvent> {
        self.observable_receiver.recv().await
    }

    /// Requests cancellation and joins backend first, then coordinator. If a
    /// task exceeds its bound it is aborted and still awaited before return.
    ///
    /// # Errors
    ///
    /// Returns the first backend, panic, or timeout error after both task slots
    /// have been reclaimed.
    pub async fn shutdown(&mut self) -> Result<(), ServiceError> {
        self.state
            .store(ServiceState::Stopping as u8, Ordering::Release);
        self.cancellation.cancel();
        let names = ["backend", "coordinator"];
        let mut first_error = None;
        for (slot, name) in self.tasks.iter_mut().zip(names) {
            let Some(task) = slot.as_mut() else {
                continue;
            };
            let task_error = match tokio::time::timeout(self.shutdown_timeout, &mut *task).await {
                Ok(Ok(Ok(()))) => None,
                Ok(Ok(Err(error))) => Some(error),
                Ok(Err(_)) => Some(ServiceError::TaskPanicked),
                Err(_) => {
                    task.abort();
                    let _ = (&mut *task).await;
                    Some(ServiceError::ShutdownTimedOut { task: name })
                }
            };
            *slot = None;
            if first_error.is_none() {
                first_error = task_error;
            }
        }
        self.state.store(
            if first_error.is_some() {
                ServiceState::Failed as u8
            } else {
                ServiceState::Stopped as u8
            },
            Ordering::Release,
        );
        first_error.map_or(Ok(()), Err)
    }
}

impl Drop for ServiceHandle {
    fn drop(&mut self) {
        self.cancellation.cancel();
        for task in self.tasks.iter().flatten() {
            task.abort();
        }
    }
}

struct TaskCountGuard(Arc<AtomicUsize>);

impl TaskCountGuard {
    fn new(counter: Arc<AtomicUsize>) -> Self {
        counter.fetch_add(1, Ordering::AcqRel);
        Self(counter)
    }
}

impl Drop for TaskCountGuard {
    fn drop(&mut self) {
        self.0.fetch_sub(1, Ordering::AcqRel);
    }
}

async fn coordinator_loop(
    mut grid_events: mpsc::Receiver<crate::types::GridEvent>,
    mut controls: mpsc::Receiver<ControlCommand>,
    observable: mpsc::Sender<ObservableEvent>,
    cancellation: CancellationTokenSource,
    state: Arc<AtomicU8>,
) -> Result<(), ServiceError> {
    send_state(&observable, ServiceState::Starting, &cancellation).await?;
    loop {
        tokio::select! {
            () = cancellation.token().cancelled() => break,
            control = controls.recv() => match control {
                Some(ControlCommand::Pause) => {
                    state.store(ServiceState::Paused as u8, Ordering::Release);
                    send_state(&observable, ServiceState::Paused, &cancellation).await?;
                }
                Some(ControlCommand::Resume | ControlCommand::ReloadBehavior) => {
                    state.store(ServiceState::Running as u8, Ordering::Release);
                    send_state(&observable, ServiceState::Running, &cancellation).await?;
                }
                Some(ControlCommand::Shutdown) | None => {
                    cancellation.cancel();
                    break;
                }
            },
            event = grid_events.recv() => if let Some(event) = event {
                    if event.kind == GridEventKind::BackendReady {
                        state.store(ServiceState::Running as u8, Ordering::Release);
                        send_state(&observable, ServiceState::Running, &cancellation).await?;
                    }
                    send_observable(&observable, ObservableEvent::Grid(event), &cancellation).await?;
                } else {
                    cancellation.cancel();
                    break;
                }
        }
    }
    if ServiceState::from_atomic(state.load(Ordering::Acquire)) != ServiceState::Failed {
        state.store(ServiceState::Stopped as u8, Ordering::Release);
    }
    Ok(())
}

async fn send_state(
    observable: &mpsc::Sender<ObservableEvent>,
    state: ServiceState,
    cancellation: &CancellationTokenSource,
) -> Result<(), ServiceError> {
    send_observable(
        observable,
        ObservableEvent::StateChanged {
            state: state.as_str(),
        },
        cancellation,
    )
    .await
}

async fn send_observable(
    observable: &mpsc::Sender<ObservableEvent>,
    event: ObservableEvent,
    cancellation: &CancellationTokenSource,
) -> Result<(), ServiceError> {
    tokio::select! {
        () = cancellation.token().cancelled() => Ok(()),
        result = observable.send(event) => result.map_err(|_| ServiceError::ObservableQueueClosed),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::backend::BackendFuture;
    use crate::config::AgentConfig;
    use crate::types::GridEvent;
    use libremetaverse_types::compat::CancellationToken;

    fn offline_config() -> AgentConfig {
        AgentConfig::offline("https://llm.example.invalid/chat", "test-key")
            .expect("valid offline config")
    }

    #[tokio::test]
    async fn offline_skeleton_starts_and_shuts_down_without_orphan_tasks() {
        let mut handle = AgentService::offline(offline_config())
            .expect("offline composition")
            .start()
            .expect("service starts");
        assert_eq!(handle.active_task_count(), OWNED_TASKS);

        let mut saw_ready = false;
        for _ in 0..3 {
            let event = tokio::time::timeout(Duration::from_secs(1), handle.next_event())
                .await
                .expect("observable event arrives")
                .expect("observable queue remains open");
            if matches!(
                event,
                ObservableEvent::Grid(crate::types::GridEvent {
                    kind: GridEventKind::BackendReady,
                    ..
                })
            ) {
                saw_ready = true;
                break;
            }
        }
        assert!(saw_ready);
        handle.shutdown().await.expect("ordered shutdown");
        assert_eq!(handle.state(), ServiceState::Stopped);
        assert_eq!(handle.active_task_count(), 0);
    }

    #[tokio::test]
    async fn pause_resume_and_shutdown_controls_are_bounded_and_observable() {
        let mut handle = AgentService::offline(offline_config())
            .expect("offline composition")
            .start()
            .expect("service starts");
        handle
            .command(ControlCommand::Pause)
            .await
            .expect("pause queued");
        let mut paused = false;
        for _ in 0..5 {
            if matches!(
                handle.next_event().await,
                Some(ObservableEvent::StateChanged { state: "paused" })
            ) {
                paused = true;
                break;
            }
        }
        assert!(paused);
        handle
            .command(ControlCommand::Resume)
            .await
            .expect("resume queued");
        handle.shutdown().await.expect("clean shutdown");
    }

    #[tokio::test]
    async fn dropping_handle_cancels_and_aborts_owned_tasks() {
        let handle = AgentService::offline(offline_config())
            .expect("offline composition")
            .start()
            .expect("service starts");
        let counter = Arc::clone(&handle.active_tasks);
        drop(handle);
        tokio::time::timeout(Duration::from_secs(1), async {
            while counter.load(Ordering::Acquire) != 0 {
                tokio::task::yield_now().await;
            }
        })
        .await
        .expect("aborted task guards complete");
    }

    struct StubbornBackend;

    impl GridBackend for StubbornBackend {
        fn name(&self) -> &'static str {
            "stubborn-test-backend"
        }

        fn run(
            &self,
            _events: mpsc::Sender<GridEvent>,
            _cancellation: CancellationToken,
        ) -> BackendFuture<'_, Result<(), BackendError>> {
            Box::pin(async {
                std::future::pending::<()>().await;
                Ok(())
            })
        }
    }

    #[tokio::test]
    async fn cancelled_shutdown_future_keeps_join_handles_owned() {
        let mut handle = AgentService::new(offline_config(), Arc::new(StubbornBackend))
            .start()
            .expect("service starts");
        let counter = Arc::clone(&handle.active_tasks);
        let mut shutdown = Box::pin(handle.shutdown());
        tokio::select! {
            result = &mut shutdown => panic!("stubborn backend unexpectedly stopped: {result:?}"),
            () = tokio::task::yield_now() => {}
        }
        drop(shutdown);
        assert!(handle.tasks.iter().all(Option::is_some));
        assert!(matches!(
            handle.state(),
            ServiceState::Stopping | ServiceState::Stopped
        ));
        drop(handle);
        tokio::time::timeout(Duration::from_secs(1), async {
            while counter.load(Ordering::Acquire) != 0 {
                tokio::task::yield_now().await;
            }
        })
        .await
        .expect("handle drop reclaims tasks retained after shutdown cancellation");
    }
}
