//! Source emitters — typed config → Rust source string.
//!
//! Two entry points:
//!
//!   * [`emit_kind`]   — one kind's `.rs` file. Generates struct +
//!                       `KubeResource` impl. Idempotent: same input
//!                       always produces the same string (no clock,
//!                       no env, no random).
//!   * [`emit_module`] — module-level `.rs` (e.g. `core_v1/mod.rs`)
//!                       declaring every per-kind submodule in the
//!                       group. Pure data-driven.

use crate::catalog::KindEntry;
use crate::openapi::KindShape;

/// Generated-file header. Embedded into every emit so consumers can
/// recognize generator output + know what to expect on regenerate.
const HEADER: &str = "\
//! GENERATED — DO NOT EDIT by hand. Source: engenho-kube-codegen.
//!
//! Regenerate via `cargo run -p engenho-kube-codegen -- \\
//!     --schema engenho-types/vendor/openapi/v1.34.0 \\
//!     --output engenho-types/src/generated_v1_34`.
//!
//! Edit src/catalog.rs to add or remove kinds.

#![allow(clippy::module_name_repetitions)]

use std::borrow::Cow;
use serde::{Deserialize, Serialize};

use crate::kind::{GroupVersionKind, GroupVersionResource, KubeResource, Scope};
use crate::meta::ObjectMeta;
";

/// Emit the per-kind Rust source.
///
/// `description` is taken from the OpenAPI schema's `description`
/// field and inlined into the struct's rustdoc. Multiline doc
/// comments are prefixed with `///` per line (no smart wrapping —
/// upstream's doc strings are already wrapped at ~80).
#[must_use]
pub fn emit_kind(entry: &KindEntry, shape: &KindShape) -> String {
    let scope = if entry.cluster_scoped { "Scope::Cluster" } else { "Scope::Namespaced" };
    let api_version = if entry.group.is_empty() {
        entry.version.to_string()
    } else {
        format!("{}/{}", entry.group, entry.version)
    };
    // Use only the first paragraph of the OpenAPI description.
    // Subsequent paragraphs sometimes contain indented examples that
    // rustdoc interprets as Rust code blocks and tries to compile —
    // breaking `cargo test --doc`. Full description remains in the
    // vendored OpenAPI source.
    let doc = if shape.description.is_empty() {
        format!("/// `{}/{}` typed kind — generated by engenho-kube-codegen.", api_version, entry.kind)
    } else {
        let first_para = shape.description
            .split("\n\n")
            .next()
            .unwrap_or(&shape.description);
        first_para
            .lines()
            .map(|l| {
                // Trim leading whitespace so rustdoc never sees indent
                // (the 4-space indent rule for code blocks).
                let trimmed = l.trim_start();
                if trimmed.is_empty() { "///".to_string() } else { format!("/// {trimmed}") }
            })
            .collect::<Vec<_>>()
            .join("\n")
    };
    format!(
        "{HEADER}\n\
{doc}\n\
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]\n\
pub struct {kind} {{\n\
    /// Standard object metadata.\n\
    #[serde(default, skip_serializing_if = \"is_empty_meta\")]\n\
    pub metadata: ObjectMeta,\n\
    /// Spec (typed expansion is M0.0.4; today opaque JSON).\n\
    #[serde(default, skip_serializing_if = \"serde_json::Value::is_null\")]\n\
    pub spec: serde_json::Value,\n\
    /// Status (typed expansion is M0.0.4; today opaque JSON).\n\
    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
    pub status: Option<serde_json::Value>,\n\
}}\n\
\n\
impl KubeResource for {kind} {{\n\
    const GVK: GroupVersionKind = GroupVersionKind {{\n\
        group:   {group:?},\n\
        version: {version:?},\n\
        kind:    {kind:?},\n\
    }};\n\
    const GVR: GroupVersionResource = GroupVersionResource {{\n\
        group:    {group:?},\n\
        version:  {version:?},\n\
        resource: {resource:?},\n\
    }};\n\
    const SCOPE: Scope = {scope};\n\
\n\
    fn name(&self) -> Cow<'_, str> {{\n\
        Cow::Borrowed(self.metadata.name.as_str())\n\
    }}\n\
    fn namespace(&self) -> Option<Cow<'_, str>> {{\n\
        self.metadata.namespace.as_deref().map(Cow::Borrowed)\n\
    }}\n\
    fn resource_version(&self) -> Option<Cow<'_, str>> {{\n\
        if self.metadata.resource_version.is_empty() {{\n\
            None\n\
        }} else {{\n\
            Some(Cow::Borrowed(self.metadata.resource_version.as_str()))\n\
        }}\n\
    }}\n\
}}\n\
\n\
fn is_empty_meta(m: &ObjectMeta) -> bool {{ m == &ObjectMeta::default() }}\n\
",
        kind     = entry.kind,
        group    = entry.group,
        version  = entry.version,
        resource = entry.resource,
    )
}

/// Emit a module-level `mod.rs` listing every per-kind submodule in
/// `entries` (typically all entries with the same `module` field).
///
/// `module_name` ⇒ the module path (e.g. `core_v1`); used only in the
/// header comment.
#[must_use]
pub fn emit_module(module_name: &str, entries: &[&KindEntry]) -> String {
    let mut out = format!("//! GENERATED — `{module_name}` typed kinds. Source: engenho-kube-codegen.\n\n");
    for e in entries {
        out.push_str(&format!("mod {};\n", e.kind.to_lowercase()));
    }
    out.push('\n');
    for e in entries {
        out.push_str(&format!("pub use {}::{};\n", e.kind.to_lowercase(), e.kind));
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::openapi::{KindShape, XGvk};
    use std::collections::BTreeMap;

    fn entry() -> KindEntry {
        KindEntry {
            kind: "Pod", openapi_key: "io.k8s.api.core.v1.Pod",
            group: "", version: "v1", resource: "pods",
            cluster_scoped: false, module: "core_v1",
        }
    }

    fn shape() -> KindShape {
        KindShape {
            gvks: vec![XGvk {
                group: "".into(), version: "v1".into(), kind: "Pod".into(),
            }],
            description: "Pod is a collection of containers that can run on a host.".into(),
            properties: BTreeMap::new(),
            required: vec![],
        }
    }

    #[test]
    fn emit_kind_contains_struct_decl() {
        let s = emit_kind(&entry(), &shape());
        assert!(s.contains("pub struct Pod"));
        assert!(s.contains("impl KubeResource for Pod"));
    }

    #[test]
    fn emit_kind_namespaced_scope() {
        let s = emit_kind(&entry(), &shape());
        assert!(s.contains("Scope::Namespaced"));
    }

    #[test]
    fn emit_kind_cluster_scoped_when_flag_set() {
        let mut e = entry();
        e.cluster_scoped = true;
        e.kind = "Namespace";
        e.resource = "namespaces";
        let s = emit_kind(&e, &shape());
        assert!(s.contains("Scope::Cluster"));
        assert!(s.contains("pub struct Namespace"));
    }

    #[test]
    fn emit_kind_resource_in_gvr() {
        let s = emit_kind(&entry(), &shape());
        assert!(s.contains(r#"resource: "pods""#));
    }

    #[test]
    fn emit_kind_doc_from_openapi_description() {
        let s = emit_kind(&entry(), &shape());
        assert!(s.contains("Pod is a collection of containers"));
    }

    #[test]
    fn emit_kind_is_deterministic() {
        let s1 = emit_kind(&entry(), &shape());
        let s2 = emit_kind(&entry(), &shape());
        assert_eq!(s1, s2);
    }

    #[test]
    fn emit_module_lists_each_kind() {
        let entries: Vec<KindEntry> = vec![
            entry(),
            KindEntry { kind: "Service", openapi_key: "io.k8s.api.core.v1.Service",
                group: "", version: "v1", resource: "services",
                cluster_scoped: false, module: "core_v1" },
        ];
        let refs: Vec<&KindEntry> = entries.iter().collect();
        let m = emit_module("core_v1", &refs);
        assert!(m.contains("mod pod;"));
        assert!(m.contains("mod service;"));
        assert!(m.contains("pub use pod::Pod;"));
        assert!(m.contains("pub use service::Service;"));
    }
}
