//! v0.20 #5: `plakat init` — bootstrap a starter project directory
//! with a runnable `scenario.hjson`, a `wildcards/` directory
//! (with a few example files), and a `.gitignore` that keeps the
//! `out/` dir from being committed.
//!
//! The goal is to give a brand-new user a one-liner that produces
//! something they can edit and run immediately, instead of asking
//! them to copy-paste pieces of the tutorials before they can
//! generate anything.

use anyhow::{Context, Result, bail};
use clap::Args;
use std::path::{Path, PathBuf};

#[derive(Args, Debug)]
pub struct InitArgs {
    /// Directory to bootstrap. Created if missing. Defaults to the
    /// current directory.
    #[arg(default_value = ".")]
    pub dir: PathBuf,

    /// Minimal mode — write only `scenario.hjson`. Skip the
    /// `wildcards/` directory and `.gitignore`. Useful when adding
    /// a scenario alongside an existing project that has its own
    /// `.gitignore`.
    #[arg(long)]
    pub minimal: bool,

    /// Overwrite files that already exist. Default behaviour is to
    /// error on conflict so nothing the user wrote gets clobbered
    /// silently.
    #[arg(long)]
    pub force: bool,
}

pub async fn run(args: InitArgs) -> Result<()> {
    let root = args.dir;
    std::fs::create_dir_all(&root)
        .with_context(|| format!("creating target directory {}", root.display()))?;

    let mut written = Vec::<PathBuf>::new();

    write_file(
        &root.join("scenario.hjson"),
        SCENARIO_TEMPLATE,
        args.force,
        &mut written,
    )?;

    if !args.minimal {
        let wildcards_dir = root.join("wildcards");
        std::fs::create_dir_all(&wildcards_dir).with_context(|| {
            format!("creating wildcards directory {}", wildcards_dir.display())
        })?;
        for (name, contents) in WILDCARD_TEMPLATES {
            write_file(
                &wildcards_dir.join(name),
                contents,
                args.force,
                &mut written,
            )?;
        }
        write_file(
            &root.join(".gitignore"),
            GITIGNORE_TEMPLATE,
            args.force,
            &mut written,
        )?;
    }

    println!("Initialised plakat project at {}", root.display());
    for path in &written {
        // Display paths relative to the project root when possible.
        let display = path.strip_prefix(&root).unwrap_or(path);
        println!("  + {}", display.display());
    }
    println!();
    println!("Next:");
    println!("  plakat scenario {}", root.join("scenario.hjson").display());
    println!("  # or, before generating, validate the file:");
    println!(
        "  plakat scenario {} --dry-run",
        root.join("scenario.hjson").display()
    );
    Ok(())
}

fn write_file(
    path: &Path,
    contents: &str,
    force: bool,
    written: &mut Vec<PathBuf>,
) -> Result<()> {
    if path.exists() && !force {
        bail!(
            "{} already exists — pass --force to overwrite",
            path.display()
        );
    }
    std::fs::write(path, contents)
        .with_context(|| format!("writing {}", path.display()))?;
    written.push(path.to_path_buf());
    Ok(())
}

/// A self-contained starter scenario. Trimmed from
/// `examples/scenario.hjson`: same catalog shape (scene + weather
/// reused across tasks) but only two tasks, no persona scaffolding,
/// no upscale block, no commented-out alternate paths. Targets
/// SD 1.5 because it's the only non-gated, no-token-required
/// default the user can run end-to-end with zero setup.
pub(crate) const SCENARIO_TEMPLATE: &str = r#"{
    # ---------- plakat starter scenario ----------
    # Generated by `plakat init`. Edit freely.
    #
    # Run:
    #   plakat scenario scenario.hjson
    #
    # Validate without generating:
    #   plakat scenario scenario.hjson --dry-run
    # --------------------------------------------

    # SD 1.5 — open weights, no HF token required.
    model: sd15

    # Auto-pick the best device on this machine.
    device: auto

    # SD 1.5 was trained at 512².
    size: 512x512

    # Images per task.
    count: 1

    steps: 28
    guidance: 7.0

    # Base seed — per-image seeds are seed, seed+count, seed+2*count, …
    seed: 1000

    # Where rendered PNGs go (created on first run).
    out: ./out

    # Prompt enhancer: `local` runs a small Qwen2.5-1.5B model on
    # device (no API key, ~1 GB one-time download). Swap to
    # `deepseek` or `gemini` if you have an API key set.
    enhancer: local

    scheduler: euler-a

    # Reusable scene + weather descriptions. Each task picks one
    # from each catalog by `name`; the strings get woven into the
    # final prompt at render time.
    scene:
    [
        {
            name: forest
            prompt: "a forest of tall pines, dappled light"
        }
        {
            name: harbor
            prompt: "a harbor at the edge of a stone-walled town"
        }
    ]

    weather:
    [
        {
            name: golden_hour
            prompt: "golden hour, warm low sun"
        }
        {
            name: rain
            prompt: "light rain on cobblestones, soft reflections"
        }
    ]

    tasks:
    [
        {
            name: forest_golden
            scene: forest
            weather: golden_hour
            prompt: "a lone traveler with a wooden staff, painterly"
        }
        {
            name: harbor_rain
            scene: harbor
            weather: rain
            prompt: "merchants under awnings, children splashing in puddles"
        }
    ]
}
"#;

/// `(filename, contents)` for the starter wildcard files written
/// under `wildcards/`. These are for `plakat generate
/// --wildcard-dir ./wildcards` (the scenario engine has its own
/// scene/weather catalog mechanism — see SCENARIO_TEMPLATE).
/// Three options per file is enough for users to *see* expansion
/// working without burying the example under placeholder text.
pub(crate) const WILDCARD_TEMPLATES: &[(&str, &str)] = &[
    (
        "subject.txt",
        "# One option per line. Blank lines and `#` comments are skipped.\n\
         # Use via:  plakat generate \"__subject__ in a forest\" --wildcard-dir ./wildcards\n\
         a lone traveler with a wooden staff\n\
         a fox watching from the underbrush\n\
         a child carrying a paper lantern\n",
    ),
    (
        "style.txt",
        "# One option per line. Blank lines and `#` comments are skipped.\n\
         oil painting, visible brushstrokes\n\
         watercolour, soft washes\n\
         pencil sketch, hatching\n",
    ),
    (
        "lighting.txt",
        "# One option per line. Blank lines and `#` comments are skipped.\n\
         golden hour, warm low sun\n\
         overcast, soft diffuse light\n\
         lantern light, deep shadows\n",
    ),
];

/// A focused `.gitignore` — keeps the rendered images out of git
/// without making assumptions about the wider repo. Users who run
/// `plakat init` inside an existing repo just get one more rule;
/// users in a fresh directory get the obvious defaults.
pub(crate) const GITIGNORE_TEMPLATE: &str = "# plakat: generated images live here\n\
out/\n\
\n\
# plakat: enhancer + cache scratch\n\
.plakat-cache/\n";

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

    #[tokio::test]
    async fn init_writes_all_files_by_default() {
        let tmp = tempdir().unwrap();
        let root = tmp.path().join("project");
        run(InitArgs {
            dir: root.clone(),
            minimal: false,
            force: false,
        })
        .await
        .unwrap();
        assert!(root.join("scenario.hjson").exists());
        assert!(root.join(".gitignore").exists());
        assert!(root.join("wildcards/subject.txt").exists());
        assert!(root.join("wildcards/style.txt").exists());
        assert!(root.join("wildcards/lighting.txt").exists());
    }

    #[tokio::test]
    async fn init_minimal_skips_wildcards_and_gitignore() {
        let tmp = tempdir().unwrap();
        let root = tmp.path().join("project");
        run(InitArgs {
            dir: root.clone(),
            minimal: true,
            force: false,
        })
        .await
        .unwrap();
        assert!(root.join("scenario.hjson").exists());
        assert!(!root.join(".gitignore").exists());
        assert!(!root.join("wildcards").exists());
    }

    #[tokio::test]
    async fn init_errors_on_existing_scenario_without_force() {
        let tmp = tempdir().unwrap();
        let root = tmp.path().to_path_buf();
        std::fs::write(root.join("scenario.hjson"), "{}").unwrap();
        let err = run(InitArgs {
            dir: root,
            minimal: true,
            force: false,
        })
        .await
        .unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("already exists"), "got {msg}");
        assert!(msg.contains("--force"), "got {msg}");
    }

    #[tokio::test]
    async fn init_force_overwrites_existing_files() {
        let tmp = tempdir().unwrap();
        let root = tmp.path().to_path_buf();
        std::fs::write(root.join("scenario.hjson"), "stale").unwrap();
        run(InitArgs {
            dir: root.clone(),
            minimal: true,
            force: true,
        })
        .await
        .unwrap();
        let body = std::fs::read_to_string(root.join("scenario.hjson")).unwrap();
        assert!(body.contains("plakat starter scenario"), "got: {body}");
    }

    #[test]
    fn wildcard_templates_have_at_least_one_non_comment_line() {
        // The whole point of the file is to provide *options* to
        // expand into. A file with only comments would expand to
        // the literal `__name__` token at runtime (graceful but
        // confusing on first try). Guard against that.
        for (name, body) in WILDCARD_TEMPLATES {
            let real_lines: Vec<&str> = body
                .lines()
                .map(str::trim)
                .filter(|l| !l.is_empty() && !l.starts_with('#'))
                .collect();
            assert!(
                !real_lines.is_empty(),
                "wildcard file `{name}` has no non-comment lines"
            );
        }
    }

    #[test]
    fn scenario_template_uses_only_aliases_init_can_resolve() {
        // `model: sd15` lets the user run the scenario without
        // an HF token. If a future edit swaps to a gated model
        // (sd3, flux-dev, …) the first-run UX breaks. Pin the
        // ungated default here.
        assert!(
            SCENARIO_TEMPLATE.contains("model: sd15"),
            "starter scenario should target an ungated model (sd15) \
             so first-run users without HF_TOKEN can generate"
        );
    }
}
