//! Phase 5: discover a project *tree* — every `picloud.toml` under a root
//! directory — and assemble the wire tree bundle the server's `/tree/{plan,
//! apply}` endpoints consume. Each manifest becomes one node (app or group);
//! the server resolves ancestry from its own group tree, so the on-disk
//! nesting is organizational, not authoritative here.

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

use anyhow::{bail, Context, Result};
use serde_json::{json, Value};

use crate::cmds::plan::build_bundle;
use crate::manifest::{Manifest, MANIFEST_FILE};

/// Find every `picloud.toml` under `root` (recursively), skipping the
/// `.picloud/` link-state dir, `.git`, and `scripts/` source dirs. Per-env
/// overlays (`picloud.<env>.toml`) are not matched — only the base filename.
pub fn find_manifests(root: &Path) -> Result<Vec<PathBuf>> {
    let mut out = Vec::new();
    walk(root, &mut out)?;
    out.sort();
    Ok(out)
}

fn walk(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
    let entries =
        std::fs::read_dir(dir).with_context(|| format!("reading directory {}", dir.display()))?;
    for entry in entries {
        let entry = entry?;
        let name = entry.file_name();
        let ft = entry.file_type()?;
        if ft.is_dir() {
            if matches!(
                name.to_str(),
                Some(".picloud" | ".git" | "scripts" | "target")
            ) {
                continue;
            }
            walk(&entry.path(), out)?;
        } else if name == MANIFEST_FILE {
            out.push(entry.path());
        }
    }
    Ok(())
}

/// Build the wire tree bundle (`{ nodes: [{kind, slug, bundle}] }`) from every
/// manifest under `root`. Returns the JSON plus the node count. Rejects a tree
/// that names the same (kind, slug) twice.
pub fn build_tree(root: &Path, env: Option<&str>) -> Result<(Value, usize)> {
    let paths = find_manifests(root)?;
    if paths.is_empty() {
        bail!(
            "no {MANIFEST_FILE} found under {} — nothing to apply",
            root.display()
        );
    }
    let mut nodes = Vec::with_capacity(paths.len());
    let mut seen: HashSet<(String, String)> = HashSet::new();
    for path in &paths {
        let manifest = Manifest::load_with_env(path, env)
            .with_context(|| format!("loading {}", path.display()))?;
        let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
        let bundle = build_bundle(&manifest, base_dir)?;
        let kind = if manifest.is_group() { "group" } else { "app" };
        let slug = manifest.slug().to_string();
        if !seen.insert((kind.to_string(), slug.clone())) {
            bail!("project tree declares {kind} `{slug}` more than once");
        }
        nodes.push(json!({ "kind": kind, "slug": slug, "bundle": bundle }));
    }
    let count = nodes.len();
    Ok((json!({ "nodes": nodes }), count))
}
