#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

#[path = "build_support/agent_templates.rs"]
mod agent_templates;
#[path = "build_support/edit_ops_spec.rs"]
mod edit_ops_spec;

use agent_templates::generate_codex_agent_templates;
use edit_ops_spec::{
    EditOpsSpec, NestedNodeRule, RuntimeGetRule, RuntimeSetMode, RuntimeSetRule, load_edit_ops_spec,
};
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};

// Project-local assets installed by `govctl init-skills`.
// Plugin/global-only skills such as `init` are intentionally omitted. [[RFC-0002:C-GLOBAL-COMMANDS]]
const DISTRIBUTED_SKILL_DIRS: &[&str] = &[
    "discuss",
    "gov",
    "quick",
    "spec",
    "rfc-writer",
    "adr-writer",
    "wi-writer",
    "guard-writer",
    "commit",
    "migrate",
    "decision-analysis",
    "detach",
];

fn main() {
    // Recompile if any embedded .claude/ assets change
    println!("cargo:rerun-if-changed=.claude/skills");
    println!("cargo:rerun-if-changed=.claude/agents");

    // Edit rules SSOT + schema (ADR-0030)
    println!("cargo:rerun-if-changed=gov/schema/edit-ops.schema.json");
    println!("cargo:rerun-if-changed=gov/schema/edit-ops.json");

    generate_skill_assets().expect("failed to generate skill asset manifest");
    generate_edit_rules().expect("failed to generate edit rules from SSOT");
    generate_codex_agent_templates().expect("failed to generate codex agent templates");
}

fn generate_skill_assets() -> Result<(), Box<dyn Error>> {
    let skill_root = Path::new(".claude/skills");
    let mut files = Vec::new();
    for skill_dir in DISTRIBUTED_SKILL_DIRS {
        collect_files(&skill_root.join(skill_dir), &mut files)?;
    }
    files.sort();

    let mut out = String::new();
    out.push_str("// @generated by build.rs from distributed .claude/skills/* bundles\n");
    out.push_str("// Do not edit manually.\n\n");
    out.push_str("pub const SKILL_ASSETS: &[(&str, &str)] = &[\n");
    for file in files {
        let skill_rel = file
            .strip_prefix(skill_root)
            .map_err(|e| format!("failed to relativize skill asset {}: {e}", file.display()))?;
        let skill_rel = path_to_slash_string(skill_rel)?;
        let output_rel = format!("skills/{skill_rel}");
        let source_rel = format!("/.claude/skills/{skill_rel}");
        out.push_str(&format!(
            "    ({:?}, include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), {:?}))),\n",
            output_rel, source_rel
        ));
    }
    out.push_str("];\n");

    let out_dir = std::env::var("OUT_DIR")?;
    let out_path = Path::new(&out_dir).join("skill_assets.rs");
    fs::write(out_path, out)?;
    Ok(())
}

fn collect_files(root: &Path, files: &mut Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
    for entry in
        fs::read_dir(root).map_err(|e| format!("failed to read {}: {e}", root.display()))?
    {
        let entry = entry?;
        let path = entry.path();
        let file_type = entry.file_type()?;
        if file_type.is_dir() {
            collect_files(&path, files)?;
        } else if file_type.is_file() {
            files.push(path);
        }
    }
    Ok(())
}

fn path_to_slash_string(path: &Path) -> Result<String, Box<dyn Error>> {
    let mut parts = Vec::new();
    for component in path.components() {
        let component = component.as_os_str().to_str().ok_or_else(|| {
            format!(
                "skill asset path is not valid UTF-8: {}",
                path.to_string_lossy()
            )
        })?;
        parts.push(component.to_string());
    }
    Ok(parts.join("/"))
}

fn generate_edit_rules() -> Result<(), Box<dyn Error>> {
    let spec_path = Path::new("gov/schema/edit-ops.json");
    let schema_path = Path::new("gov/schema/edit-ops.schema.json");
    let spec = load_edit_ops_spec(spec_path, schema_path)?;
    let rendered = render_edit_rules(&spec)?;
    let rendered_runtime = render_edit_runtime(&spec)?;
    let out_dir = std::env::var("OUT_DIR")?;
    let out_path = Path::new(&out_dir).join("edit_rules_generated.rs");
    let out_runtime_path = Path::new(&out_dir).join("edit_runtime_generated.rs");
    fs::write(out_path, rendered)?;
    fs::write(out_runtime_path, rendered_runtime)?;
    Ok(())
}

fn render_edit_rules(spec: &EditOpsSpec) -> Result<String, Box<dyn Error>> {
    let mut out = String::new();
    out.push_str("// @generated by build.rs from gov/schema/edit-ops.json\n");
    out.push_str("// Do not edit manually.\n\n");
    out.push_str(&format!(
        "#[cfg(test)]\npub const EDIT_RULES_VERSION: u32 = {};\n\n",
        spec.version
    ));

    out.push_str("define_alias_resolver! {\n");
    for alias in &spec.aliases {
        out.push_str(&format!(
            "    ({:?}, {:?}),\n",
            alias.alias, alias.canonical
        ));
    }
    out.push_str("}\n\n");

    out.push_str("define_legacy_prefix_resolver! {\n");
    for rule in &spec.legacy_prefixes {
        out.push_str(&format!("    ({:?}, [", rule.prefix));
        for (idx, field) in rule.allowed_fields.iter().enumerate() {
            if idx > 0 {
                out.push_str(", ");
            }
            out.push_str(&format!("{field:?}"));
        }
        out.push_str("]),\n");
    }
    out.push_str("}\n\n");

    out.push_str("pub const SIMPLE_RULES: &[SimpleFieldRule] = &[\n");
    for field in &spec.simple_rules {
        let kind = match field.kind.as_str() {
            "scalar" => "Scalar",
            "list" => "List",
            other => return Err(format!("unknown simple field kind in SSOT: {other}").into()),
        };
        out.push_str("    SimpleFieldRule {\n");
        out.push_str(&format!("        artifact: {:?},\n", field.artifact));
        out.push_str(&format!("        name: {:?},\n", field.name));
        out.push_str(&format!("        kind: FieldKind::{kind},\n"));
        out.push_str("        verbs: &[\n");
        for verb in &field.verbs {
            out.push_str(&format!("            {verb:?},\n"));
        }
        out.push_str("        ],\n");
        out.push_str("    },\n");
    }
    out.push_str("];\n\n");

    let mut nested_defs = String::new();
    let mut nested_roots = String::new();
    for root in &spec.nested_rules {
        let const_name = format!(
            "NESTED_NODE_{}_{}_ROOT",
            root.artifact.to_uppercase(),
            sanitize_const_fragment(&root.root)
        );
        render_nested_node_defs(&mut nested_defs, &const_name, &root.node)?;
        nested_roots.push_str("    NestedRootRule {\n");
        nested_roots.push_str(&format!("        artifact: {:?},\n", root.artifact));
        nested_roots.push_str(&format!("        root: {:?},\n", root.root));
        nested_roots.push_str(&format!(
            "        content_path: {},\n",
            runtime_path_expr(&root.content_path)
        ));
        nested_roots.push_str(&format!("        node: &{},\n", const_name));
        nested_roots.push_str("    },\n");
    }

    out.push_str(&nested_defs);
    out.push_str("pub const NESTED_RULES: &[NestedRootRule] = &[\n");
    out.push_str(&nested_roots);
    out.push_str("];\n");

    out.push('\n');
    out.push_str("pub const VALIDATION_RULES: &[FieldValidationRule] = &[\n");
    for rule in &spec.validation_rules {
        let kind = match rule.rule.as_str() {
            "semver" => "Semver",
            "clause_superseded_by" => "ClauseSupersededBy",
            "artifact_ref" => "ArtifactRef",
            "enum_value" => "EnumValue",
            other => return Err(format!("unknown validation rule in SSOT: {other}").into()),
        };
        out.push_str("    FieldValidationRule {\n");
        out.push_str(&format!("        artifact: {:?},\n", rule.artifact));
        out.push_str(&format!("        field: {:?},\n", rule.field));
        out.push_str(&format!("        kind: ValidationKind::{kind},\n"));
        out.push_str("    },\n");
    }
    out.push_str("];\n");

    Ok(out)
}

fn sanitize_const_fragment(name: &str) -> String {
    name.chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() {
                ch.to_ascii_uppercase()
            } else {
                '_'
            }
        })
        .collect()
}

fn render_nested_node_defs(
    out: &mut String,
    const_name: &str,
    node: &NestedNodeRule,
) -> Result<(), Box<dyn Error>> {
    match node {
        NestedNodeRule::Scalar { verbs, set_mode } => {
            out.push_str(&format!(
                "const {const_name}: NestedNodeRule = NestedNodeRule {{\n"
            ));
            out.push_str("    kind: NestedNodeKind::Scalar,\n");
            out.push_str(&format!("    verbs: {},\n", render_verbs_expr(verbs)));
            out.push_str("    text_key: None,\n");
            out.push_str(&format!(
                "    set_mode: {},\n",
                render_nested_scalar_mode_expr(set_mode.as_ref())?
            ));
            out.push_str("    item: None,\n");
            out.push_str("    fields: &[],\n");
            out.push_str("};\n\n");
        }
        NestedNodeRule::Object { verbs, fields } => {
            for field in fields {
                let child_const = format!("{const_name}_{}", sanitize_const_fragment(&field.name));
                render_nested_node_defs(out, &child_const, &field.node)?;
            }
            let fields_const = format!("{const_name}_FIELDS");
            out.push_str(&format!("const {fields_const}: &[NestedChildRule] = &[\n"));
            for field in fields {
                let child_const = format!("{const_name}_{}", sanitize_const_fragment(&field.name));
                out.push_str("    NestedChildRule {\n");
                out.push_str(&format!("        name: {:?},\n", field.name));
                out.push_str(&format!("        node: &{},\n", child_const));
                out.push_str("    },\n");
            }
            out.push_str("];\n");
            out.push_str(&format!(
                "const {const_name}: NestedNodeRule = NestedNodeRule {{\n"
            ));
            out.push_str("    kind: NestedNodeKind::Object,\n");
            out.push_str(&format!("    verbs: {},\n", render_verbs_expr(verbs)));
            out.push_str("    text_key: None,\n");
            out.push_str("    set_mode: None,\n");
            out.push_str("    item: None,\n");
            out.push_str(&format!("    fields: {},\n", fields_const));
            out.push_str("};\n\n");
        }
        NestedNodeRule::List {
            verbs,
            text_key,
            item,
        } => {
            let item_const = format!("{const_name}_ITEM");
            render_nested_node_defs(out, &item_const, item)?;
            out.push_str(&format!(
                "const {const_name}: NestedNodeRule = NestedNodeRule {{\n"
            ));
            out.push_str("    kind: NestedNodeKind::List,\n");
            out.push_str(&format!("    verbs: {},\n", render_verbs_expr(verbs)));
            out.push_str(&format!(
                "    text_key: {},\n",
                match text_key {
                    Some(key) => format!("Some({key:?})"),
                    None => "None".to_string(),
                }
            ));
            out.push_str("    set_mode: None,\n");
            out.push_str(&format!("    item: Some(&{}),\n", item_const));
            out.push_str("    fields: &[],\n");
            out.push_str("};\n\n");
        }
    }
    Ok(())
}

fn render_verbs_expr(verbs: &[String]) -> String {
    let mut out = String::from("&[");
    for (idx, verb) in verbs.iter().enumerate() {
        if idx > 0 {
            out.push_str(", ");
        }
        out.push_str(&format!("{verb:?}"));
    }
    out.push(']');
    out
}

fn render_nested_scalar_mode_expr(mode: Option<&RuntimeSetMode>) -> Result<String, Box<dyn Error>> {
    match mode {
        None => Ok("None".to_string()),
        Some(RuntimeSetMode::String) => Ok("Some(NestedScalarMode::String)".to_string()),
        Some(RuntimeSetMode::Integer) => Ok("Some(NestedScalarMode::Integer)".to_string()),
        Some(RuntimeSetMode::Enum {
            allowed,
            invalid_msg,
            code,
        }) => {
            let allowed_expr = {
                let mut expr = String::from("&[");
                for (idx, value) in allowed.iter().enumerate() {
                    if idx > 0 {
                        expr.push_str(", ");
                    }
                    expr.push_str(&format!("{value:?}"));
                }
                expr.push(']');
                expr
            };
            let code_expr = match code {
                Some(code) => format!("Some(DiagnosticCode::{})", diagnostic_code_variant(code)?),
                None => "None".to_string(),
            };
            Ok(format!(
                "Some(NestedScalarMode::Enum {{ allowed: {}, invalid_msg: {:?}, code: {} }})",
                allowed_expr, invalid_msg, code_expr
            ))
        }
    }
}

fn diagnostic_code_variant(code: &str) -> Result<&str, Box<dyn Error>> {
    code.strip_prefix('E')
        .map(|_| code)
        .ok_or_else(|| format!("invalid diagnostic code in SSOT: {code}").into())
}

fn render_edit_runtime(spec: &EditOpsSpec) -> Result<String, Box<dyn Error>> {
    let mut out = String::new();
    out.push_str("// @generated by build.rs from gov/schema/edit-ops.json\n");
    out.push_str("// Do not edit manually.\n\n");
    out.push_str("const RUNTIME_FIELDS: &[RuntimeFieldEntry] = &[\n");
    for field in &spec.runtime_fields {
        out.push_str("    RuntimeFieldEntry {\n");
        out.push_str(&format!(
            "        artifact: {},\n",
            runtime_artifact_expr(&field.artifact)?
        ));
        out.push_str(&format!("        field: {:?},\n", field.name));
        out.push_str(&format!(
            "        get: {},\n",
            runtime_get_expr(field.get.as_ref())?
        ));
        out.push_str(&format!(
            "        set: {},\n",
            runtime_set_expr(field.set.as_ref())?
        ));
        out.push_str(&format!(
            "        list_path: {},\n",
            runtime_list_path_expr(field.list_path.as_ref())
        ));
        out.push_str("    },\n");
    }
    out.push_str("];\n");
    Ok(out)
}

fn runtime_artifact_expr(artifact: &str) -> Result<&'static str, Box<dyn Error>> {
    match artifact {
        "rfc" => Ok("ArtifactType::Rfc"),
        "clause" => Ok("ArtifactType::Clause"),
        "adr" => Ok("ArtifactType::Adr"),
        "work" => Ok("ArtifactType::WorkItem"),
        "guard" => Ok("ArtifactType::Guard"),
        other => Err(format!("unknown runtime artifact in SSOT: {other}").into()),
    }
}

fn runtime_path_expr(path: &[String]) -> String {
    let mut out = String::from("&[");
    for (idx, seg) in path.iter().enumerate() {
        if idx > 0 {
            out.push_str(", ");
        }
        out.push_str(&format!("{seg:?}"));
    }
    out.push(']');
    out
}

fn runtime_list_path_expr(path: Option<&Vec<String>>) -> String {
    match path {
        Some(path) => format!("Some({})", runtime_path_expr(path)),
        None => "None".to_string(),
    }
}

fn runtime_get_expr(get: Option<&RuntimeGetRule>) -> Result<String, Box<dyn Error>> {
    let Some(get) = get else {
        return Ok("None".to_string());
    };
    let render = match get.render.as_str() {
        "scalar" => "RenderMode::Scalar".to_string(),
        "csv_strings" => "RenderMode::CsvStrings".to_string(),
        "line_strings" => "RenderMode::LineStrings".to_string(),
        "text_lines" => {
            let text_key = get.text_key.as_ref().ok_or_else(|| {
                "runtime get render=text_lines requires text_key in SSOT".to_string()
            })?;
            format!("RenderMode::TextLines {{ text_key: {:?} }}", text_key)
        }
        "status_lines" => {
            let status_key = get.status_key.as_ref().ok_or_else(|| {
                "runtime get render=status_lines requires status_key in SSOT".to_string()
            })?;
            let text_key = get.text_key.as_ref().ok_or_else(|| {
                "runtime get render=status_lines requires text_key in SSOT".to_string()
            })?;
            format!(
                "RenderMode::StatusLines {{ status_key: {:?}, text_key: {:?} }}",
                status_key, text_key
            )
        }
        other => return Err(format!("unknown runtime get render in SSOT: {other}").into()),
    };
    Ok(format!(
        "Some(SimpleFieldSpec {{ path: {}, render: {} }})",
        runtime_path_expr(&get.path),
        render
    ))
}

fn runtime_set_expr(set: Option<&RuntimeSetRule>) -> Result<String, Box<dyn Error>> {
    let Some(set) = set else {
        return Ok("None".to_string());
    };
    let mode = match &set.mode {
        RuntimeSetMode::String => "SetMode::String".to_string(),
        RuntimeSetMode::Integer => "SetMode::Integer".to_string(),
        RuntimeSetMode::Enum {
            allowed,
            invalid_msg,
            code,
        } => {
            let allowed_expr = runtime_path_expr(allowed);
            let code_expr = match code {
                Some(code) => format!("Some(DiagnosticCode::{code})"),
                None => "None".to_string(),
            };
            format!(
                "SetMode::Enum {{ allowed: {}, invalid_msg: {:?}, code: {} }}",
                allowed_expr, invalid_msg, code_expr
            )
        }
    };
    Ok(format!(
        "Some(SimpleSetSpec {{ path: {}, mode: {} }})",
        runtime_path_expr(&set.path),
        mode
    ))
}
