use std::path::{Path, PathBuf};
use std::str::FromStr;

use uv_cache::CacheShard;
use uv_cache_info::CacheInfo;
use uv_distribution_filename::WheelFilename;
use uv_distribution_types::{BuildInfo, Hashed};
use uv_fs::files;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_platform_tags::Tags;
use uv_pypi_types::{HashDigest, HashDigests};

/// The information about the wheel we either just built or got from the cache.
#[derive(Debug, Clone)]
pub(crate) struct BuiltWheelMetadata {
    /// The path to the built wheel.
    pub(crate) path: Box<Path>,
    /// The expected path to the downloaded wheel's entry in the cache.
    pub(crate) target: Box<Path>,
    /// The parsed filename.
    pub(crate) filename: WheelFilename,
    /// The computed hashes of the source distribution from which the wheel was built.
    pub(crate) hashes: HashDigests,
    /// The cache information for the underlying source distribution.
    pub(crate) cache_info: CacheInfo,
    /// The build information for the wheel.
    pub(crate) build_info: BuildInfo,
}

impl BuiltWheelMetadata {
    /// Create a [`BuiltWheelMetadata`] from a [`BuiltWheelFile`].
    pub(crate) fn from_file(
        file: BuiltWheelFile,
        hashes: HashDigests,
        cache_info: CacheInfo,
        build_info: BuildInfo,
    ) -> Self {
        Self {
            path: file.path,
            target: file.target,
            filename: file.filename,
            hashes,
            cache_info,
            build_info,
        }
    }
}

impl Hashed for BuiltWheelMetadata {
    fn hashes(&self) -> &[HashDigest] {
        self.hashes.as_slice()
    }
}

/// The path to a built wheel file, along with its parsed filename.
#[derive(Debug, Clone)]
pub(crate) struct BuiltWheelFile {
    /// The path to the built wheel.
    pub(crate) path: Box<Path>,
    /// The expected path to the downloaded wheel's entry in the cache.
    pub(crate) target: Box<Path>,
    /// The parsed filename.
    pub(crate) filename: WheelFilename,
}

impl BuiltWheelFile {
    /// Find a compatible wheel in the cache.
    pub(crate) fn find_in_cache(
        tags: &Tags,
        cache_shard: &CacheShard,
    ) -> Result<Option<Self>, std::io::Error> {
        for file in files(cache_shard)? {
            if let Some(metadata) = Self::from_path(file, cache_shard) {
                // Validate that the wheel is compatible with the target platform.
                if metadata.filename.is_compatible(tags) {
                    return Ok(Some(metadata));
                }
            }
        }
        Ok(None)
    }

    /// Try to parse a distribution from a cached directory name (like `typing-extensions-4.8.0-py3-none-any.whl`).
    fn from_path(path: PathBuf, cache_shard: &CacheShard) -> Option<Self> {
        let filename = path.file_name()?.to_str()?;
        let filename = WheelFilename::from_str(filename).ok()?;
        Some(Self {
            target: cache_shard.join(filename.stem()).into_boxed_path(),
            path: path.into_boxed_path(),
            filename,
        })
    }

    /// Returns `true` if the wheel matches the given package name and version.
    pub(crate) fn matches(&self, name: Option<&PackageName>, version: Option<&Version>) -> bool {
        name.is_none_or(|name| self.filename.name == *name)
            && version.is_none_or(|version| self.filename.version == *version)
    }
}
