use serde::{Deserialize, Serialize};
use sha2::Digest;

use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb, RawObjectStore};

#[derive(Clone, Debug)]
pub enum PackBytes {
    Eager(std::sync::Arc<[u8]>),
    LazyExternal {
        sha256_hex: String,
        store: std::sync::Arc<ExternalRawStoreDb>,
        cache: std::sync::Arc<std::sync::OnceLock<std::sync::Arc<[u8]>>>,
    },
    LazyRepoBytes {
        sha256_hex: String,
        store: std::sync::Arc<ExternalRepoBytesDb>,
        cache: std::sync::Arc<std::sync::OnceLock<std::sync::Arc<[u8]>>>,
    },
}

impl PackBytes {
    pub fn eager(bytes: Vec<u8>) -> Self {
        Self::Eager(std::sync::Arc::from(bytes))
    }

    pub fn lazy_external(sha256_hex: String, store: std::sync::Arc<ExternalRawStoreDb>) -> Self {
        Self::LazyExternal {
            sha256_hex,
            store,
            cache: std::sync::Arc::new(std::sync::OnceLock::new()),
        }
    }

    pub fn lazy_repo_bytes(sha256_hex: String, store: std::sync::Arc<ExternalRepoBytesDb>) -> Self {
        Self::LazyRepoBytes {
            sha256_hex,
            store,
            cache: std::sync::Arc::new(std::sync::OnceLock::new()),
        }
    }

    pub fn as_slice(&self) -> Result<&[u8], String> {
        match self {
            Self::Eager(bytes) => Ok(bytes.as_ref()),
            Self::LazyExternal {
                sha256_hex,
                store,
                cache,
            } => {
                if cache.get().is_none() {
                    let bytes = store
                        .get_blob_bytes(sha256_hex)
                        .map_err(|e| e.to_string())?
                        .ok_or_else(|| format!("missing raw blob for sha256={sha256_hex}"))?;
                    let _ = cache.set(std::sync::Arc::from(bytes));
                }
                let bytes = cache
                    .get()
                    .ok_or_else(|| format!("missing raw blob cache for sha256={sha256_hex}"))?;
                Ok(bytes.as_ref())
            }
            Self::LazyRepoBytes {
                sha256_hex,
                store,
                cache,
            } => {
                if cache.get().is_none() {
                    let bytes = store
                        .get_blob_bytes(sha256_hex)
                        .map_err(|e| e.to_string())?
                        .ok_or_else(|| format!("missing repo bytes for sha256={sha256_hex}"))?;
                    let _ = cache.set(std::sync::Arc::from(bytes));
                }
                let bytes = cache
                    .get()
                    .ok_or_else(|| format!("missing repo bytes cache for sha256={sha256_hex}"))?;
                Ok(bytes.as_ref())
            }
        }
    }

    pub fn to_vec(&self) -> Result<Vec<u8>, String> {
        Ok(self.as_slice()?.to_vec())
    }
}

impl PartialEq for PackBytes {
    fn eq(&self, other: &Self) -> bool {
        match (self.as_slice(), other.as_slice()) {
            (Ok(a), Ok(b)) => a == b,
            _ => false,
        }
    }
}

impl Eq for PackBytes {}

#[derive(Clone, Debug)]
pub struct PackFile {
    pub rsync_uri: String,
    pub bytes: PackBytes,
    pub sha256: [u8; 32],
}

impl PackFile {
    pub fn new(rsync_uri: impl Into<String>, bytes: PackBytes, sha256: [u8; 32]) -> Self {
        Self {
            rsync_uri: rsync_uri.into(),
            bytes,
            sha256,
        }
    }

    pub fn from_bytes_with_sha256(
        rsync_uri: impl Into<String>,
        bytes: Vec<u8>,
        sha256: [u8; 32],
    ) -> Self {
        Self::new(rsync_uri, PackBytes::eager(bytes), sha256)
    }

    pub fn from_lazy_external_raw_store(
        rsync_uri: impl Into<String>,
        sha256_hex: String,
        sha256: [u8; 32],
        store: std::sync::Arc<ExternalRawStoreDb>,
    ) -> Self {
        Self::new(
            rsync_uri,
            PackBytes::lazy_external(sha256_hex, store),
            sha256,
        )
    }

    pub fn from_lazy_repo_bytes(
        rsync_uri: impl Into<String>,
        sha256_hex: String,
        sha256: [u8; 32],
        store: std::sync::Arc<ExternalRepoBytesDb>,
    ) -> Self {
        Self::new(
            rsync_uri,
            PackBytes::lazy_repo_bytes(sha256_hex, store),
            sha256,
        )
    }

    pub fn from_bytes_compute_sha256(rsync_uri: impl Into<String>, bytes: Vec<u8>) -> Self {
        let sha256 = compute_sha256_32(&bytes);
        Self::new(rsync_uri, PackBytes::eager(bytes), sha256)
    }

    pub fn bytes(&self) -> Result<&[u8], String> {
        self.bytes.as_slice()
    }

    pub fn bytes_cloned(&self) -> Result<Vec<u8>, String> {
        self.bytes.to_vec()
    }

    pub fn compute_sha256(&self) -> Result<[u8; 32], String> {
        Ok(compute_sha256_32(self.bytes()?))
    }
}

impl PartialEq for PackFile {
    fn eq(&self, other: &Self) -> bool {
        self.rsync_uri == other.rsync_uri
            && self.sha256 == other.sha256
            && self.bytes == other.bytes
    }
}

impl Eq for PackFile {}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackTime {
    pub rfc3339_utc: String,
}

impl PackTime {
    pub fn from_utc_offset_datetime(t: time::OffsetDateTime) -> Self {
        use time::format_description::well_known::Rfc3339;
        let utc = t.to_offset(time::UtcOffset::UTC);
        let s = utc.format(&Rfc3339).expect("format RFC 3339 UTC time");
        Self { rfc3339_utc: s }
    }

    pub fn parse(&self) -> Result<time::OffsetDateTime, String> {
        use time::format_description::well_known::Rfc3339;
        time::OffsetDateTime::parse(&self.rfc3339_utc, &Rfc3339).map_err(|e| e.to_string())
    }
}

pub(super) fn compute_sha256_32(bytes: &[u8]) -> [u8; 32] {
    let digest = sha2::Sha256::digest(bytes);
    let mut out = [0u8; 32];
    out.copy_from_slice(&digest);
    out
}
