//! First-class background command use cases (B8 sub-task 3).
//!
//! These use cases own the *application* side of a command-backed background
//! task: they allocate the [`TaskId`], create the persisted task
//! ([`BackgroundTaskState::Queued`] → [`Running`](BackgroundTaskState::Running))
//! and hand a [`BackgroundTaskSpec`] to the runner. They talk **only** to ports:
//! [`BackgroundTaskStore`], [`BackgroundTaskRunner`], [`Clock`] and
//! [`IdGenerator`], plus the runtime-only [`BackgroundCommandArchive`] abstraction
//! defined below (see its doc for why retry recall is not a domain port).
//!
//! The runner never writes the store; the completion sink does. So beyond the
//! initial create/transition, these use cases do not persist terminal state —
//! except on a *spawn failure*, where they mark the just-created task `Failed` so
//! it never dangles in `Running` with no worker behind it.

use std::sync::Arc;

use domain::ports::{
    BackgroundTaskPortError, BackgroundTaskRunner, BackgroundTaskSpec, BackgroundTaskStore, Clock,
    IdGenerator, SpawnSpec,
};
use domain::{
    AgentId, BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskState,
    BackgroundTaskWakePolicy, ProjectId, TaskId,
};

use crate::error::AppError;

/// Maps a background-task port error into the application error shape.
fn map_port_err(err: BackgroundTaskPortError) -> AppError {
    match err {
        BackgroundTaskPortError::NotFound => AppError::NotFound("background task".to_owned()),
        BackgroundTaskPortError::AlreadyExists => {
            AppError::Invalid("background task already exists".to_owned())
        }
        BackgroundTaskPortError::Invalid(msg) => AppError::Invalid(msg),
        BackgroundTaskPortError::Runner(msg) => AppError::Process(msg),
        BackgroundTaskPortError::Store(msg) => AppError::Store(msg),
    }
}

/// Input for [`SpawnBackgroundCommand`].
#[derive(Debug, Clone)]
pub struct SpawnBackgroundCommandInput {
    /// Owning project.
    pub project_id: ProjectId,
    /// Agent that owns completion delivery.
    pub owner_agent_id: AgentId,
    /// Human-facing label.
    pub label: String,
    /// Command invocation to run.
    pub command: SpawnSpec,
    /// Completion wake policy.
    pub wake_policy: BackgroundTaskWakePolicy,
    /// Optional absolute deadline, epoch milliseconds.
    pub deadline_ms: Option<u64>,
}

/// Output of [`SpawnBackgroundCommand`] / [`RetryBackgroundTask`].
#[derive(Debug, Clone)]
pub struct SpawnBackgroundCommandOutput {
    /// The created task, in its `Running` state.
    pub task: BackgroundTask,
}

/// Creates a command-backed background task and hands it to the runner.
pub struct SpawnBackgroundCommand {
    store: Arc<dyn BackgroundTaskStore>,
    runner: Arc<dyn BackgroundTaskRunner>,
    clock: Arc<dyn Clock>,
    ids: Arc<dyn IdGenerator>,
}

impl SpawnBackgroundCommand {
    /// Wires the use case to its ports.
    #[must_use]
    pub fn new(
        store: Arc<dyn BackgroundTaskStore>,
        runner: Arc<dyn BackgroundTaskRunner>,
        clock: Arc<dyn Clock>,
        ids: Arc<dyn IdGenerator>,
    ) -> Self {
        Self {
            store,
            runner,
            clock,
            ids,
        }
    }

    /// Allocates a task id, persists it (`Queued`→`Running`) and spawns it.
    ///
    /// # Errors
    /// [`AppError`] if a store/runner operation or a domain invariant fails.
    pub async fn execute(
        &self,
        input: SpawnBackgroundCommandInput,
    ) -> Result<SpawnBackgroundCommandOutput, AppError> {
        let task = self
            .create_and_run(
                input.project_id,
                input.owner_agent_id,
                input.label,
                input.command,
                input.wake_policy,
                input.deadline_ms,
            )
            .await?;
        Ok(SpawnBackgroundCommandOutput { task })
    }

    /// Shared create/transition/spawn path, reused by retry.
    async fn create_and_run(
        &self,
        project_id: ProjectId,
        owner_agent_id: AgentId,
        label: String,
        command: SpawnSpec,
        wake_policy: BackgroundTaskWakePolicy,
        deadline_ms: Option<u64>,
    ) -> Result<BackgroundTask, AppError> {
        let task_id = TaskId::from_uuid(self.ids.new_uuid());
        let now = u64::try_from(self.clock.now_millis().max(0)).unwrap_or(0);

        let task = BackgroundTask::new(
            task_id,
            project_id,
            owner_agent_id,
            BackgroundTaskKind::Command { label },
            wake_policy,
            now,
            deadline_ms,
        )
        .map_err(|e| AppError::Invalid(e.to_string()))?;
        self.store.create(&task).await.map_err(map_port_err)?;

        let running = task
            .transition(BackgroundTaskState::Running, now)
            .map_err(|e| AppError::Invalid(e.to_string()))?;
        self.store.save(&running).await.map_err(map_port_err)?;

        let spec = BackgroundTaskSpec {
            task_id,
            project_id,
            owner_agent_id,
            kind: running.kind.clone(),
            wake_policy,
            command: Some(command),
            deadline_ms,
        };

        if let Err(err) = self.runner.spawn(spec).await {
            // Never leave the task dangling in `Running` with no worker: mark it
            // Failed so reconcile/UI see a terminal state. Best-effort persist.
            if let Ok(failed) = running.complete(BackgroundTaskResult::Failure {
                finished_at_ms: now,
                exit_code: None,
                error: format!("failed to spawn background command: {err}"),
                stdout_tail: None,
                stderr_tail: None,
            }) {
                let _ = self.store.save(&failed).await;
            }
            return Err(map_port_err(err));
        }

        Ok(running)
    }
}

/// Output of [`CancelBackgroundTask`].
#[derive(Debug, Clone)]
pub struct CancelBackgroundTaskOutput {
    /// The task as currently persisted, if it still exists. The terminal
    /// `Cancelled` state is written by the completion sink, not here.
    pub task: Option<BackgroundTask>,
}

/// Requests cancellation of a running background task.
pub struct CancelBackgroundTask {
    store: Arc<dyn BackgroundTaskStore>,
    runner: Arc<dyn BackgroundTaskRunner>,
}

impl CancelBackgroundTask {
    /// Wires the use case to its ports.
    #[must_use]
    pub fn new(store: Arc<dyn BackgroundTaskStore>, runner: Arc<dyn BackgroundTaskRunner>) -> Self {
        Self { store, runner }
    }

    /// Signals the runner to cancel `task_id` (idempotent).
    ///
    /// # Errors
    /// [`AppError`] if the runner or store fails.
    pub async fn execute(&self, task_id: TaskId) -> Result<CancelBackgroundTaskOutput, AppError> {
        self.runner.cancel(task_id).await.map_err(map_port_err)?;
        let task = self.store.get(task_id).await.map_err(map_port_err)?;
        Ok(CancelBackgroundTaskOutput { task })
    }
}

/// Read-only recall of the command invocation a background task was spawned with.
///
/// This is a **runtime, in-memory** abstraction, *not* a domain port: the recall
/// lives wherever the runner keeps its live invocations for the current session
/// and is deliberately **never persisted** (a [`SpawnSpec`] can carry secrets and
/// must never land in `.ideai/background-tasks/*.json`, which travels with the
/// project). Retry is therefore **session-scoped** in V1 — a task whose runner no
/// longer remembers its spec (e.g. after a restart) simply cannot be retried.
///
/// The persisted [`BackgroundTask`] carries no [`SpawnSpec`] (the command line is
/// an execution detail, not domain state). Retry, however, must re-run the *same*
/// command under a fresh task id, so it recovers the original invocation from
/// whatever component still holds it — the runner. Keeping that recall behind this
/// application-level trait lets [`RetryBackgroundTask`] stay decoupled from the
/// concrete runner without dragging the concept into the domain.
pub trait BackgroundCommandArchive: Send + Sync {
    /// Returns the [`SpawnSpec`] a command-backed task was spawned with, if the
    /// runner still remembers it. `None` when the task is unknown, was not
    /// command-backed, or is no longer in the current session's registry.
    fn spec_for(&self, task_id: TaskId) -> Option<SpawnSpec>;
}

/// Re-runs a terminal command-backed task under a brand-new task id.
///
/// The original command is recovered from the [`BackgroundCommandArchive`]; the
/// deadline is intentionally dropped (an absolute past deadline would expire the
/// retry instantly).
pub struct RetryBackgroundTask {
    store: Arc<dyn BackgroundTaskStore>,
    archive: Arc<dyn BackgroundCommandArchive>,
    spawn: Arc<SpawnBackgroundCommand>,
}

impl RetryBackgroundTask {
    /// Wires the use case to its ports and the spawn use case.
    #[must_use]
    pub fn new(
        store: Arc<dyn BackgroundTaskStore>,
        archive: Arc<dyn BackgroundCommandArchive>,
        spawn: Arc<SpawnBackgroundCommand>,
    ) -> Self {
        Self {
            store,
            archive,
            spawn,
        }
    }

    /// Retries `task_id`, returning the freshly-created task.
    ///
    /// # Errors
    /// [`AppError::NotFound`] if the task is unknown, [`AppError::Invalid`] if it
    /// is not a terminal command task or its command is no longer available.
    pub async fn execute(&self, task_id: TaskId) -> Result<SpawnBackgroundCommandOutput, AppError> {
        let old = self
            .store
            .get(task_id)
            .await
            .map_err(map_port_err)?
            .ok_or_else(|| AppError::NotFound(format!("background task {task_id}")))?;

        if !old.is_terminal() {
            return Err(AppError::Invalid(
                "cannot retry a task that is still running".to_owned(),
            ));
        }
        let label = match &old.kind {
            BackgroundTaskKind::Command { label } => label.clone(),
            _ => {
                return Err(AppError::Invalid(
                    "only command-backed tasks can be retried".to_owned(),
                ))
            }
        };
        let command = self.archive.spec_for(task_id).ok_or_else(|| {
            AppError::Invalid("original command is no longer available for retry".to_owned())
        })?;

        let task = self
            .spawn
            .create_and_run(
                old.project_id,
                old.owner_agent_id,
                label,
                command,
                old.wake_policy,
                None,
            )
            .await?;
        Ok(SpawnBackgroundCommandOutput { task })
    }
}
