use std::fmt;
use std::fs;
use std::io::Read;
use std::path::{Component, Path, PathBuf};

/// Error type for package artifact path resolution and file reads.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackageArtifactError {
    EmptyPath,
    AbsolutePath {
        path: String,
    },
    PathEscape {
        path: String,
    },
    AssetReadFailed {
        path: String,
        resolved: String,
        reason: String,
    },
}

impl fmt::Display for PackageArtifactError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyPath => write!(f, "asset path is required"),
            Self::AbsolutePath { path } => {
                write!(
                    f,
                    "asset path '{path}' must be relative to the package root"
                )
            }
            Self::PathEscape { path } => {
                write!(f, "asset path '{path}' must stay inside the package root")
            }
            Self::AssetReadFailed {
                path,
                resolved,
                reason,
            } => write!(f, "failed to read asset '{path}' at '{resolved}': {reason}"),
        }
    }
}

impl std::error::Error for PackageArtifactError {}

/// Validate and resolve a package-relative asset path under a package base directory.
pub fn resolve_package_asset_path(
    base_dir: &Path,
    relative_path: &str,
) -> Result<PathBuf, PackageArtifactError> {
    validate_package_relative_path(relative_path)?;
    Ok(resolve_package_path(base_dir, relative_path))
}

/// Read a package-relative asset after validating that the path stays inside the package root.
pub fn read_package_file(
    base_dir: &Path,
    relative_path: &str,
) -> Result<Vec<u8>, PackageArtifactError> {
    let (mut file, resolved) = open_package_file(base_dir, relative_path)?;
    let mut bytes = Vec::new();
    file.read_to_end(&mut bytes)
        .map_err(|error| PackageArtifactError::AssetReadFailed {
            path: relative_path.to_string(),
            resolved: resolved.display().to_string(),
            reason: error.to_string(),
        })?;
    Ok(bytes)
}

pub(crate) fn open_package_file(
    base_dir: &Path,
    relative_path: &str,
) -> Result<(fs::File, PathBuf), PackageArtifactError> {
    let resolved = resolve_existing_package_asset_path(base_dir, relative_path)?;
    if !resolved.is_file() {
        return Err(PackageArtifactError::AssetReadFailed {
            path: relative_path.to_string(),
            resolved: resolved.display().to_string(),
            reason: "asset path is not a file".to_string(),
        });
    }
    let file =
        fs::File::open(&resolved).map_err(|error| PackageArtifactError::AssetReadFailed {
            path: relative_path.to_string(),
            resolved: resolved.display().to_string(),
            reason: error.to_string(),
        })?;
    Ok((file, resolved))
}

/// Validate that an asset path is relative and cannot escape the package root.
pub(crate) fn validate_package_relative_path(
    relative_path: &str,
) -> Result<(), PackageArtifactError> {
    let path = Path::new(relative_path);
    if relative_path.trim().is_empty() {
        return Err(PackageArtifactError::EmptyPath);
    }

    if path.is_absolute() {
        return Err(PackageArtifactError::AbsolutePath {
            path: relative_path.to_string(),
        });
    }

    if path.components().any(|component| {
        matches!(
            component,
            Component::ParentDir | Component::RootDir | Component::Prefix(_)
        )
    }) {
        return Err(PackageArtifactError::PathEscape {
            path: relative_path.to_string(),
        });
    }

    Ok(())
}

fn resolve_package_path(base_dir: &Path, relative_path: &str) -> PathBuf {
    base_dir.join(relative_path)
}

fn resolve_existing_package_asset_path(
    base_dir: &Path,
    relative_path: &str,
) -> Result<PathBuf, PackageArtifactError> {
    validate_package_relative_path(relative_path)?;
    let root = base_dir
        .canonicalize()
        .map_err(|error| PackageArtifactError::AssetReadFailed {
            path: relative_path.to_string(),
            resolved: base_dir.display().to_string(),
            reason: format!("failed to resolve package root: {error}"),
        })?;
    let candidate = root.join(relative_path);
    let resolved =
        candidate
            .canonicalize()
            .map_err(|error| PackageArtifactError::AssetReadFailed {
                path: relative_path.to_string(),
                resolved: candidate.display().to_string(),
                reason: error.to_string(),
            })?;

    if !resolved.starts_with(&root) {
        return Err(PackageArtifactError::PathEscape {
            path: relative_path.to_string(),
        });
    }

    Ok(resolved)
}
