use std::{
    fmt,
    io::{self, Read},
    path::PathBuf,
};

/// Specification for generating a script that emits bytes to stdout.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbedSpec {
    /// Inputs to concatenate into the generated embed script.
    ///
    /// If this list is empty, [`crate::embed::write_embed`] reads from stdin once.
    pub inputs: Vec<InputSource>,
}

/// Specification for generating a self-extracting bundle script.
///
/// The generic reader type is intentionally exposed. Most users can let it
/// infer to a concrete reader type such as `File`; users that need mixed reader
/// types can choose `R = dyn Read` and cast individual readers to
/// `&mut dyn Read`.
pub struct BundleSpec<'a, R: Read + ?Sized = io::Empty> {
    /// Policy used by the generated script to remove its work directory.
    pub cleanup: CleanupPolicy,
    /// Whether the generated script should reuse a content-derived directory.
    pub shared: bool,
    /// Archive inputs to embed, extracted in the order listed here.
    pub archives: Vec<ArchiveSource<'a, R>>,
    /// Environment variables exported before running the inner command.
    pub env: Vec<EnvAssignment>,
    /// Command executed by the generated script after extraction.
    pub command: InnerCommand,
}

impl<R: Read + ?Sized> fmt::Debug for BundleSpec<'_, R> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("BundleSpec")
            .field("cleanup", &self.cleanup)
            .field("shared", &self.shared)
            .field("archives", &self.archives)
            .field("env", &self.env)
            .field("command", &self.command)
            .finish()
    }
}

/// Archive input for a bundle script.
pub enum ArchiveSource<'a, R: Read + ?Sized = io::Empty> {
    /// Read an archive from a filesystem path while generating the script.
    Path(PathBuf),
    /// Read an archive from an arbitrary reader while generating the script.
    Reader {
        /// Human-readable name used in diagnostics and shared identity context.
        label: String,
        /// Reader that provides archive bytes.
        reader: &'a mut R,
    },
}

impl<'a, R: Read + ?Sized> ArchiveSource<'a, R> {
    /// Creates an archive source from a filesystem path.
    pub fn path(path: impl Into<PathBuf>) -> Self {
        Self::Path(path.into())
    }

    /// Creates an archive source from a reader and a diagnostic label.
    pub fn reader(label: impl Into<String>, reader: &'a mut R) -> Self {
        Self::Reader {
            label: label.into(),
            reader,
        }
    }
}

impl<R: Read + ?Sized> fmt::Debug for ArchiveSource<'_, R> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Path(path) => f.debug_tuple("Path").field(path).finish(),
            Self::Reader { label, .. } => f
                .debug_struct("Reader")
                .field("label", label)
                .field("reader", &"<reader>")
                .finish(),
        }
    }
}

/// Input for an embed script.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputSource {
    /// Read input from a filesystem path while generating the script.
    Path(PathBuf),
    /// Read input from stdin while generating the script.
    Stdin,
}

/// Environment variable assignment embedded into a bundle script.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvAssignment {
    /// Variable name. It must already be validated as a POSIX shell name.
    pub name: String,
    /// Variable value exported by the generated script.
    pub value: String,
}

/// Inner command embedded into a bundle script.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InnerCommand {
    /// Program name or path.
    pub program: String,
    /// Arguments supplied before runtime arguments.
    pub args: Vec<String>,
}

/// Cleanup behavior for the generated bundle script.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CleanupPolicy {
    /// Remove the work directory only when the inner command succeeds.
    Success,
    /// Remove the work directory only when the inner command fails.
    Failure,
    /// Always remove the work directory.
    Always,
    /// Never remove the work directory.
    Never,
}

impl CleanupPolicy {
    /// Returns the shell literal used inside generated scripts.
    pub fn as_shell_value(self) -> &'static str {
        match self {
            Self::Success => "success",
            Self::Failure => "failure",
            Self::Always => "always",
            Self::Never => "never",
        }
    }
}

/// Runtime flags reserved by generated bundle scripts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeFlag {
    /// Extract the bundle, print the work directory, and skip the inner command.
    Extract,
    /// Keep the work directory regardless of cleanup policy.
    NoRm,
    /// Skip the inner command after extraction.
    NoRun,
}
