//! Serializable types for the LFS domain.

use serde::{Deserialize, Serialize};

/// The SHA256 OID of an LFS object.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct LfsOid(pub String);

impl LfsOid {
    pub fn new(hex: &str) -> Self {
        Self(hex.to_lowercase())
    }

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

    pub fn is_valid(&self) -> bool {
        self.0.len() == 64 && self.0.chars().all(|c| c.is_ascii_hexdigit())
    }
}

impl std::fmt::Display for LfsOid {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

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

/// An LFS pointer file parsed from a blob.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LfsPointer {
    pub version: String,
    pub oid: LfsOid,
    pub size: u64,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub extra: Vec<(String, String)>,
}

impl LfsPointer {
    pub fn from_bytes(data: &[u8]) -> Option<Self> {
        Self::from_str(std::str::from_utf8(data).ok()?)
    }

    pub fn from_str(s: &str) -> Option<Self> {
        let mut version = None;
        let mut oid = None;
        let mut size = None;
        let mut extra = Vec::new();

        for line in s.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            if let Some(val) = line.strip_prefix("version ") {
                version = Some(val.trim().to_string());
            } else if let Some(val) = line.strip_prefix("oid sha256:") {
                oid = Some(LfsOid::new(val.trim()));
            } else if let Some(val) = line.strip_prefix("size ") {
                size = val.trim().parse::<u64>().ok();
            } else if let Some((k, v)) = line.split_once(' ') {
                extra.push((k.to_string(), v.to_string()));
            }
        }

        let version = version?;
        if version != "https://git-lfs.github.com/spec/v1" {
            return None;
        }
        let oid = oid?;
        if !oid.is_valid() {
            return None;
        }
        let size = size?;

        Some(Self {
            version,
            oid,
            size,
            extra,
        })
    }

    pub fn to_string(&self) -> String {
        let mut lines = Vec::with_capacity(3 + self.extra.len());
        lines.push(format!("version {}", self.version));
        lines.push(format!("oid sha256:{}", self.oid));
        lines.push(format!("size {}", self.size));
        for (k, v) in &self.extra {
            lines.push(format!("{} {}", k, v));
        }
        lines.join("\n") + "\n"
    }

    /// Path inside `.git/lfs/objects/` where this object would be stored locally.
    pub fn local_object_path(&self, git_dir: &std::path::Path) -> std::path::PathBuf {
        let oid = self.oid.as_str();
        // LFS OIDs must be at least 4 chars (2+2 split) for the path layout.
        // Valid LFS OIDs are 64-char SHA256 hex, so this is always safe for valid pointers.
        // Defensive check: if OID is somehow invalid, use the full OID as filename.
        if oid.len() < 4 {
            return git_dir.join("lfs").join("objects").join(oid);
        }
        let (p1, rest) = oid.split_at(2);
        let (p2, _) = rest.split_at(2);
        git_dir
            .join("lfs")
            .join("objects")
            .join(p1)
            .join(p2)
            .join(oid)
    }
}

/// Metadata for an LFS-managed file in a tree entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LfsEntry {
    pub path: String,
    pub pointer: LfsPointer,
    pub size: u64,
}

/// LFS configuration for a repository.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LfsConfig {
    pub endpoint: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub access_token: Option<String>,
}

impl LfsConfig {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn endpoint(mut self, url: &str) -> Self {
        self.endpoint = Some(url.to_string());
        self
    }

    pub fn access_token(mut self, token: &str) -> Self {
        self.access_token = Some(token.to_string());
        self
    }
}
