use std::{fmt::Display, future::Future, time::Duration};

use cache::AppCache;
use serde::{Serialize, de::DeserializeOwned};

use crate::{bare::GitBare, graphql::GraphqlContext};

const KEY_SEPARATOR: &str = ":";

pub fn cache_key(namespace: &str, parts: &[&str]) -> String {
    let mut segments: Vec<&str> = vec![namespace];
    for part in parts {
        if !part.is_empty() {
            segments.push(part);
        }
    }
    segments.join(KEY_SEPARATOR)
}

pub fn cache_key_with_revision(
    namespace: &str,
    revision: impl Display,
    parts: &[&str],
) -> String {
    let mut key = cache_key(namespace, parts);
    key.push_str(KEY_SEPARATOR);
    key.push_str(&revision.to_string());
    key
}

fn path_hash(repo: &GitBare) -> String {
    let path_str = repo.bare_dir.to_string_lossy();
    let hash = simple_hash(&path_str);
    hash[..8].to_string()
}

fn simple_hash(s: &str) -> String {
    let mut h: u64 = 0;
    for b in s.bytes() {
        h = h.wrapping_mul(31).wrapping_add(b as u64);
    }
    format!("{:016x}", h)
}

pub async fn repo_revision(ctx: &GraphqlContext) -> String {
    let repo = ctx.repo.clone();
    let result = tokio::task::spawn_blocking(move || {
        repo.git_command_stdout(vec![
            "rev-parse".to_string(),
            "HEAD".to_string(),
        ])
    })
    .await;
    match result {
        Ok(Ok(oid)) => oid.trim().to_string(),
        _ => "unknown".to_string(),
    }
}

pub fn mutable_cache_key(
    ctx: &GraphqlContext,
    namespace: &str,
    parts: &[&str],
    revision: &str,
) -> String {
    let ph = path_hash(&ctx.repo);
    let mut all_parts: Vec<&str> = vec![&ph];
    all_parts.extend_from_slice(parts);
    cache_key_with_revision(namespace, revision, &all_parts)
}

pub async fn cached_json<T, F, Fut>(
    cache: &AppCache,
    key: &str,
    _ttl: Duration,
    build: F,
) -> anyhow::Result<T>
where
    T: Serialize + DeserializeOwned,
    F: FnOnce() -> Fut,
    Fut: Future<Output = anyhow::Result<T>>,
{
    if let Ok(Some(cached)) = cache.get::<T>(key).await {
        return Ok(cached);
    }

    let value = build().await?;
    cache.set(key, &value).await.ok();
    Ok(value)
}

pub const IMMUTABLE_TTL: Duration = Duration::from_secs(86400);
pub const MUTABLE_TTL: Duration = Duration::from_secs(300);
