//! `cph` — the command-line entrypoint for the checker.
//!
//! Owned by **WU-5**. Parses args, constructs the typst [`Engine`], runs the
//! `cph-check` pipeline against an engineering file, and prints diagnostics.
//!
//! Convention: **diagnostics go to stderr, results go to stdout.** `check`
//! exits 1 when there is any `Error`-severity diagnostic (warnings alone exit
//! 0); `build` exits 1 when the PDF could not be produced.

use std::path::PathBuf;
use std::process::ExitCode;

use clap::{Parser, Subcommand};
use cph_check::CheckReport;
use cph_typst::Engine;

/// The `cph` checker for curriculum engineering files.
#[derive(Debug, Parser)]
#[command(name = "cph", version, about, long_about = None)]
struct Cli {
    /// Override the `cph-render` package directory (the folder containing
    /// `lib.typ` / `typst.toml`). Defaults to the engine's own resolution
    /// (`CPH_RENDER_DIR` env var, else the repo `render/`).
    #[arg(long, global = true, value_name = "DIR")]
    render_dir: Option<PathBuf>,

    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Run the full check pipeline and print diagnostics. Exits 1 on any error.
    Check {
        /// Path to the engineering-file root (the folder with `manifest.toml`).
        path: PathBuf,
    },
    /// Build a PDF for a render target. Exits 1 if the build fails.
    Build {
        /// Path to the engineering-file root (the folder with `manifest.toml`).
        path: PathBuf,
        /// Render target to export.
        #[arg(long, default_value = "student")]
        target: String,
        /// Output PDF path. Defaults to `<PATH>/build/<target>.pdf`.
        #[arg(short = 'o', long, value_name = "OUT")]
        out: Option<PathBuf>,
    },
    /// Print a shell-completion script to stdout (clap_complete; ADR-0013 opt-in
    /// sibling: a local convenience, no lesson involved). Pipe to your shell's
    /// completion file, e.g. `cph completions zsh > ~/.zfunc/_cph`.
    Completions {
        /// Which shell to generate completions for.
        shell: CompletionTarget,
    },
}

#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum CompletionTarget {
    Bash,
    Zsh,
    Fish,
    PowerShell,
    Elvish,
}

fn main() -> ExitCode {
    let cli = Cli::parse();

    let engine = match &cli.render_dir {
        Some(dir) => Engine::with_render_dir(dir.clone()),
        None => Engine::new(),
    };

    match cli.command {
        Command::Check { path } => run_check(&path, &engine),
        Command::Build { path, target, out } => run_build(&path, &engine, &target, out),
        Command::Completions { shell } => run_completions(shell),
    }
}

/// Emit a shell-completion script for `shell` to stdout. The script is built
/// from the same `Cli` clap definition above, so it tracks subcommands/flags as
/// they evolve.
fn run_completions(shell: CompletionTarget) -> ExitCode {
    use clap::CommandFactory;
    use clap_complete::Shell as CompShell;

    let sh = match shell {
        CompletionTarget::Bash => CompShell::Bash,
        CompletionTarget::Zsh => CompShell::Zsh,
        CompletionTarget::Fish => CompShell::Fish,
        CompletionTarget::PowerShell => CompShell::PowerShell,
        CompletionTarget::Elvish => CompShell::Elvish,
    };
    let mut cmd = Cli::command();
    clap_complete::generate(sh, &mut cmd, "cph", &mut std::io::stdout());
    ExitCode::SUCCESS
}

/// Print every diagnostic in `report` to stderr, followed by a summary line.
fn print_diagnostics(report: &CheckReport) {
    for d in &report.diagnostics {
        eprintln!("{d}");
    }
    eprintln!(
        "{} error{}, {} warning{}",
        report.error_count(),
        plural(report.error_count()),
        report.warning_count(),
        plural(report.warning_count()),
    );
}

fn plural(n: usize) -> &'static str {
    if n == 1 {
        ""
    } else {
        "s"
    }
}

fn run_check(path: &std::path::Path, engine: &Engine) -> ExitCode {
    let report = cph_check::check(path, engine);
    print_diagnostics(&report);

    if report.has_errors() {
        ExitCode::FAILURE
    } else {
        println!("check passed: {}", path.display());
        ExitCode::SUCCESS
    }
}

fn run_build(
    path: &std::path::Path,
    engine: &Engine,
    target: &str,
    out: Option<PathBuf>,
) -> ExitCode {
    // A target whose steps are shell commands (a tool-generated asset bundle,
    // ADR-0009 category (b) — e.g. KenKen interactives via `kendoku`) is run by
    // executing those commands, not by compiling a typst template. Detect that
    // shape up front and route accordingly.
    if cph_check::target_is_shell(path, target) {
        return run_shell_build(path, engine, target);
    }

    // A target whose steps assemble markdown (ADR-0015: slides outline / 逐字稿
    // transcript surfaces) is built by concatenating per-element `<field>.md`
    // files in parts order, not by compiling a typst template.
    if cph_check::target_is_markdown_assemble(path, target) {
        return run_markdown_assemble_build(path, engine, target);
    }

    let out_path = out.unwrap_or_else(|| path.join("build").join(format!("{target}.pdf")));

    let (pdf, report) = cph_check::build(path, engine, target);
    print_diagnostics(&report);

    match pdf {
        Some(bytes) => {
            if let Some(parent) = out_path.parent() {
                if let Err(e) = std::fs::create_dir_all(parent) {
                    eprintln!(
                        "error: cannot create output directory '{}': {e}",
                        parent.display()
                    );
                    return ExitCode::FAILURE;
                }
            }
            if let Err(e) = std::fs::write(&out_path, &bytes) {
                eprintln!("error: cannot write '{}': {e}", out_path.display());
                return ExitCode::FAILURE;
            }
            println!("wrote {} ({} bytes)", out_path.display(), bytes.len());
            ExitCode::SUCCESS
        }
        None => {
            eprintln!("build failed: {} errors", report.error_count());
            ExitCode::FAILURE
        }
    }
}

/// Run a shell-step target: execute its declared commands in the engineering
/// root. Arbitrary command execution is **opt-in** — it only happens on an
/// explicit `cph build --target <name>`, and each command is printed before it
/// runs so the user sees exactly what is executed.
fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> ExitCode {
    eprintln!(
        "running shell-step target '{target}' (commands execute in {})",
        path.display()
    );
    let report = cph_check::run_shell_target(path, engine, target);
    print_diagnostics(&report.check);

    if report.outcomes.is_empty() && !report.ok {
        // Refused before running anything (check errors / unknown target / no
        // shell steps): the diagnostics above explain why.
        if report.check.has_errors() {
            eprintln!(
                "build refused: {} errors (fix the lesson first)",
                report.check.error_count()
            );
        } else {
            eprintln!("build failed: target '{target}' has no shell steps to run");
        }
        return ExitCode::FAILURE;
    }

    for outcome in &report.outcomes {
        eprintln!("$ {}", outcome.run);
        if !outcome.stdout.trim().is_empty() {
            print!("{}", outcome.stdout);
        }
        if !outcome.ok() {
            eprint!("{}", outcome.stderr);
            eprintln!(
                "step failed (exit {})",
                outcome
                    .status
                    .map(|c| c.to_string())
                    .unwrap_or_else(|| "signal".into())
            );
        }
    }

    if report.ok {
        println!(
            "shell-step target '{target}' completed: {} step(s) ok",
            report.outcomes.len()
        );
        ExitCode::SUCCESS
    } else {
        eprintln!("shell-step target '{target}' failed");
        ExitCode::FAILURE
    }
}

/// Run an assemble-markdown target: concatenate each element's `<field>.md`
/// (in parts order) and write the single-file artifact (ADR-0015). Like the
/// shell path, this is opt-in — only on an explicit `cph build --target <name>`,
/// and never in `check`.
fn run_markdown_assemble_build(path: &std::path::Path, engine: &Engine, target: &str) -> ExitCode {
    eprintln!(
        "assembling markdown target '{target}' (reading parts under {})",
        path.display()
    );
    let report = cph_check::run_markdown_assemble_target(path, engine, target);
    print_diagnostics(&report.check);

    if report.outcomes.is_empty() && !report.ok {
        if report.check.has_errors() {
            eprintln!(
                "build refused: {} errors (fix the lesson first)",
                report.check.error_count()
            );
        } else {
            eprintln!("build failed: target '{target}' has no assemble-markdown steps to run");
        }
        return ExitCode::FAILURE;
    }

    for outcome in &report.outcomes {
        eprintln!(
            "assembled field '{}' from {} part(s)",
            outcome.field, outcome.parts_read
        );
        if let Some(out_rel) = &outcome.written {
            println!(
                "wrote {} ({} bytes)",
                path.join(out_rel).display(),
                outcome.body.len()
            );
        }
        if let Some(err) = &outcome.error {
            eprintln!("assemble failed: {err}");
        }
    }

    if report.ok {
        println!(
            "assemble-markdown target '{target}' completed: {} step(s) ok",
            report.outcomes.len()
        );
        ExitCode::SUCCESS
    } else {
        eprintln!("assemble-markdown target '{target}' failed");
        ExitCode::FAILURE
    }
}
