//! Remote-location value objects: the strategy that locates where a project's
//! filesystem and processes actually live (local, SSH, or WSL).

use serde::{Deserialize, Serialize};

use crate::error::DomainError;

/// SSH authentication strategy.
///
/// The domain only models *which* strategy is selected; resolving keys,
/// agents or known-hosts is an infrastructure concern.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum SshAuth {
    /// Use the running SSH agent.
    Agent,
    /// Use a private-key file at the given (remote-machine-agnostic) path.
    Key {
        /// Path to the private key on the local machine.
        path: String,
    },
    /// Interactive / stored password authentication.
    Password,
}

/// Strategy describing where a project is physically located and executed.
///
/// Invariants:
/// - `Ssh.port` ∈ `1..=65535`,
/// - `Ssh.host` / `Ssh.user` / `Ssh.remote_root` non-empty,
/// - `Wsl.distro` non-empty.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum RemoteRef {
    /// The local machine.
    Local,
    /// A remote host reached over SSH.
    #[serde(rename_all = "camelCase")]
    Ssh {
        /// Hostname or IP.
        host: String,
        /// TCP port (`1..=65535`).
        port: u16,
        /// Remote user.
        user: String,
        /// Authentication strategy.
        auth: SshAuth,
        /// Absolute root path on the remote machine.
        remote_root: String,
    },
    /// A Windows Subsystem for Linux distribution.
    Wsl {
        /// Distribution name (e.g. `Ubuntu-22.04`).
        distro: String,
    },
}

impl RemoteRef {
    /// Convenience constructor for the local machine.
    #[must_use]
    pub const fn local() -> Self {
        Self::Local
    }

    /// Validated constructor for an SSH remote.
    ///
    /// # Errors
    /// Returns [`DomainError`] if the port is `0`, or if `host`, `user` or
    /// `remote_root` is empty.
    pub fn ssh(
        host: impl Into<String>,
        port: u16,
        user: impl Into<String>,
        auth: SshAuth,
        remote_root: impl Into<String>,
    ) -> Result<Self, DomainError> {
        let host = host.into();
        let user = user.into();
        let remote_root = remote_root.into();
        crate::validation::non_empty(&host, "ssh.host")?;
        crate::validation::non_empty(&user, "ssh.user")?;
        crate::validation::non_empty(&remote_root, "ssh.remote_root")?;
        // u16 already bounds the upper end; only port 0 is invalid.
        if port == 0 {
            return Err(DomainError::InvalidPort { port: 0 });
        }
        Ok(Self::Ssh {
            host,
            port,
            user,
            auth,
            remote_root,
        })
    }

    /// Validated constructor for a WSL remote.
    ///
    /// # Errors
    /// Returns [`DomainError`] if `distro` is empty.
    pub fn wsl(distro: impl Into<String>) -> Result<Self, DomainError> {
        let distro = distro.into();
        crate::validation::non_empty(&distro, "wsl.distro")?;
        Ok(Self::Wsl { distro })
    }

    /// Returns the coarse kind of this remote, for adapter selection.
    #[must_use]
    pub fn kind(&self) -> RemoteKind {
        match self {
            Self::Local => RemoteKind::Local,
            Self::Ssh { .. } => RemoteKind::Ssh,
            Self::Wsl { .. } => RemoteKind::Wsl,
        }
    }
}

/// Coarse discriminant used by the [`crate::ports::RemoteHost`] port to advertise
/// its kind without exposing connection details.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RemoteKind {
    /// Local execution.
    Local,
    /// SSH remote.
    Ssh,
    /// WSL distribution.
    Wsl,
}
