use oxrdf::{Graph, NamedNode, TermRef, TripleRef};
use oxrdfio::{RdfFormat, RdfParser};
use std::collections::{BTreeSet, HashSet, VecDeque};
use std::error::Error;
use std::fs::{self, File};
use std::io::Cursor;
use std::io::Write;
use std::path::{Path, PathBuf};
use url::Url;

const MF_MANIFEST: &str = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#Manifest";
const MF_INCLUDE: &str = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#include";
const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
const SHT_VALIDATE: &str = "http://www.w3.org/ns/shacl-test#Validate";

fn main() -> Result<(), Box<dyn Error>> {
    let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
    let suite_roots = vec![
        crate_dir
            .join("tests")
            .join("test-suite")
            .join("manifest.ttl"),
        crate_dir
            .join("tests")
            .join("data-shapes")
            .join("data-shapes-test-suite")
            .join("tests")
            .join("manifest.ttl"),
    ];
    let dest_path = PathBuf::from(std::env::var("OUT_DIR")?).join("generated_manifest_tests.rs");

    if !suite_roots.iter().any(|m| m.exists()) {
        write_empty_manifest_tests(&dest_path)?;
        for root in suite_roots {
            emit_rerun_metadata(&root);
        }
        return Ok(());
    }

    let mut manifests = BTreeSet::new();
    for root in suite_roots.iter().filter(|p| p.exists()) {
        manifests.extend(collect_manifest_files(root)?);
    }

    let mut file = File::create(&dest_path)?;

    writeln!(file, "// @generated by build.rs - do not edit manually")?;
    writeln!(file, "generate_test_cases! {{")?;
    for manifest in manifests {
        let rel = manifest.strip_prefix(&crate_dir).unwrap_or(&manifest);
        let rel_str = rel.to_string_lossy().replace('\\', "/");
        let test_name = path_to_ident(&rel_str);
        writeln!(file, "    {test_name}: \"{rel_str}\",")?;
    }
    writeln!(file, "}}")?;

    for root in suite_roots {
        emit_rerun_metadata(&root);
    }

    Ok(())
}

fn write_empty_manifest_tests(path: &Path) -> Result<(), Box<dyn Error>> {
    let mut file = File::create(path)?;
    writeln!(file, "// @generated by build.rs - no manifests found")?;
    writeln!(file, "generate_test_cases! {{}}")?;
    Ok(())
}

fn emit_rerun_metadata(root_manifest: &Path) {
    println!("cargo:rerun-if-changed={}", root_manifest.display());
}

fn path_to_ident(path: &str) -> String {
    let mut trimmed = path.strip_prefix("tests/").unwrap_or(path);
    if let Some(stripped) = trimmed.strip_suffix(".ttl") {
        trimmed = stripped;
    }
    let mut ident = String::from("test_");
    for ch in trimmed.chars() {
        let c = if ch.is_ascii_alphanumeric() {
            ch.to_ascii_lowercase()
        } else {
            '_'
        };
        ident.push(c);
    }
    while ident.contains("__") {
        ident = ident.replace("__", "_");
    }
    if ident.ends_with('_') {
        ident.pop();
    }
    ident
}

fn collect_manifest_files(root_manifest: &Path) -> Result<BTreeSet<PathBuf>, Box<dyn Error>> {
    let mut to_visit = VecDeque::new();
    let mut visited = HashSet::new();
    let mut manifests_with_tests = BTreeSet::new();

    to_visit.push_back(root_manifest.to_path_buf());

    while let Some(manifest_path) = to_visit.pop_front() {
        let canonical = manifest_path.canonicalize()?;
        if !visited.insert(canonical.clone()) {
            continue;
        }

        let manifest_graph = parse_manifest(&canonical)?;
        if has_validate_tests(&manifest_graph) {
            manifests_with_tests.insert(canonical.clone());
        }

        for include in manifest_includes(&manifest_graph, &canonical)? {
            to_visit.push_back(include);
        }
    }

    Ok(manifests_with_tests)
}

fn parse_manifest(path: &Path) -> Result<Graph, Box<dyn Error>> {
    let content = load_manifest_text(path)?;
    let base_url = Url::from_file_path(path).map_err(|_| "invalid file path")?;
    let mut graph = Graph::new();
    let parser = RdfParser::from_format(RdfFormat::Turtle).with_base_iri(base_url.as_str())?;
    for quad in parser.for_reader(Cursor::new(content.as_bytes())) {
        let quad = quad?;
        graph.insert(TripleRef::new(&quad.subject, &quad.predicate, &quad.object));
    }
    Ok(graph)
}

fn load_manifest_text(path: &Path) -> Result<String, Box<dyn Error>> {
    let content = fs::read_to_string(path)?;
    let has_rdf_prefix = content.contains("@prefix rdf:")
        || content.contains("@PREFIX rdf:")
        || content.contains("PREFIX rdf:");
    if has_rdf_prefix {
        Ok(content)
    } else {
        Ok(format!(
            "@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n{}",
            content
        ))
    }
}

fn has_validate_tests(graph: &Graph) -> bool {
    let rdf_type = NamedNode::new(RDF_TYPE).expect("valid IRI");
    let sht_validate = NamedNode::new(SHT_VALIDATE).expect("valid IRI");
    graph.iter().any(|triple| {
        triple.predicate == rdf_type.as_ref() && triple.object == sht_validate.as_ref().into()
    })
}

fn manifest_includes(graph: &Graph, manifest_path: &Path) -> Result<Vec<PathBuf>, Box<dyn Error>> {
    let rdf_type = NamedNode::new(RDF_TYPE)?;
    let mf_manifest = NamedNode::new(MF_MANIFEST)?;
    let mf_include = NamedNode::new(MF_INCLUDE)?;

    let manifest_node = graph
        .subjects_for_predicate_object(rdf_type.as_ref(), mf_manifest.as_ref())
        .next()
        .ok_or("mf:Manifest subject not found")?;

    let mut includes = Vec::new();
    for include in graph.objects_for_subject_predicate(manifest_node, mf_include.as_ref()) {
        match include {
            TermRef::NamedNode(nn) => {
                let iri = nn.as_str();
                match Url::parse(iri) {
                    Ok(url) if url.scheme() == "file" => {
                        if let Ok(pb) = url.to_file_path() {
                            includes.push(pb);
                        }
                    }
                    Ok(url) if url.scheme() == "urn" => {
                        if let Some(parent) = manifest_path.parent() {
                            let path = url.path().trim_start_matches('/');
                            if !path.is_empty() {
                                includes.push(parent.join(path));
                            }
                        }
                    }
                    _ => {
                        if let Some(parent) = manifest_path.parent() {
                            includes.push(parent.join(iri));
                        }
                    }
                }
            }
            TermRef::Literal(lit) => {
                if let Some(parent) = manifest_path.parent() {
                    includes.push(parent.join(lit.value()));
                }
            }
            _ => {}
        }
    }
    Ok(includes)
}
