//! Agent assembler — composes markdown from manifest + blocks.
//! Output is deterministic: same manifest + blocks → byte-identical .md.

use crate::manifest::Manifest;
use crate::registry_client;
use crate::substrate;
use std::fs;
use std::path::Path;

pub fn assemble(m: &Manifest, blocks_dir: &Path) -> Result<String, String> {
    // Substrate role expansion uses the kit root (parent of _blocks/).
    let root = blocks_dir
        .parent()
        .ok_or_else(|| "blocks_dir has no parent (can't locate _roles/ and _capabilities/)".to_string())?;

    let mut out = String::new();

    write_frontmatter(m, &mut out);
    write_role(m, &mut out);
    write_substrate(m, root, &mut out)?;
    write_rule_blocks(m, &mut out)?;
    write_blocks(m, blocks_dir, &mut out)?;
    write_domain_scope(m, &mut out);
    write_handoffs(m, &mut out);
    write_output_format(m, &mut out);
    write_forbidden(m, &mut out);
    write_references(m, &mut out);

    Ok(out)
}

fn write_substrate(m: &Manifest, root: &Path, out: &mut String) -> Result<(), String> {
    let Some(role) = &m.substrate_role else {
        return Ok(());
    };
    let section = substrate::build_substrate_section(root, role)?;
    out.push_str(&section);
    Ok(())
}

fn write_frontmatter(m: &Manifest, out: &mut String) {
    let desc = m.description.replace('\n', " ");
    out.push_str("---\n");
    out.push_str(&format!("name: {}\n", m.name));
    out.push_str(&format!("description: {}\n", desc.trim()));
    out.push_str(&format!("tools: {}\n", m.tools.join(", ")));
    out.push_str(&format!("model: {}\n", m.model));
    // v0.39: optional provider for DNA-resolved kei agent dispatch.
    if let Some(prov) = &m.provider {
        if !prov.is_empty() {
            out.push_str(&format!("provider: {}\n", prov));
        }
    }
    out.push_str("---\n\n");
    out.push_str(&format!(
        "<!-- GENERATED by _assembler (Rust) from _manifests/{}.toml — DO NOT EDIT. Edit the manifest. -->\n\n",
        m.name
    ));
}

fn write_role(m: &Manifest, out: &mut String) {
    out.push_str("# ROLE\n\n");
    out.push_str(m.role.trim());
    out.push_str("\n\n");
}

fn write_blocks(m: &Manifest, blocks_dir: &Path, out: &mut String) -> Result<(), String> {
    for block in &m.blocks {
        let path = blocks_dir.join(format!("{block}.md"));
        let text = fs::read_to_string(&path)
            .map_err(|e| format!("read {}: {e}", path.display()))?;
        out.push_str(text.trim());
        out.push_str("\n\n");
    }
    Ok(())
}

fn write_rule_blocks(m: &Manifest, out: &mut String) -> Result<(), String> {
    if m.rule_blocks.is_empty() {
        return Ok(());
    }
    let db_path = registry_client::default_db_path();
    if !db_path.exists() {
        eprintln!("warn [assembler]: registry not found at {} — skipping rule_blocks", db_path.display());
        return Ok(());
    }
    let conn = registry_client::open_read_only(&db_path)?;
    for name in &m.rule_blocks {
        match registry_client::find_rule(&conn, name) {
            Ok(Some(body)) => {
                out.push_str(&format!("<!-- RULE: {name} -->\n"));
                out.push_str(body.trim());
                out.push_str("\n<!-- /RULE -->\n\n");
            }
            Ok(None) => {
                return Err(format!(
                    "rule_block '{name}' not found in registry — \
                     run `kei-decompose decompose-rules` first or remove from manifest"
                ));
            }
            Err(e) => return Err(format!("registry lookup for '{name}': {e}")),
        }
    }
    Ok(())
}

fn write_domain_scope(m: &Manifest, out: &mut String) {
    out.push_str("# DOMAIN SCOPE\n\n**In:**\n");
    for item in &m.domain_in {
        out.push_str(&format!("- {item}\n"));
    }
    out.push_str("\n**Out (hand off):**\n");
    for h in &m.handoff {
        out.push_str(&format!("- `{}` — {}\n", h.target, h.trigger));
    }
    out.push('\n');
}

fn write_handoffs(m: &Manifest, out: &mut String) {
    out.push_str("# HANDOFFS\n\n");
    for h in &m.handoff {
        out.push_str(&format!("- **{}** — {}\n", h.target, h.trigger));
    }
    out.push('\n');
}

fn write_output_format(m: &Manifest, out: &mut String) {
    out.push_str("# OUTPUT FORMAT\n\n```\n");
    out.push_str(&format!("=== {} REPORT ===\n", m.name.to_uppercase()));
    out.push_str("Goal: <one-line>\n");
    out.push_str("Scope: <in / out>\n");
    out.push_str("Plan: <N steps>\n");
    out.push_str("Executed: <files touched, LOC delta>\n");
    out.push_str("Verify: <each criterion pass/fail>\n");
    out.push_str("Evidence grades: <E1-E6 for each major claim>\n");
    out.push_str("Handoffs made: <list>\n");
    for extra in &m.output_extra_fields {
        out.push_str(extra);
        out.push('\n');
    }
    out.push_str("Blockers / next: <list>\n");
    out.push_str("```\n\n");
}

fn write_forbidden(m: &Manifest, out: &mut String) {
    out.push_str("# FORBIDDEN\n\n");
    for item in &m.forbidden_domain {
        out.push_str(&format!("- {item}\n"));
    }
    out.push('\n');
}

fn write_references(m: &Manifest, out: &mut String) {
    out.push_str("# REFERENCES\n\n");
    out.push_str("- `~/.claude/CLAUDE.md` — baseline umbrella\n");
    out.push_str("- `~/.claude/memory/MEMORY.md` — memory index (adjust if your Claude Code user-slug path differs)\n");
    if let Some(mp) = &m.memory_project {
        out.push_str(&format!(
            "- `~/.claude/memory/{mp}` — project memory (adjust path if needed)\n"
        ));
    }
    if let Some(pc) = &m.project_claudemd {
        out.push_str(&format!("- `{pc}` — project CLAUDE.md\n"));
    }
    if let Some(refs) = &m.references {
        // Open registry for path-atom resolution. Missing DB is non-fatal —
        // references then fall through unchanged (advisory only).
        let conn = {
            let db_path = registry_client::default_db_path();
            if db_path.exists() {
                registry_client::open_read_only(&db_path).ok()
            } else {
                None
            }
        };
        for r in &refs.extra {
            let resolved = resolve_path_atom_ref(r, conn.as_ref());
            out.push_str(&format!("- `{resolved}`\n"));
        }
    }
}

/// Resolve a `path:NAME/file.md` reference to an opaque content-addressed
/// form `{path::NAME}/file.md` if `NAME` is a registered path-atom.
///
/// Behaviour:
/// - Input does not start with `path:` → return unchanged.
/// - Input starts with `path:` but lookup fails (no atom / not a path-atom /
///   no registry) → emit a stderr warning and return the input unchanged.
///   This is advisory: the warn surfaces typos in manifests but never
///   blocks rendering.
/// - Lookup succeeds → return `{path::NAME}/<suffix>`.
fn resolve_path_atom_ref(r: &str, conn: Option<&rusqlite::Connection>) -> String {
    let Some(rest) = r.strip_prefix("path:") else {
        return r.to_string();
    };
    let (name, suffix) = match rest.split_once('/') {
        Some((n, s)) => (n, s),
        // `path:NAME` with no `/file` part — atom-only ref. Resolve to
        // `{path::NAME}` so the public output is still opaque.
        None => (rest, ""),
    };
    let Some(c) = conn else {
        eprintln!(
            "warn [assembler]: 'path:{name}' reference but registry DB not open — passing through"
        );
        return r.to_string();
    };
    match registry_client::is_path_atom(c, name) {
        Ok(true) => {
            if suffix.is_empty() {
                format!("{{path::{name}}}")
            } else {
                format!("{{path::{name}}}/{suffix}")
            }
        }
        Ok(false) => {
            eprintln!(
                "warn [assembler]: 'path:{name}' not found in registry as path-atom — passing through"
            );
            r.to_string()
        }
        Err(e) => {
            eprintln!("warn [assembler]: registry lookup for path-atom '{name}': {e}");
            r.to_string()
        }
    }
}

#[cfg(test)]
mod write_references_tests {
    use super::resolve_path_atom_ref;

    #[test]
    fn passthrough_non_path_ref() {
        let r = "~/.claude/memory/foo.md";
        assert_eq!(resolve_path_atom_ref(r, None), r);
    }

    #[test]
    fn passthrough_path_ref_when_no_db() {
        // No registry conn → emit warn (suppressed in test) + passthrough.
        let r = "path:user-memory/foo.md";
        assert_eq!(resolve_path_atom_ref(r, None), r);
    }
}
