use rattler_digest::{Md5, Md5Hash, Sha256, Sha256Hash};
use serde::{Deserialize, Serialize};
use serde_with::{OneOrMany, formats::PreferMany, serde_as};
use std::path::PathBuf;

use crate::stage0::types::{ConditionalList, IncludeExclude, JinjaTemplate, Value};

/// Parse a concrete SHA256 checksum from a hex string.
///
/// An empty string is treated as a placeholder of all zeros
/// (`0000...0000`), so recipe authors can scaffold a recipe before they have
/// computed the real checksum. The source download later verifies the hash and
/// reports a mismatch that includes the actual checksum. See issue #2524.
pub(crate) fn parse_sha256_hex(s: &str) -> Option<Sha256Hash> {
    if s.is_empty() {
        return Some(Sha256Hash::default());
    }
    rattler_digest::parse_digest_from_hex::<Sha256>(s)
}

/// Parse a concrete MD5 checksum from a hex string.
///
/// As with [`parse_sha256_hex`], an empty string is treated as an all-zeros
/// placeholder (`0000...0000`) so recipes can be scaffolded before the real
/// checksum is known. See issue #2524.
pub(crate) fn parse_md5_hex(s: &str) -> Option<Md5Hash> {
    if s.is_empty() {
        return Some(Md5Hash::default());
    }
    rattler_digest::parse_digest_from_hex::<Md5>(s)
}

/// Source information - can be Git, Url, or Path
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum Source {
    /// Git source pointing to a Git repository
    Git(GitSource),
    /// Url source pointing to a tarball or similar
    Url(UrlSource),
    /// Path source pointing to a local file or directory
    Path(PathSource),
}

/// A git revision (branch, tag or commit)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GitRev {
    /// A git branch, tag, or commit (template or concrete)
    Value(Value<String>),
}

impl Default for GitRev {
    fn default() -> Self {
        Self::Value(Value::new_concrete("HEAD".to_string(), None))
    }
}

/// A Git repository URL (can be template or concrete)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitUrl(pub Value<String>);

/// Git source information
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GitSource {
    /// Url to the git repository (template or concrete)
    #[serde(rename = "git")]
    pub url: GitUrl,

    /// Optionally a revision to checkout (can specify as rev, tag, or branch)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rev: Option<GitRev>,

    /// Optionally a tag to checkout
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tag: Option<GitRev>,

    /// Optionally a branch to checkout
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub branch: Option<GitRev>,

    /// Optionally a depth to clone the repository
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub depth: Option<Value<i32>>,

    /// Optionally patches to apply to the source code
    #[serde(default, skip_serializing_if = "ConditionalList::is_empty")]
    pub patches: ConditionalList<String>,

    /// Optionally a folder name under the `work` directory
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_directory: Option<Value<PathBuf>>,

    /// Optionally request the lfs pull in git source
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lfs: Option<Value<bool>>,

    /// Whether to recursively initialize and update submodules (defaults to true)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub submodules: Option<Value<bool>>,

    /// Optionally an expected commit hash to verify after checkout
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expected_commit: Option<Value<String>>,

    /// Filter for files to include/exclude from the checked-out tree
    #[serde(default)]
    pub filter: IncludeExclude,
}

/// Attestation verification configuration
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AttestationConfig {
    /// URL to download the attestation bundle from (e.g., .sigstore.json file)
    /// Auto-derived for PyPI sources if not specified.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bundle_url: Option<Value<String>>,

    /// Publisher identities to verify (e.g., "github:owner/repo" or "github:owner/repo@refs/tags/v1.0")
    /// All specified publishers must match.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub publishers: Vec<Value<String>>,
}

impl AttestationConfig {
    /// Check if the attestation config is empty (no verification configured)
    pub fn is_empty(&self) -> bool {
        self.bundle_url.is_none() && self.publishers.is_empty()
    }

    /// Collect all variables used in this attestation config
    pub fn used_variables(&self) -> Vec<String> {
        let mut vars = Vec::new();
        if let Some(bundle_url) = &self.bundle_url {
            vars.extend(bundle_url.used_variables());
        }
        for publisher in &self.publishers {
            vars.extend(publisher.used_variables());
        }
        vars
    }
}

/// A url source (usually a tar.gz or tar.bz2 archive)
#[serde_as]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UrlSource {
    /// Url(s) to the source code (template or concrete)
    /// Can be a single URL or a list of URLs (for mirrors)
    #[serde_as(as = "OneOrMany<_, PreferMany>")]
    #[serde(default)]
    pub url: Vec<Value<String>>,

    /// Optionally a sha256 checksum to verify the downloaded file
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        serialize_with = "sha256_serialization::serialize",
        deserialize_with = "sha256_serialization::deserialize"
    )]
    pub sha256: Option<Value<Sha256Hash>>,

    /// Optionally a md5 checksum to verify the downloaded file
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        serialize_with = "md5_serialization::serialize",
        deserialize_with = "md5_serialization::deserialize"
    )]
    pub md5: Option<Value<Md5Hash>>,

    /// Optionally a file name to rename the downloaded file
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub file_name: Option<Value<String>>,

    /// Patches to apply to the source code
    #[serde(default, skip_serializing_if = "ConditionalList::is_empty")]
    pub patches: ConditionalList<String>,

    /// Optionally a folder name under the `work` directory
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_directory: Option<Value<PathBuf>>,

    /// Optional attestation verification configuration
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub attestation: Option<AttestationConfig>,

    /// Filter for files to include/exclude from the extracted source
    #[serde(default)]
    pub filter: IncludeExclude,
}

/// A local path source
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PathSource {
    /// Path to the local source code (template or concrete)
    pub path: Value<PathBuf>,

    /// Optionally a sha256 checksum to verify the source code
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        serialize_with = "sha256_serialization::serialize",
        deserialize_with = "sha256_serialization::deserialize"
    )]
    pub sha256: Option<Value<Sha256Hash>>,

    /// Optionally a md5 checksum to verify the source code
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        serialize_with = "md5_serialization::serialize",
        deserialize_with = "md5_serialization::deserialize"
    )]
    pub md5: Option<Value<Md5Hash>>,

    /// Patches to apply to the source code
    #[serde(default, skip_serializing_if = "ConditionalList::is_empty")]
    pub patches: ConditionalList<String>,

    /// Optionally a folder name under the `work` directory
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_directory: Option<Value<PathBuf>>,

    /// Optionally a file name to rename the file to
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub file_name: Option<Value<PathBuf>>,

    /// Whether to use the `.gitignore` file in the source directory
    #[serde(default = "default_use_gitignore", skip_serializing_if = "is_true")]
    pub use_gitignore: bool,

    /// Filter for files to include/exclude from the source
    #[serde(default)]
    pub filter: IncludeExclude,
}

fn default_use_gitignore() -> bool {
    true
}

fn is_true(value: &bool) -> bool {
    *value
}

/// Serialize/deserialize a SHA256 hash Value as a hex string (or Jinja template)
mod sha256_serialization {
    use super::*;
    use serde::{Deserializer, Serializer};

    pub fn serialize<S>(value: &Option<Value<Sha256Hash>>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match value {
            None => serializer.serialize_none(),
            Some(v) if v.is_concrete() => {
                serializer.serialize_str(&hex::encode(v.as_concrete().unwrap()))
            }
            Some(v) if v.is_template() => {
                serializer.serialize_str(v.as_template().unwrap().source())
            }
            _ => unreachable!("Value must be either concrete or template"),
        }
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Value<Sha256Hash>>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let Some(s) = Option::<String>::deserialize(deserializer)? else {
            return Ok(None);
        };
        if s.contains("${{") {
            let template = JinjaTemplate::new(s).map_err(serde::de::Error::custom)?;
            Ok(Some(Value::new_template(template, None)))
        } else {
            let hash = parse_sha256_hex(&s)
                .ok_or_else(|| serde::de::Error::custom(format!("Invalid SHA256 checksum: {s}")))?;
            Ok(Some(Value::new_concrete(hash, None)))
        }
    }
}

/// Serialize/deserialize an MD5 hash Value as a hex string (or Jinja template)
mod md5_serialization {
    use super::*;
    use serde::{Deserializer, Serializer};

    pub fn serialize<S>(value: &Option<Value<Md5Hash>>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match value {
            None => serializer.serialize_none(),
            Some(v) if v.is_concrete() => {
                serializer.serialize_str(&hex::encode(v.as_concrete().unwrap()))
            }
            Some(v) if v.is_template() => {
                serializer.serialize_str(v.as_template().unwrap().source())
            }
            _ => unreachable!("Value must be either concrete or template"),
        }
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Value<Md5Hash>>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let Some(s) = Option::<String>::deserialize(deserializer)? else {
            return Ok(None);
        };
        if s.contains("${{") {
            let template = JinjaTemplate::new(s).map_err(serde::de::Error::custom)?;
            Ok(Some(Value::new_template(template, None)))
        } else {
            let hash = parse_md5_hex(&s)
                .ok_or_else(|| serde::de::Error::custom(format!("Invalid MD5 checksum: {s}")))?;
            Ok(Some(Value::new_concrete(hash, None)))
        }
    }
}

impl Source {
    /// Collect all variables used in this source
    pub fn used_variables(&self) -> Vec<String> {
        match self {
            Source::Git(git) => git.used_variables(),
            Source::Url(url) => url.used_variables(),
            Source::Path(path) => path.used_variables(),
        }
    }
}

impl GitSource {
    /// Collect all variables used in the git source
    pub fn used_variables(&self) -> Vec<String> {
        let GitSource {
            url,
            rev,
            tag,
            branch,
            depth,
            patches,
            target_directory,
            lfs,
            submodules,
            expected_commit,
            filter,
        } = self;

        let mut vars = Vec::new();
        vars.extend(url.0.used_variables());
        if let Some(GitRev::Value(v)) = rev {
            vars.extend(v.used_variables());
        }
        if let Some(GitRev::Value(v)) = tag {
            vars.extend(v.used_variables());
        }
        if let Some(GitRev::Value(v)) = branch {
            vars.extend(v.used_variables());
        }
        if let Some(depth) = depth {
            vars.extend(depth.used_variables());
        }
        // Extract variables from patches (both values and conditionals)
        vars.extend(patches.used_variables());
        if let Some(td) = target_directory {
            vars.extend(td.used_variables());
        }
        if let Some(lfs) = lfs {
            vars.extend(lfs.used_variables());
        }
        if let Some(submodules) = submodules {
            vars.extend(submodules.used_variables());
        }
        if let Some(ec) = expected_commit {
            vars.extend(ec.used_variables());
        }
        vars.extend(filter.used_variables());
        vars.sort();
        vars.dedup();
        vars
    }
}

impl UrlSource {
    /// Collect all variables used in the url source
    pub fn used_variables(&self) -> Vec<String> {
        let UrlSource {
            url,
            sha256,
            md5,
            file_name,
            patches,
            target_directory,
            attestation,
            filter,
        } = self;

        let mut vars = Vec::new();
        for url in url {
            vars.extend(url.used_variables());
        }
        if let Some(sha256) = sha256 {
            vars.extend(sha256.used_variables());
        }
        if let Some(md5) = md5 {
            vars.extend(md5.used_variables());
        }
        if let Some(file_name) = file_name {
            vars.extend(file_name.used_variables());
        }
        // Extract variables from patches (both values and conditionals)
        for item in patches {
            vars.extend(item.used_variables());
        }
        if let Some(td) = target_directory {
            vars.extend(td.used_variables());
        }
        if let Some(attestation) = attestation {
            vars.extend(attestation.used_variables());
        }
        vars.extend(filter.used_variables());
        vars.sort();
        vars.dedup();
        vars
    }
}

impl PathSource {
    /// Collect all variables used in the path source
    pub fn used_variables(&self) -> Vec<String> {
        let PathSource {
            path,
            sha256,
            md5,
            patches,
            target_directory,
            file_name,
            use_gitignore: _,
            filter,
        } = self;

        let mut vars = Vec::new();

        vars.extend(path.used_variables());
        vars.extend(patches.used_variables());
        if let Some(sha256) = sha256 {
            vars.extend(sha256.used_variables());
        }
        if let Some(md5) = md5 {
            vars.extend(md5.used_variables());
        }
        // Skip patches as PathBuf doesn't easily support template extraction
        if let Some(td) = target_directory {
            vars.extend(td.used_variables());
        }
        if let Some(fn_val) = file_name {
            vars.extend(fn_val.used_variables());
        }
        vars.extend(filter.used_variables());
        vars.sort();
        vars.dedup();
        vars
    }
}
