//! The four terminal use cases (ARCHITECTURE §6, L3).

use std::sync::Arc;

use domain::ports::{EventBus, PtyPort, SpawnSpec};
use domain::{
    DomainEvent, NodeId, ProjectPath, PtySize, SessionId, SessionKind, SessionStatus,
    TerminalSession,
};

use crate::error::AppError;

use super::registry::TerminalSessions;

/// Input for [`OpenTerminal::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenTerminalInput {
    /// Working directory for the shell (absolute path; defaults applied by the
    /// caller — typically the project root).
    pub cwd: String,
    /// Initial terminal height in rows.
    pub rows: u16,
    /// Initial terminal width in columns.
    pub cols: u16,
    /// Command to run. `None` ⇒ the platform default login shell.
    pub command: Option<String>,
    /// Arguments for the command.
    pub args: Vec<String>,
    /// The layout leaf hosting this session. `None` ⇒ a fresh node id (L4 will
    /// thread the real layout node through here).
    pub node_id: Option<NodeId>,
}

/// Output of [`OpenTerminal::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenTerminalOutput {
    /// The created terminal session (its `id` is the [`SessionId`] minted by the
    /// PTY layer and reused everywhere — write/resize/close, the output channel).
    pub session: TerminalSession,
}

/// Opens a PTY in a cwd, creates a [`TerminalSession`], registers the handle.
pub struct OpenTerminal {
    pty: Arc<dyn PtyPort>,
    sessions: Arc<TerminalSessions>,
    events: Arc<dyn EventBus>,
}

impl OpenTerminal {
    /// Builds the use case from its injected ports/services.
    #[must_use]
    pub fn new(
        pty: Arc<dyn PtyPort>,
        sessions: Arc<TerminalSessions>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            pty,
            sessions,
            events,
        }
    }

    /// Executes the open: validate cwd + size, spawn the PTY, snapshot the
    /// session, register the live handle, publish [`DomainEvent::LayoutChanged`].
    ///
    /// # Errors
    /// - [`AppError::Invalid`] for a non-absolute cwd or a zero-sized terminal,
    /// - [`AppError::Process`] if the PTY fails to spawn.
    pub async fn execute(&self, input: OpenTerminalInput) -> Result<OpenTerminalOutput, AppError> {
        let cwd = ProjectPath::new(input.cwd).map_err(|e| AppError::Invalid(e.to_string()))?;
        let size =
            PtySize::new(input.rows, input.cols).map_err(|e| AppError::Invalid(e.to_string()))?;

        let command = input.command.unwrap_or_else(default_shell);
        let spec = SpawnSpec {
            command,
            args: input.args,
            cwd: cwd.clone(),
            env: Vec::new(),
            context_plan: None,
            sandbox: None,
        };

        // The PTY layer owns the session identity; we adopt the returned handle's
        // id as the `TerminalSession.id` (single source of truth, ARCHITECTURE §4).
        let handle = self.pty.spawn(spec, size).await?;
        let session_id = handle.session_id;
        let node_id = input.node_id.unwrap_or_else(NodeId::new_random);

        let mut session =
            TerminalSession::starting(session_id, node_id, cwd, SessionKind::Plain, size);
        session.status = SessionStatus::Running;

        self.sessions.insert(handle, session.clone());

        // Output streaming + per-session Channel wiring happens in the presentation
        // layer (it owns the transport). Announce so the UI can react.
        self.events.publish(DomainEvent::PtyOutput {
            session_id,
            bytes: Vec::new(),
        });

        Ok(OpenTerminalOutput { session })
    }
}

/// Input for [`WriteToTerminal::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteToTerminalInput {
    /// Target session.
    pub session_id: SessionId,
    /// Bytes to write (typically keystrokes from xterm.js).
    pub data: Vec<u8>,
}

/// Forwards bytes (keystrokes) to a live PTY.
pub struct WriteToTerminal {
    pty: Arc<dyn PtyPort>,
    sessions: Arc<TerminalSessions>,
}

impl WriteToTerminal {
    /// Builds the use case.
    #[must_use]
    pub fn new(pty: Arc<dyn PtyPort>, sessions: Arc<TerminalSessions>) -> Self {
        Self { pty, sessions }
    }

    /// Writes to the session's PTY.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the session is unknown,
    /// - [`AppError::Process`] on PTY I/O failure.
    pub fn execute(&self, input: WriteToTerminalInput) -> Result<(), AppError> {
        let handle = self
            .sessions
            .handle(&input.session_id)
            .ok_or_else(|| AppError::NotFound(format!("terminal session {}", input.session_id)))?;
        self.pty.write(&handle, &input.data)?;
        Ok(())
    }
}

/// Input for [`ResizeTerminal::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResizeTerminalInput {
    /// Target session.
    pub session_id: SessionId,
    /// New height in rows.
    pub rows: u16,
    /// New width in columns.
    pub cols: u16,
}

/// Resizes a live PTY.
pub struct ResizeTerminal {
    pty: Arc<dyn PtyPort>,
    sessions: Arc<TerminalSessions>,
}

impl ResizeTerminal {
    /// Builds the use case.
    #[must_use]
    pub fn new(pty: Arc<dyn PtyPort>, sessions: Arc<TerminalSessions>) -> Self {
        Self { pty, sessions }
    }

    /// Resizes the session's PTY.
    ///
    /// # Errors
    /// - [`AppError::Invalid`] for a zero-sized terminal,
    /// - [`AppError::NotFound`] if the session is unknown,
    /// - [`AppError::Process`] on PTY failure.
    pub fn execute(&self, input: ResizeTerminalInput) -> Result<(), AppError> {
        let size =
            PtySize::new(input.rows, input.cols).map_err(|e| AppError::Invalid(e.to_string()))?;
        let handle = self
            .sessions
            .handle(&input.session_id)
            .ok_or_else(|| AppError::NotFound(format!("terminal session {}", input.session_id)))?;
        self.pty.resize(&handle, size)?;
        Ok(())
    }
}

/// Input for [`CloseTerminal::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseTerminalInput {
    /// Target session.
    pub session_id: SessionId,
}

/// Output of [`CloseTerminal::execute`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CloseTerminalOutput {
    /// Exit code reported by the killed process (`None` if signalled).
    pub code: Option<i32>,
}

/// Kills a live PTY and forgets its handle.
pub struct CloseTerminal {
    pty: Arc<dyn PtyPort>,
    sessions: Arc<TerminalSessions>,
}

impl CloseTerminal {
    /// Builds the use case.
    #[must_use]
    pub fn new(pty: Arc<dyn PtyPort>, sessions: Arc<TerminalSessions>) -> Self {
        Self { pty, sessions }
    }

    /// Kills the session's PTY and removes it from the registry. Idempotent on
    /// the registry side (removing an unknown session is a no-op error).
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the session is unknown,
    /// - [`AppError::Process`] if the kill fails.
    pub async fn execute(
        &self,
        input: CloseTerminalInput,
    ) -> Result<CloseTerminalOutput, AppError> {
        let handle = self
            .sessions
            .remove(&input.session_id)
            .ok_or_else(|| AppError::NotFound(format!("terminal session {}", input.session_id)))?;
        let status = self.pty.kill(&handle).await?;
        Ok(CloseTerminalOutput { code: status.code })
    }
}

/// The platform default interactive shell.
///
/// Resolution policy lives in the application layer (a metier default), not the
/// adapter, so it is uniform and testable. On Unix we honour `$SHELL`, falling
/// back to `/bin/sh`; on Windows we use `cmd.exe` (a ConPTY spike point — PowerShell
/// could become the default, ARCHITECTURE §13.1).
fn default_shell() -> String {
    #[cfg(windows)]
    {
        std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_owned())
    }
    #[cfg(not(windows))]
    {
        std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_owned())
    }
}
