use std::path::PathBuf;

use anyhow::{Context, Result};
use log::{debug, info};

use crate::parser::gitlab::fetch::cache_dir;

/// Parsed OCI function reference.
#[derive(Debug, Clone)]
pub struct OciFuncRef {
    pub image: String,
    pub func_path: String,
}

/// Parse a func reference as an OCI image reference.
/// Returns `None` for local (`./`, `/`) or git (`host/repo@rev`) refs.
pub fn parse_oci_func_ref(func_ref: &str) -> Option<OciFuncRef> {
    if func_ref.starts_with("./") || func_ref.starts_with('/') {
        return None;
    }

    let (image_part, func_path) = if let Some((img, path)) = func_ref.split_once(' ') {
        (img, path.to_string())
    } else {
        (func_ref, "func.yml".to_string())
    };

    if !image_part.contains(':') {
        return None;
    }

    // Git refs use @ for revision — skip deprecated git loading
    if image_part.contains('@') {
        return None;
    }

    Some(OciFuncRef {
        image: image_part.to_string(),
        func_path,
    })
}

fn func_cache_dir() -> Option<PathBuf> {
    Some(cache_dir()?.join("oci-funcs"))
}

fn sanitize_image_key(image: &str) -> String {
    image
        .replace("://", "_")
        .replace(['/', ':'], "_")
        .replace("..", "_")
}

/// Fetch `func.yml` content from an OCI image, using a local cache.
pub fn fetch_oci_func_yml(oci_ref: &OciFuncRef) -> Result<String> {
    let cache_base = func_cache_dir().context("cannot determine cache directory")?;
    let image_dir = cache_base.join(sanitize_image_key(&oci_ref.image));
    let func_file = image_dir.join(&oci_ref.func_path);

    anyhow::ensure!(
        !std::path::Path::new(&oci_ref.func_path)
            .components()
            .any(|c| matches!(c, std::path::Component::ParentDir)),
        "func_path '{}' escapes the image cache directory",
        oci_ref.func_path
    );

    if func_file.exists() {
        debug!("OCI func cache hit: {}", func_file.display());
        return std::fs::read_to_string(&func_file)
            .with_context(|| format!("failed to read cached {}", func_file.display()));
    }

    info!("Fetching OCI func image {}...", oci_ref.image);

    #[cfg(target_os = "linux")]
    {
        let reference = oci_unpack::Reference::try_from(oci_ref.image.as_str())
            .with_context(|| format!("invalid OCI reference '{}'", oci_ref.image))?;

        oci_unpack::Unpacker::new(reference)
            .require_sandbox(false)
            .unpack(&image_dir)
            .with_context(|| format!("failed to unpack OCI image '{}'", oci_ref.image))?;

        std::fs::read_to_string(&func_file)
            .with_context(|| format!("func.yml not found in OCI image at '{}'", oci_ref.func_path))
    }

    #[cfg(not(target_os = "linux"))]
    {
        let _ = (image_dir, func_file);
        anyhow::bail!(
            "OCI func image unpacking requires Linux (image '{}'); \
             use a local func: path instead",
            oci_ref.image
        );
    }
}

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

    #[test]
    fn test_parse_oci_func_ref_simple() {
        let r = parse_oci_func_ref("registry.gitlab.com/org/func:1.0").unwrap();
        assert_eq!(r.image, "registry.gitlab.com/org/func:1.0");
        assert_eq!(r.func_path, "func.yml");
    }

    #[test]
    fn test_parse_oci_func_ref_with_subpath() {
        let r = parse_oci_func_ref(
            "registry.gitlab.com/gitlab-org/ci-cd/runner-tools/gitlab-functions-examples/echo:1 reverse/func.yml",
        )
        .unwrap();
        assert_eq!(
            r.image,
            "registry.gitlab.com/gitlab-org/ci-cd/runner-tools/gitlab-functions-examples/echo:1"
        );
        assert_eq!(r.func_path, "reverse/func.yml");
    }

    #[test]
    fn test_parse_oci_func_ref_local_returns_none() {
        assert!(parse_oci_func_ref("./my-func").is_none());
        assert!(parse_oci_func_ref("/opt/funcs/deploy").is_none());
    }

    #[test]
    fn test_parse_oci_func_ref_no_tag_returns_none() {
        assert!(parse_oci_func_ref("registry.gitlab.com/org/func").is_none());
    }

    #[test]
    fn test_parse_oci_func_ref_git_returns_none() {
        assert!(parse_oci_func_ref("gitlab.com/funcs/my-git-repo@v1.0.0").is_none());
        assert!(parse_oci_func_ref("gitlab.com/funcs/repo@main").is_none());
    }

    #[test]
    fn test_sanitize_image_key() {
        assert_eq!(
            sanitize_image_key("registry.gitlab.com/org/func:1.0"),
            "registry.gitlab.com_org_func_1.0"
        );
        assert_eq!(
            sanitize_image_key("docker.io/library/alpine:3"),
            "docker.io_library_alpine_3"
        );
    }

    #[test]
    fn test_sanitize_image_key_no_traversal() {
        assert!(!sanitize_image_key("../../../etc/passwd:latest").contains(".."));
    }

    #[test]
    fn test_func_cache_dir_under_cache() {
        if let Some(dir) = func_cache_dir() {
            assert!(dir.ends_with("oci-funcs"));
        }
    }

    #[test]
    fn test_fetch_oci_func_yml_rejects_path_traversal() {
        let oci_ref = OciFuncRef {
            image: "registry.example.com/img:1.0".to_string(),
            func_path: "../../etc/passwd".to_string(),
        };
        let result = fetch_oci_func_yml(&oci_ref);
        assert!(result.is_err());
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("escapes the image cache directory")
        );
    }
}
