use std::{fmt, path::PathBuf};


/// Result type for `cicero_distribution`.
pub type DistributionResult<T> = Result<T, DistributionError>;

/// Error type for `cicero_distribution`.
#[derive(Debug)]
pub struct DistributionError {
    context: DistributionErrorContext,
    source: DistributionErrorSource,
}
#[derive(Debug)]
enum DistributionErrorContext {
    Bundle { name: String },
    Create { name: String },
    CreateStaging { dir: PathBuf },
    StageFromBuild { target_path: PathBuf },
    StageFromPath { source_path: PathBuf, target_path: PathBuf },
}

impl DistributionError {
    pub(crate) fn bundle(name: impl Into<String>, source: impl Into<DistributionErrorSource>) -> Self {
        Self {
            context: DistributionErrorContext::Bundle { name: name.into() },
            source: source.into()
        }
    }
    pub(crate) fn create(name: impl Into<String>, source: impl Into<DistributionErrorSource>) -> Self {
        Self {
            context: DistributionErrorContext::Create { name: name.into() },
            source: source.into()
        }
    }
    pub(crate) fn create_staging(dir: impl Into<PathBuf>, source: impl Into<DistributionErrorSource>) -> Self {
        Self {
            context: DistributionErrorContext::CreateStaging { dir: dir.into() },
            source: source.into()
        }
    }
    pub(crate) fn stage_from_build(target_path: impl Into<PathBuf>, source: impl Into<DistributionErrorSource>) -> Self {
        Self {
            context: DistributionErrorContext::StageFromBuild {
                target_path: target_path.into(),
            },
            source: source.into()
        }
    }
    pub(crate) fn stage_from_path(source_path: impl Into<PathBuf>, target_path: impl Into<PathBuf>, source: impl Into<DistributionErrorSource>) -> Self {
        Self {
            context: DistributionErrorContext::StageFromPath {
                source_path: source_path.into(),
                target_path: target_path.into(),
            },
            source: source.into()
        }
    }
}

impl std::error::Error for DistributionError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self.source {
            DistributionErrorSource::Bundler { bundler: _, source } => Some(source.as_ref()),
            DistributionErrorSource::CleanDistributionFiles(error) => Some(error.as_ref()),
            DistributionErrorSource::CopyDir(error) => Some(error),
            DistributionErrorSource::Io(error) => Some(error),
            DistributionErrorSource::Message(_) => None,
            DistributionErrorSource::Nested(error) => Some(error),
            DistributionErrorSource::StripPathPrefix(error) => Some(error),
            DistributionErrorSource::UserScript(error) => Some(error.as_ref()),
        }
    }
}
impl fmt::Display for DistributionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.context {
            DistributionErrorContext::Bundle { name } => write!(f, "Error while bundling distribution \"{name}\""),
            DistributionErrorContext::Create { name } => write!(f, "Error while creating distribution \"{name}\""),
            DistributionErrorContext::CreateStaging { dir } => write!(f, "Error while creating distribution staging directory {dir:?}"),
            DistributionErrorContext::StageFromBuild { target_path } => write!(f, "Error while staging distribution path {target_path:?} from build"),
            DistributionErrorContext::StageFromPath { source_path, target_path } => write!(f, "Error while staging distribution path {target_path:?} from source path {source_path:?}"),
        }?;

        match &self.source {
            DistributionErrorSource::Bundler { bundler, source: _ } => write!(f, " in the bundler `{bundler}`"),
            DistributionErrorSource::CleanDistributionFiles(_) => write!(f, " when cleaning distribution files"),
            DistributionErrorSource::CopyDir(_) => write!(f, " when copying a directory"),
            DistributionErrorSource::Io(_) => write!(f, " when performing an IO operation"),
            DistributionErrorSource::Message(message) => write!(f, " because {message}"),
            DistributionErrorSource::Nested(_) => write!(f, " due to an underlying distribution error"),
            DistributionErrorSource::StripPathPrefix(_) => write!(f, " when stripping a path prefix"),
            DistributionErrorSource::UserScript(_) => write!(f, " when running user-provided creation code"),
        }?;

        Ok(())
    }
}

#[derive(Debug)]
pub(crate) enum DistributionErrorSource {
    /// Error while bundling the distribution files.
    Bundler { bundler: &'static str, source: cicero_core::Error },
    /// Error while cleaning the distribution files before starting a rebuild.
    CleanDistributionFiles(cicero_core::Error),
    /// Error while copying a directory recursively.
    CopyDir(crate::fs::copy_dir::error::CopyDirError),
    /// Error while performing an IO operation.
    Io(std::io::Error),
    /// Logic error described as a message.
    Message(&'static str),
    /// Another `DistributionError` caused this `DistributionError`.
    Nested(Box<DistributionError>),
    /// Error while stripping a path prefix
    StripPathPrefix(std::path::StripPrefixError),
    /// Error originating in user-provided code, which was executed for building the distribution.
    UserScript(cicero_core::Error),
}
impl From<crate::fs::copy_dir::error::CopyDirError> for DistributionErrorSource {
    fn from(value: crate::fs::copy_dir::error::CopyDirError) -> Self {
        Self::CopyDir(value)
    }
}
impl From<std::io::Error> for DistributionErrorSource {
    fn from(value: std::io::Error) -> Self {
        Self::Io(value)
    }
}
impl From<&'static str> for DistributionErrorSource {
    fn from(value: &'static str) -> Self {
        Self::Message(value)
    }
}
impl From<DistributionError> for DistributionErrorSource {
    fn from(value: DistributionError) -> Self {
        Self::Nested(Box::new(value))
    }
}
impl From<std::path::StripPrefixError> for DistributionErrorSource {
    fn from(value: std::path::StripPrefixError) -> Self {
        Self::StripPathPrefix(value)
    }
}
