use std::path::PathBuf;

use rpki::object_projection::{ObjectType, parse_object_json, resolve_object_type};

#[derive(Debug, PartialEq, Eq)]
struct Args {
    object_type: ObjectType,
    input_path: Option<PathBuf>,
    out_path: Option<PathBuf>,
    pretty: bool,
    entry_limit: usize,
}

impl Default for Args {
    fn default() -> Self {
        Self {
            object_type: ObjectType::Auto,
            input_path: None,
            out_path: None,
            pretty: true,
            entry_limit: 50,
        }
    }
}

fn usage() -> &'static str {
    "Usage: rpki_object_parse --type auto|cer|mft|crl|roa|asa|aspa --input <object-file> [--out <parsed.json>] [--entry-limit <n|all>] [--compact]"
}

fn main() {
    if let Err(err) = real_main() {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

fn real_main() -> Result<(), String> {
    let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
    let input_path = args.input_path.as_ref().expect("validated");
    let bytes = std::fs::read(input_path)
        .map_err(|e| format!("read input failed: {}: {e}", input_path.display()))?;
    let object_type = resolve_object_type(args.object_type, input_path)?;
    let parsed = parse_object_json(object_type, input_path, &bytes, args.entry_limit);
    let rendered = if args.pretty {
        serde_json::to_string_pretty(&parsed).map_err(|e| e.to_string())?
    } else {
        serde_json::to_string(&parsed).map_err(|e| e.to_string())?
    };

    if let Some(out_path) = args.out_path.as_ref() {
        if let Some(parent) = out_path.parent() {
            std::fs::create_dir_all(parent)
                .map_err(|e| format!("create output parent failed: {}: {e}", parent.display()))?;
        }
        std::fs::write(out_path, rendered)
            .map_err(|e| format!("write output failed: {}: {e}", out_path.display()))?;
    } else {
        println!("{rendered}");
    }
    Ok(())
}

fn parse_args(argv: &[String]) -> Result<Args, String> {
    let mut args = Args::default();
    let mut index = 1usize;
    while index < argv.len() {
        match argv[index].as_str() {
            "--type" => {
                index += 1;
                let value = argv.get(index).ok_or("--type requires a value")?;
                args.object_type =
                    ObjectType::parse(value).map_err(|err| format!("{err}\n{}", usage()))?;
            }
            "--input" | "--in" => {
                index += 1;
                args.input_path = Some(PathBuf::from(
                    argv.get(index).ok_or("--input requires a value")?,
                ));
            }
            "--out" => {
                index += 1;
                args.out_path = Some(PathBuf::from(
                    argv.get(index).ok_or("--out requires a value")?,
                ));
            }
            "--entry-limit" => {
                index += 1;
                let value = argv.get(index).ok_or("--entry-limit requires a value")?;
                args.entry_limit = parse_limit(value)?;
            }
            "--all" => {
                args.entry_limit = usize::MAX;
            }
            "--compact" => {
                args.pretty = false;
            }
            "--pretty" => {
                args.pretty = true;
            }
            "-h" | "--help" => return Err(usage().to_string()),
            other => return Err(format!("unknown argument: {other}\n{}", usage())),
        }
        index += 1;
    }
    if args.input_path.is_none() {
        return Err(format!("--input is required\n{}", usage()));
    }
    Ok(args)
}

fn parse_limit(value: &str) -> Result<usize, String> {
    if value.eq_ignore_ascii_case("all") {
        return Ok(usize::MAX);
    }
    value
        .parse::<usize>()
        .map_err(|_| format!("invalid --entry-limit: {value}"))
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::*;

    #[test]
    fn parse_args_accepts_plan_shape() {
        let args = parse_args(&[
            "rpki_object_parse".to_string(),
            "--type".to_string(),
            "auto".to_string(),
            "--input".to_string(),
            "a.roa".to_string(),
            "--out".to_string(),
            "parsed.json".to_string(),
            "--entry-limit".to_string(),
            "5".to_string(),
        ])
        .expect("parse args");
        assert_eq!(args.object_type, ObjectType::Auto);
        assert_eq!(args.input_path.as_deref(), Some(Path::new("a.roa")));
        assert_eq!(args.out_path.as_deref(), Some(Path::new("parsed.json")));
        assert_eq!(args.entry_limit, 5);
    }

    #[test]
    fn parse_args_accepts_aspa_alias_and_all_limit() {
        let args = parse_args(&[
            "rpki_object_parse".to_string(),
            "--type".to_string(),
            "asa".to_string(),
            "--in".to_string(),
            "a.asa".to_string(),
            "--entry-limit".to_string(),
            "all".to_string(),
            "--compact".to_string(),
        ])
        .expect("parse args");
        assert_eq!(args.object_type, ObjectType::Aspa);
        assert_eq!(args.entry_limit, usize::MAX);
        assert!(!args.pretty);
    }

    #[test]
    fn resolve_auto_type_from_extension() {
        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("a.mft")).expect("resolve"),
            ObjectType::Mft
        );
        assert_eq!(
            resolve_object_type(ObjectType::Auto, Path::new("a.asa")).expect("resolve"),
            ObjectType::Aspa
        );
    }

    #[test]
    fn unknown_auto_type_is_rejected() {
        let err = resolve_object_type(ObjectType::Auto, Path::new("a.bin")).unwrap_err();
        assert!(err.contains("cannot infer object type"), "{err}");
    }
}
