use anyhow::{Context, Result, bail};
use camino::Utf8PathBuf;
use clap::{Args, Parser, Subcommand};
use gha_runner_image_proof::{
    CheckOptions, OutputFormat, ProbeDockerOptions, ProbeHostOptions, SetupNodeCmdOptions,
    render_receipt, run_check, run_probe_docker, run_probe_host, run_setup_node,
};
use std::fs;
use std::process::ExitCode;

#[derive(Debug, Parser)]
#[command(
    version,
    about = "Validate runner-image manifests, probe hosts and Docker images, classify setup-node \
             offline — all with receipts."
)]
struct Cli {
    #[command(subcommand)]
    command: Command,

    #[arg(long, global = true, value_enum, default_value = "text")]
    format: OutputFormat,

    #[arg(long, global = true, value_name = "PATH")]
    output: Option<Utf8PathBuf>,

    #[arg(long, global = true)]
    strict: bool,
}

#[derive(Debug, Subcommand)]
enum Command {
    Check(CheckArgs),
    ProbeHost(ProbeHostArgs),
    ProbeDocker(ProbeDockerArgs),
    SetupNode(SetupNodeArgs),
}

#[derive(Debug, Args)]
struct CheckArgs {
    #[arg(long, value_name = "PATH")]
    manifest: Utf8PathBuf,
}

#[derive(Debug, Args)]
struct ProbeHostArgs {
    #[arg(long, value_name = "PATH")]
    manifest: Utf8PathBuf,

    #[arg(long, value_name = "NAME")]
    tool: Vec<String>,

    #[arg(long, value_name = "NAME")]
    package: Vec<String>,

    #[arg(long, value_name = "NAME")]
    capability: Vec<String>,
}

#[derive(Debug, Args)]
struct ProbeDockerArgs {
    #[arg(long, value_name = "PATH")]
    manifest: Utf8PathBuf,

    #[arg(long, value_name = "IMAGE")]
    image: String,

    #[arg(long, value_name = "NAME")]
    tool: Vec<String>,

    #[arg(long, value_name = "NAME")]
    package: Vec<String>,

    #[arg(long, value_name = "NAME")]
    capability: Vec<String>,
}

#[derive(Debug, Args)]
struct SetupNodeArgs {
    #[arg(long, value_name = "PATH")]
    manifest: Utf8PathBuf,

    #[arg(long, value_name = "SPEC")]
    version: String,

    #[arg(long, value_name = "ARCH", default_value = "x64")]
    arch: String,

    #[arg(long = "check-latest")]
    check_latest: bool,
}

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("error: {e:#}");
            ExitCode::FAILURE
        }
    }
}

fn run() -> Result<()> {
    let cli = Cli::parse();

    let receipt = match &cli.command {
        Command::Check(args) => {
            ensure_file(&args.manifest)?;
            run_check(&CheckOptions {
                manifest_path: args.manifest.clone(),
            })?
        }
        Command::ProbeHost(args) => {
            ensure_file(&args.manifest)?;
            run_probe_host(&ProbeHostOptions {
                manifest_path: args.manifest.clone(),
                tools: args.tool.clone(),
                packages: args.package.clone(),
                capabilities: args.capability.clone(),
            })?
        }
        Command::ProbeDocker(args) => {
            ensure_file(&args.manifest)?;
            if args.image.trim().is_empty() {
                bail!("--image must not be empty");
            }
            run_probe_docker(&ProbeDockerOptions {
                manifest_path: args.manifest.clone(),
                image: args.image.clone(),
                tools: args.tool.clone(),
                packages: args.package.clone(),
                capabilities: args.capability.clone(),
            })?
        }
        Command::SetupNode(args) => {
            ensure_file(&args.manifest)?;
            if args.version.trim().is_empty() {
                bail!("--version must not be empty");
            }
            run_setup_node(&SetupNodeCmdOptions {
                manifest_path: args.manifest.clone(),
                version_spec: args.version.clone(),
                arch: args.arch.clone(),
                check_latest: args.check_latest,
            })?
        }
    };

    let rendered = render_receipt(&receipt, cli.format)?;
    if let Some(output) = cli.output {
        if let Some(parent) = output.parent() {
            fs::create_dir_all(parent).with_context(|| format!("creating {parent}"))?;
        }
        fs::write(&output, rendered).with_context(|| format!("writing {output}"))?;
    } else {
        print!("{rendered}");
    }

    if receipt.summary.failed > 0 || (cli.strict && receipt.summary.warnings > 0) {
        bail!("runner image proof failed");
    }
    Ok(())
}

fn ensure_file(path: &Utf8PathBuf) -> Result<()> {
    if !path.is_file() {
        bail!("--manifest must point at an existing file: {path}");
    }
    Ok(())
}
