use std::{ffi::OsString, io, path::PathBuf};

/// Error type returned by `bdlx-core` operations.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// User-facing usage or argument error.
    #[error("{0}")]
    Usage(String),
    /// I/O failure with operation context.
    #[error("{context}: {source}")]
    Io {
        /// Human-readable operation context.
        context: String,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },
    /// Non-UTF-8 CLI argument or command component.
    #[error("non-UTF-8 {context}: {value:?}")]
    NonUtf8Arg {
        /// Argument role used in diagnostics.
        context: &'static str,
        /// Original non-UTF-8 value.
        value: OsString,
    },
    /// Archive bytes did not look like a supported tar format.
    #[error("unsupported archive format for {path}")]
    UnsupportedArchiveFormat {
        /// Path or label used to identify the unsupported archive.
        path: String,
    },
    /// The stdin marker was specified more than once.
    #[error("input `-` may appear at most once")]
    DuplicateStdin,
    /// The bundle command is missing its required outer `--` delimiter.
    #[error("missing `--` delimiter before inner command")]
    MissingBundleDelimiter,
    /// The bundle command has no inner command after `--`.
    #[error("missing inner command after `--`")]
    MissingInnerCommand,
    /// An unknown bundle option was found before `--`.
    #[error("unknown bundle option `{0}`")]
    UnknownBundleOption(String),
    /// An assignment-like token had an invalid environment variable name.
    #[error(
        "invalid env assignment `{assignment}`: variable name must match [A-Za-z_][A-Za-z0-9_]*"
    )]
    InvalidEnvAssignment {
        /// Original token that failed validation.
        assignment: String,
    },
    /// Shared mode was requested without disabling cleanup.
    #[error("`--shared` requires `--rm never`")]
    SharedRequiresRmNever,
    /// Internal guard for APIs that can consume stdin at most once.
    #[error("bundle runtime only supports stdin once")]
    StdinAlreadyConsumed,
    /// Payload encoding failed.
    #[error("failed to encode payload for {label}")]
    EncodeFailure {
        /// Path or label of the payload being encoded.
        label: String,
    },
    /// Internal script-generation invariant failed.
    #[error("internal shell generation error: {0}")]
    Internal(String),
}

impl Error {
    /// Returns the conventional process exit code for this error.
    pub fn exit_code(&self) -> i32 {
        match self {
            Self::Usage(_)
            | Self::NonUtf8Arg { .. }
            | Self::UnsupportedArchiveFormat { .. }
            | Self::DuplicateStdin
            | Self::MissingBundleDelimiter
            | Self::MissingInnerCommand
            | Self::UnknownBundleOption(_)
            | Self::InvalidEnvAssignment { .. }
            | Self::SharedRequiresRmNever => 2,
            Self::Io { .. }
            | Self::StdinAlreadyConsumed
            | Self::EncodeFailure { .. }
            | Self::Internal(_) => 1,
        }
    }

    /// Creates an I/O error with path context.
    pub fn io_path(
        context: impl Into<String>,
        path: impl Into<PathBuf>,
        source: io::Error,
    ) -> Self {
        let path = path.into();
        Self::Io {
            context: format!("{} ({})", context.into(), path.display()),
            source,
        }
    }

    /// Creates an I/O error with operation context.
    pub fn io(context: impl Into<String>, source: io::Error) -> Self {
        Self::Io {
            context: context.into(),
            source,
        }
    }
}

/// Result alias used by `bdlx-core`.
pub type Result<T, E = Error> = std::result::Result<T, E>;
