//! Emit a `scenario` HJSON from resolved + compiled scenes. `deser-hjson` is
//! deserialize-only, so this is a small hand-written formatter: unquoted keys +
//! JSON-escaped string values + `//` comments (all valid HJSON). Output is
//! **deterministic** (no timestamp) so corpus proofs are byte-stable.

use super::resolver::{ResolvedGlobals, ResolvedScene};

/// A scene with its final compiled prompt + negative (post-LLM or verbatim).
pub struct CompiledScene {
    pub scene: ResolvedScene,
    pub prompt: String,
    pub negative: String,
}

fn q(s: &str) -> String {
    // serde_json gives correct, deterministic escaping for any content.
    serde_json::to_string(s).unwrap_or_else(|_| format!("{s:?}"))
}

/// Render the scenario HJSON. `input_name` and `provider` go into the header
/// comment; both are inputs, so the output stays deterministic.
pub fn emit(globals: &ResolvedGlobals, scenes: &[CompiledScene], input_name: &str, provider: &str) -> String {
    let mut o = String::new();
    o.push_str(&format!("// Generated by: plakat compile {input_name}\n"));
    o.push_str(&format!(
        "// Provider: {provider}  ·  Global model family: {}\n",
        globals.family.label()
    ));
    o.push_str("// Edit freely, then: plakat scenario <this file>\n");
    o.push_str("{\n");

    // ---- global generation parameters ----
    if let Some(m) = &globals.model {
        o.push_str(&format!("  model: {m}\n"));
    }
    if let Some(s) = &globals.size {
        o.push_str(&format!("  size: {s}\n"));
    }
    if let Some(n) = globals.steps {
        o.push_str(&format!("  steps: {n}\n"));
    }
    if let Some(g) = globals.guidance {
        o.push_str(&format!("  guidance: {g}\n"));
    }
    if let Some(s) = globals.seed {
        o.push_str(&format!("  seed: {s}\n"));
    }
    if let Some(c) = globals.count {
        o.push_str(&format!("  count: {c}\n"));
    }
    if let Some(s) = &globals.scheduler {
        o.push_str(&format!("  scheduler: {s}\n"));
    }
    if let Some(r) = globals.refine {
        o.push_str(&format!("  refine: {r}\n"));
    }
    if !globals.loras.is_empty() {
        let items: Vec<String> = globals.loras.iter().map(|l| q(l)).collect();
        o.push_str(&format!("  loras: [{}]\n", items.join(", ")));
    }
    if !globals.negative_seeds.trim().is_empty() {
        o.push_str(&format!("  negative: {}\n", q(&globals.negative_seeds)));
    }
    // Prompts are pre-enhanced by compile, so every task opts out of the
    // scenario-time enhancer. `enhancer: auto` satisfies the schema (a missing
    // key/no-LLM still validates); the per-task `enhance: false` does the work.
    o.push_str("  enhancer: auto\n\n");

    // ---- minimal scene/weather axes (scenarios cross scene×weather→tasks) ----
    o.push_str("  scene: [ { name: \"plain\", prompt: \"\" } ]\n");
    o.push_str("  weather: [ { name: \"any\", prompt: \"\" } ]\n\n");

    // ---- tasks (one per compiled scene; skipped scenes are omitted) ----
    o.push_str("  tasks:\n  [\n");
    for cs in scenes {
        let s = &cs.scene;
        o.push_str("    {\n");
        o.push_str(&format!("      name: {}\n", sanitize_name(&s.name)));
        o.push_str("      scene: plain\n");
        o.push_str("      weather: any\n");
        o.push_str("      enhance: false\n");
        if let Some(m) = &s.model_for_family {
            if globals.model.as_deref() != Some(m.as_str()) {
                o.push_str(&format!(
                    "      // note: prompt written for {} ({}); scenario runs the global model\n",
                    m,
                    s.family.label()
                ));
            }
        }
        o.push_str(&format!("      prompt: {}\n", q(&cs.prompt)));
        if !cs.negative.trim().is_empty() {
            o.push_str(&format!("      negative: {}\n", q(&cs.negative)));
        }
        if let Some(seed) = s.seed {
            o.push_str(&format!("      seed: {seed}\n"));
        }
        if let Some(c) = s.count {
            o.push_str(&format!("      count: {c}\n"));
        }
        if let Some(sz) = &s.size {
            o.push_str(&format!("      size: {sz}\n"));
        }
        if let Some(n) = s.steps {
            o.push_str(&format!("      steps: {n}\n"));
        }
        if let Some(g) = s.guidance {
            o.push_str(&format!("      guidance: {g}\n"));
        }
        if let Some(sc) = &s.scheduler {
            o.push_str(&format!("      scheduler: {sc}\n"));
        }
        if let Some(r) = s.refine {
            o.push_str(&format!("      refine: {r}\n"));
        }
        // MAP-4: emit the map directives so a `type: map` block becomes a map task.
        if let Some(t) = &s.task_type {
            o.push_str(&format!("      type: {}\n", q(t)));
        }
        if let Some(v) = &s.map_spec {
            o.push_str(&format!("      map-spec: {}\n", q(v)));
        }
        if let Some(v) = &s.map_style {
            o.push_str(&format!("      map-style: {}\n", q(v)));
        }
        if let Some(v) = &s.map_paint {
            o.push_str(&format!("      map-paint: {v}\n")); // true|false (bool in HJSON)
        }
        if let Some(v) = &s.map_scale {
            o.push_str(&format!("      map-scale: {}\n", q(v)));
        }
        if let Some(v) = &s.map_tiles {
            o.push_str(&format!("      map-tiles: {}\n", q(v)));
        }
        if let Some(v) = &s.map_sd_model {
            o.push_str(&format!("      map-sd-model: {}\n", q(v)));
        }
        if !s.map_sd_lora.is_empty() {
            let items: Vec<String> = s.map_sd_lora.iter().map(|l| q(l)).collect();
            o.push_str(&format!("      map-sd-lora: [{}]\n", items.join(", ")));
        }
        if let Some(v) = &s.map_provider {
            o.push_str(&format!("      map-provider: {}\n", q(v)));
        }
        o.push_str("    }\n");
    }
    o.push_str("  ]\n");
    o.push_str("}\n");
    o
}

/// Task names must be a single token (scenario uses them as ids). Replace any
/// run of non-alphanumeric chars with one underscore.
fn sanitize_name(name: &str) -> String {
    let mut out = String::with_capacity(name.len());
    let mut last_us = false;
    for c in name.chars() {
        if c.is_ascii_alphanumeric() {
            out.push(c.to_ascii_lowercase());
            last_us = false;
        } else if !last_us {
            out.push('_');
            last_us = true;
        }
    }
    let trimmed = out.trim_matches('_').to_string();
    if trimmed.is_empty() { "scene".into() } else { trimmed }
}

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

    fn compiled() -> (ResolvedGlobals, Vec<CompiledScene>) {
        let globals = ResolvedGlobals {
            model: Some("sdxl".into()),
            family: ModelFamily::Sdxl,
            steps: Some(30),
            negative_seeds: "blurry, watermark".into(),
            ..Default::default()
        };
        let scene = ResolvedScene {
            name: "Tundra Rider".into(),
            seed: Some(42),
            family: ModelFamily::Sdxl,
            ..Default::default()
        };
        let cs = CompiledScene {
            scene,
            prompt: "a frozen tundra, lone rider, aurora".into(),
            negative: "blurry, watermark, daylight".into(),
        };
        (globals, vec![cs])
    }

    #[test]
    fn emits_parseable_deterministic_hjson() {
        let (g, scenes) = compiled();
        let a = emit(&g, &scenes, "prompts.txt", "deepseek");
        let b = emit(&g, &scenes, "prompts.txt", "deepseek");
        assert_eq!(a, b, "deterministic (no timestamp)");
        assert!(a.contains("name: tundra_rider"), "name sanitized");
        assert!(a.contains("enhance: false"));
        assert!(a.contains("seed: 42"));
        assert!(a.contains("model: sdxl"));
        // It must round-trip through the same HJSON parser scenario uses.
        let _: serde_json::Value = deser_hjson::from_str(&a)
            .unwrap_or_else(|e| panic!("emitted HJSON did not parse: {e}\n---\n{a}"));
    }

    #[test]
    fn sanitizes_names() {
        assert_eq!(sanitize_name("Lady Mireth!"), "lady_mireth");
        assert_eq!(sanitize_name("ch01_establishing"), "ch01_establishing");
        assert_eq!(sanitize_name("***"), "scene");
    }
}
