use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize};
use std::path::Path;
use uv_distribution_types::Hashed;

use uv_pypi_types::{HashDigest, HashDigests};

/// The [`Revision`] is a thin wrapper around a unique identifier for the source distribution.
///
/// A revision represents a unique version of a source distribution, at a level more granular than
/// (e.g.) the version number of the distribution itself. For example, a source distribution hosted
/// at a URL or a local file path may have multiple revisions, each representing a unique state of
/// the distribution, despite the reported version number remaining the same.
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct Revision {
    id: RevisionId,
    hashes: HashDigests,
    #[serde(default)]
    size: Option<u64>,
}

impl Serialize for Revision {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // Cache buckets are shared with older uv versions, whose readers can ignore unknown map
        // entries but reject MessagePack arrays with additional fields.
        let mut map = serializer.serialize_map(Some(3))?;
        map.serialize_entry("id", &self.id)?;
        map.serialize_entry("hashes", &self.hashes)?;
        map.serialize_entry("size", &self.size)?;
        map.end()
    }
}

impl Revision {
    /// Initialize a new [`Revision`] with a random UUID.
    pub(crate) fn new() -> Self {
        Self {
            id: RevisionId::new(),
            hashes: HashDigests::empty(),
            size: None,
        }
    }

    /// Return the unique ID of the manifest.
    pub(crate) fn id(&self) -> &RevisionId {
        &self.id
    }

    /// Return the computed hashes of the archive.
    pub(crate) fn hashes(&self) -> &[HashDigest] {
        self.hashes.as_slice()
    }

    /// Return the computed hashes of the archive.
    pub(crate) fn into_hashes(self) -> HashDigests {
        self.hashes
    }

    /// Return the size of the downloaded archive.
    pub(crate) fn size(&self) -> Option<u64> {
        self.size
    }

    /// Set the computed hashes of the archive.
    #[must_use]
    pub(crate) fn with_hashes(mut self, hashes: HashDigests) -> Self {
        self.hashes = hashes;
        self
    }

    /// Set the size of the downloaded archive.
    #[must_use]
    pub(crate) fn with_size(mut self, size: u64) -> Self {
        self.size = Some(size);
        self
    }
}

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

/// A unique identifier for a revision of a source distribution.
///
/// Note: for compatibility with the existing `sdists-v9` bucket, this is a newtype around a
/// `String` rather than a newtype around `uv_fastid::Id`. In the future, we may want to bump
/// to `sdists-v10` and switch to using `uv_fastid::Id` directly.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct RevisionId(String);

impl RevisionId {
    /// Generate a new unique identifier for an archive.
    fn new() -> Self {
        Self(uv_fastid::Id::secure().to_string())
    }

    pub(crate) fn as_str(&self) -> &str {
        &self.0
    }
}

impl AsRef<str> for RevisionId {
    fn as_ref(&self) -> &str {
        self.0.as_ref()
    }
}

impl AsRef<Path> for RevisionId {
    fn as_ref(&self) -> &Path {
        self.0.as_ref()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Regression test for <https://github.com/astral-sh/uv/issues/19298>.
    #[test]
    fn deserialize_legacy_nanoid_revision() {
        #[derive(Serialize)]
        struct LegacyRevision {
            id: RevisionId,
            hashes: HashDigests,
        }

        // A representative 21-character nanoid ID, drawn from the same alphabet
        // used by both the old `nanoid` crate and `uv_fastid`.
        let legacy = LegacyRevision {
            id: RevisionId("HM0NxJml5hc7UjbfTWT1r".to_string()),
            hashes: HashDigests::empty(),
        };
        let bytes = rmp_serde::to_vec(&legacy).expect("serialize legacy revision");
        let parsed: Revision = rmp_serde::from_slice(&bytes).expect("deserialize legacy revision");
        assert_eq!(parsed.id().as_str(), "HM0NxJml5hc7UjbfTWT1r");
    }

    #[test]
    fn round_trip_current_revision() {
        let original = Revision::new();
        let bytes = rmp_serde::to_vec(&original).expect("serialize revision");
        let parsed: Revision = rmp_serde::from_slice(&bytes).expect("deserialize revision");
        assert_eq!(parsed.id().as_str(), original.id().as_str());
    }

    #[test]
    fn deserialize_uv_0_12_revision() {
        #[derive(Serialize)]
        struct ReleasedRevision {
            id: RevisionId,
            hashes: HashDigests,
            size: Option<u64>,
        }

        let released = ReleasedRevision {
            id: RevisionId("HM0NxJml5hc7UjbfTWT1r".to_string()),
            hashes: HashDigests::empty(),
            size: Some(42),
        };
        let bytes = rmp_serde::to_vec(&released).expect("serialize uv 0.12 revision");
        let revision: Revision =
            rmp_serde::from_slice(&bytes).expect("deserialize uv 0.12 revision");

        assert_eq!(revision.size(), Some(42));
    }

    #[test]
    fn serialize_revision_for_legacy_reader() {
        #[derive(Deserialize)]
        struct LegacyRevision {
            id: RevisionId,
            hashes: HashDigests,
        }

        let revision = Revision::new().with_size(42);
        let bytes = rmp_serde::to_vec(&revision).expect("serialize revision");
        let legacy: LegacyRevision =
            rmp_serde::from_slice(&bytes).expect("deserialize revision with legacy reader");

        assert_eq!(legacy.id.as_str(), revision.id().as_str());
        assert_eq!(legacy.hashes, HashDigests::empty());
    }
}
