use std::path::{Path, PathBuf};

use rustc_hash::FxHashSet;
use serde_json::Value;

pub(super) fn has_matching_manifest_json(
    root: &Path,
    discovered_files: &[PathBuf],
    candidate_index: Option<&super::registry::ConfigCandidateIndex>,
    matches_manifest: impl Fn(&Value) -> bool,
) -> bool {
    manifest_json_candidates(root, discovered_files)
        .into_iter()
        // Outside production mode the discovery walk already recorded which
        // directories actually contain a `manifest.json`, so skip filesystem
        // reads for candidate directories that have none. In production
        // (`None`) fall back to probing every candidate.
        .filter(|path| match candidate_index {
            Some(index) => path
                .parent()
                .is_some_and(|dir| index.dir_contains(dir, std::ffi::OsStr::new("manifest.json"))),
            None => true,
        })
        .any(|path| {
            let Ok(source) = std::fs::read_to_string(path) else {
                return false;
            };
            parse_manifest_json(&source).is_some_and(|manifest| matches_manifest(&manifest))
        })
}

pub(super) fn parse_manifest_json(source: &str) -> Option<Value> {
    serde_json::from_str(source).ok()
}

fn manifest_json_candidates(root: &Path, discovered_files: &[PathBuf]) -> Vec<PathBuf> {
    let mut seen = FxHashSet::default();
    let mut candidates = Vec::new();
    push_manifest_candidate(root, &mut seen, &mut candidates);

    for file in discovered_files {
        let mut current = file.parent();
        while let Some(dir) = current {
            if !dir.starts_with(root) {
                break;
            }
            push_manifest_candidate(dir, &mut seen, &mut candidates);
            if dir == root {
                break;
            }
            current = dir.parent();
        }
    }

    candidates
}

fn push_manifest_candidate(
    dir: &Path,
    seen: &mut FxHashSet<PathBuf>,
    candidates: &mut Vec<PathBuf>,
) {
    let candidate = dir.join("manifest.json");
    if seen.insert(candidate.clone()) {
        candidates.push(candidate);
    }
}
