//! Augmented-manifest construction (ADR-0011).
//!
//! The template (`exports/<target>.typ`) reads the manifest via
//! `toml(sys.inputs.manifest)`, then for each part `include`s its content fields
//! by a **computed** path and reads scalar fields from `<path>/element.toml`.
//! For *optional* content fields the template must know whether the file exists
//! on disk — typst has no file-exists primitive and a missing `include` is a
//! hard error (see the OPEN contract point in `render/templates/student.typ`).
//!
//! The ENGINE has filesystem access, so it closes that gap: it builds an
//! **augmented manifest** = the lesson's `[info]` + ordered `[[parts]]`, with a
//! per-part **`fields` array** listing the content fields whose `<field>.typ`
//! actually exists under the lesson root. The augmented manifest is served as an
//! in-memory virtual file in the [`crate::world::LessonWorld`] (it is **never**
//! written to the user's tree), and injected via `sys.inputs.manifest`.
//!
//! ## `fields` is computed from `cph-schema`
//!
//! `cph-schema` already encodes each kind's content fields
//! ([`cph_schema::KindSchema::content_field_names`]) — the same knowledge the
//! render package exposes as `part-fields`. We reuse it here rather than
//! re-deriving a kind→fields map, so the engine and the template agree on what a
//! kind's content fields are. For each part, a content field is listed in
//! `fields` iff `<root>/<part.path>/<field>.typ` is a real file.

use cph_model::Lesson;

/// Build the augmented-manifest TOML source for `lesson`.
///
/// The result is a self-contained TOML document the template's
/// `toml(sys.inputs.manifest)` reads. It carries `[info]` (title + optional
/// author) and the ordered `[[parts]]`, each with `kind`, `path`, and a
/// `fields = [...]` array of the content fields present on disk (per
/// [`present_fields`]). It does **not** reproduce `[project]` or `[targets.*]`
/// — the template only consumes `info` and `parts`.
pub fn build_augmented_manifest(lesson: &Lesson) -> String {
    let mut doc = toml::Table::new();

    // [info]
    let mut info = toml::Table::new();
    info.insert(
        "title".to_string(),
        toml::Value::String(lesson.info.title.clone()),
    );
    if !lesson.info.authors.is_empty() {
        let authors = lesson
            .info
            .authors
            .iter()
            .cloned()
            .map(toml::Value::String)
            .collect();
        info.insert("author".to_string(), toml::Value::Array(authors));
    }
    doc.insert("info".to_string(), toml::Value::Table(info));

    // [[parts]] — preserve declared order; attach the on-disk `fields` array.
    let parts: Vec<toml::Value> = lesson
        .parts
        .iter()
        .map(|part| {
            let mut entry = toml::Table::new();
            entry.insert("kind".to_string(), toml::Value::String(part.kind.clone()));
            entry.insert(
                "path".to_string(),
                toml::Value::String(path_to_forward_slash(&part.path)),
            );
            let fields = present_fields(lesson, part)
                .into_iter()
                .map(toml::Value::String)
                .collect();
            entry.insert("fields".to_string(), toml::Value::Array(fields));
            toml::Value::Table(entry)
        })
        .collect();
    doc.insert("parts".to_string(), toml::Value::Array(parts));

    toml::to_string(&doc).expect("augmented manifest serializes")
}

/// The content fields of `part`'s kind whose `<root>/<part.path>/<field>.typ`
/// file exists on disk, in schema order.
///
/// The kind→content-fields knowledge is reused from `cph-schema`
/// ([`cph_schema::schema_for`]); an unknown kind has no schema and yields an
/// empty list (the render package surfaces an unknown kind on its own). Both
/// required and optional content fields are probed — listing a *required* field
/// here is harmless (the template includes required fields unconditionally), and
/// it keeps `fields` a faithful "what exists on disk" record.
fn present_fields(lesson: &Lesson, part: &cph_model::Part) -> Vec<String> {
    let Some(schema) = cph_schema::schema_for(&part.kind) else {
        return Vec::new();
    };
    let part_dir = lesson.root.join(&part.path);
    schema
        .content_field_names()
        .into_iter()
        .filter(|field| part_dir.join(format!("{field}.typ")).is_file())
        .map(str::to_string)
        .collect()
}

/// Render a relative `PathBuf` as a forward-slash string, dropping any leading
/// `./` or `/` and ignoring `..` (already rejected by the cph-model loader).
/// Element folder names are UTF-8 (e.g. Chinese) and kept verbatim. The template
/// rebuilds an absolute root-relative include path as `"/" + path + "/" + field`,
/// so `path` must be a clean relative slash path.
fn path_to_forward_slash(path: &std::path::Path) -> String {
    use std::path::Component;
    let mut out = String::new();
    for comp in path.components() {
        if let Component::Normal(s) = comp {
            if !out.is_empty() {
                out.push('/');
            }
            out.push_str(&s.to_string_lossy());
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn forward_slash_path_keeps_unicode() {
        let p = std::path::Path::new("segments/开场对照导言");
        assert_eq!(path_to_forward_slash(p), "segments/开场对照导言");
    }
}
