//! `wai app`: apps made with Create (a build with the feature `app`, `wai-app` linked in).
//!
//! - `wai app guide` (`app.guide`): the line language and the WAI widgets, as a model writing
//!   an app is told them.
//! - `wai app check [<dir> | <app.oui>] [--json]` (`app.check`): a program checked against the
//!   WAI registry and WAI's rules, every error with its line and reason code. In a project
//!   (a `wai.json` beside the program, as `wai create --from app.oui` writes one), also the
//!   room the project file names for `wai dev` against the app's own, and the runtime under
//!   `vendor/` file by file against `vendor.json`. Exit 0 when everything holds, 1 when
//!   something does not, each failure named.

pub const USAGE: &str = "wai app guide | wai app check [<dir> | <app.oui>] [--json]\n            an app made with Create: what a model is told, and the app, its room and its runtime checked";

#[cfg(not(feature = "app"))]
pub fn main(_: &[String]) -> Result<bool, String> {
    Err("`wai app` needs a build with the feature `app` (cargo install --path wai/wai-rs --no-default-features --features app)".into())
}

#[cfg(feature = "app")]
pub fn main(args: &[String]) -> Result<bool, String> {
    use super::positional;
    use std::path::Path;
    match args.first().map(String::as_str) {
        Some("guide") => {
            print!("{}", wai_app::guide::guide());
            Ok(true)
        }
        Some("check") => {
            if let Some(a) = args[1..].iter().find(|a| a.starts_with("--") && *a != "--json") {
                return Err(format!("wai app check: {a}: not an option ({USAGE})"));
            }
            let pos = positional(&args[1..], &[]);
            if pos.len() > 1 {
                return Err(USAGE.into());
            }
            let at = Path::new(pos.first().copied().unwrap_or("."));
            let report = check(at)?;
            let ok = report.failures.is_empty();
            if super::envelope::json_wanted(args) {
                let body = serde_json::json!({ "program": report.program, "project": report.project, "room": report.room, "errors": report.errors, "failures": report.failures, "checked": report.checked });
                let serde_json::Value::Object(body) = body else { unreachable!("an object") };
                println!("{}", super::envelope::envelope("app.check", ok, body));
            } else {
                println!("wai app check: {}", report.program);
                for c in &report.checked {
                    println!("  ok    {c}");
                }
                for f in &report.failures {
                    println!("  FAIL  {f}");
                }
            }
            Ok(ok)
        }
        _ => Err(USAGE.into()),
    }
}

/// What `wai app check` found.
#[cfg(feature = "app")]
pub(crate) struct Report {
    pub program: String,
    pub project: Option<String>,
    pub room: Option<wai_app::RoomPlan>,
    pub errors: Vec<wai_app::ErrorOut>,
    /// What held, one line each.
    pub checked: Vec<String>,
    /// What did not, one line each.
    pub failures: Vec<String>,
}

/// Check the app at `at`: a project directory, or a program file (its directory the project's
/// when a `wai.json` is beside it).
#[cfg(feature = "app")]
pub(crate) fn check(at: &std::path::Path) -> Result<Report, String> {
    use super::project;
    use wai_app::export::{dev_mode, APP};
    let (dir, program_path) = if at.is_dir() {
        let p = project::load(at)?;
        let app = p.as_ref().and_then(|p| p.app.clone()).unwrap_or_else(|| at.join(APP));
        (at.to_path_buf(), app)
    } else {
        (at.parent().map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d }).unwrap_or(std::path::Path::new(".")).to_path_buf(), at.to_path_buf())
    };
    let text = std::fs::read_to_string(&program_path).map_err(|e| format!("{}: {e}", program_path.display()))?;
    let c = wai_app::check(&text);
    let mut r = Report { program: program_path.display().to_string(), project: None, room: c.room.clone(), errors: c.errors.clone(), checked: Vec::new(), failures: Vec::new() };
    let bad: Vec<&wai_app::ErrorOut> = c.errors.iter().filter(|e| !e.warning).collect();
    for e in &bad {
        r.failures.push(format!("{} line {}: {}: {}", APP, e.line, e.code, e.message));
    }
    if bad.is_empty() {
        r.checked.push(format!("{APP}: every line accepted by the WAI registry, {} widget(s)", c.nodes.len()));
    }
    let Some(room) = &c.room else {
        r.failures.push(format!("{APP}: the app holds no room (a Room, a WatchTogether or a Show)"));
        return Ok(r);
    };
    let Some(p) = project::load(&dir)? else {
        return Ok(r);
    };
    r.project = Some(dir.join(project::FILE).display().to_string());
    // The room `wai dev` runs for the project is the app's.
    let want = dev_mode(&room.kind);
    match p.dev.mode.as_deref() {
        None => r.failures.push(format!("{}: names no room for the app (dev.mode \"{want}\")", project::FILE)),
        Some(m) if m != want => r.failures.push(format!("{}: dev.mode is \"{m}\", and the app's room is \"{want}\"", project::FILE)),
        Some(_) => r.checked.push(format!("{}: dev.mode \"{want}\", the app's room", project::FILE)),
    }
    if room.kind == "show" {
        match p.dev.audience {
            Some(a) if a == room.audience => r.checked.push(format!("{}: dev.audience {a}, the show's", project::FILE)),
            got => r.failures.push(format!("{}: dev.audience is {}, and the show's audience is {}", project::FILE, got.map_or("absent".into(), |a| a.to_string()), room.audience)),
        }
    }
    if room.kind == "watch" {
        let source = room.source.as_deref().unwrap_or_default();
        let want_path = dir.join("media").join(source);
        match &p.dev.watch {
            Some(w) if *w == want_path => {
                if w.is_file() {
                    r.checked.push(format!("{}: dev.watch media/{source}, present", project::FILE));
                } else {
                    r.failures.push(format!("media/{source} is not there: put the file the room watches at it"));
                }
            }
            got => r.failures.push(format!("{}: dev.watch is {}, and the app watches media/{source}", project::FILE, got.as_ref().map_or("absent".into(), |w| w.display().to_string()))),
        }
    }
    // The runtime under vendor/, file by file.
    let vendor = dir.join("vendor.json");
    match std::fs::read_to_string(&vendor).map_err(|e| e.to_string()).and_then(|t| serde_json::from_str::<serde_json::Value>(&t).map_err(|e| e.to_string())) {
        Err(e) => r.failures.push(format!("vendor.json: {e}")),
        Ok(v) => {
            let files = v["files"].as_array().cloned().unwrap_or_default();
            let mut good = 0;
            for f in &files {
                let (Some(path), Some(sum)) = (f["path"].as_str(), f["sha256"].as_str()) else {
                    r.failures.push("vendor.json: a file without its path or its SHA-256".into());
                    continue;
                };
                if path.split('/').any(|s| s.is_empty() || s == "." || s == "..") {
                    r.failures.push(format!("vendor.json: the path {path} leaves vendor/"));
                    continue;
                }
                match std::fs::read(dir.join("vendor").join(path)) {
                    Err(_) => r.failures.push(format!("vendor/{path} is missing (vendor.json names it)")),
                    Ok(b) => {
                        let got = wai_app::runtime::sha256_hex(&b);
                        if got == sum {
                            good += 1;
                        } else {
                            r.failures.push(format!("vendor/{path}: its SHA-256 is {got}, vendor.json says {sum}"));
                        }
                    }
                }
            }
            if files.is_empty() {
                r.failures.push("vendor.json lists no runtime file".into());
            } else if good == files.len() {
                r.checked.push(format!("vendor/: {good} runtime file(s), each the one vendor.json names"));
            }
        }
    }
    Ok(r)
}
