use std::env;
use std::fs::{self, File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};

use mkfs_xfs::image::{ManifestDiff, MetadataExtentSummary, MetadataImage};
use mkfs_xfs::{FormatOptions, FormatPlan, InodeGenerationPolicy};

const DEFAULT_SIZE: u64 = 1 << 30;
const DEFAULT_UUID: [u8; 16] = [
    0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
];
const DEFAULT_UUID_STR: &str = "00112233-4455-6677-8899-aabbccddeeff";

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

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let size = image_size()?;
    let sector_size = sector_size()?;
    let reference = reference_path();
    let options = FormatOptions::new(size, DEFAULT_UUID)
        .with_sector_size(sector_size)
        .with_inode_generation(InodeGenerationPolicy::XfsprogsDeterministicSeed);
    let plan = FormatPlan::new(options)?;
    let expected = MetadataImage::from_plan(&plan).expect("metadata image");

    make_reference_image(&reference, size, sector_size)?;
    let mut file = File::open(&reference)?;
    let actual = expected.summarize_reader_extents(&mut file)?;
    let diffs = expected.diff_manifest(&actual);

    if diffs.is_empty() {
        println!("xfsprogs reference matches current Rust metadata manifest");
        return Ok(());
    }

    println!(
        "xfsprogs reference differs from current Rust metadata manifest ({} diffs)",
        diffs.len()
    );
    for diff in diffs.iter().take(20) {
        println!("{}", DiffDisplay(diff));
    }
    if diffs.len() > 20 {
        println!("... {} more diffs", diffs.len() - 20);
    }
    Err("comparison failed".into())
}

fn image_size() -> Result<u64, Box<dyn std::error::Error>> {
    match env::args().nth(1) {
        Some(value) => Ok(value.parse()?),
        None => Ok(DEFAULT_SIZE),
    }
}

fn sector_size() -> Result<u32, Box<dyn std::error::Error>> {
    match env::args().nth(2) {
        Some(value) => Ok(value.parse()?),
        None => Ok(512),
    }
}

fn reference_path() -> PathBuf {
    let mut path = env::temp_dir();
    path.push(format!("mkfs-xfs-reference-{}.img", std::process::id()));
    path
}

fn make_reference_image(path: &Path, size: u64, sector_size: u32) -> io::Result<()> {
    match fs::remove_file(path) {
        Ok(()) => {}
        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
        Err(err) => return Err(err),
    }

    let file = OpenOptions::new().create_new(true).write(true).open(path)?;
    file.set_len(size)?;
    drop(file);

    let data_arg = format!("file,name={},size={size}", path.display());
    let metadata_arg = format!("uuid={DEFAULT_UUID_STR}");
    let sector_arg = format!("size={sector_size}");
    let output = Command::new("mkfs.xfs")
        .env("DETERMINISTIC_SEED", "1")
        .args([
            "-f",
            "-d",
            &data_arg,
            "-m",
            &metadata_arg,
            "-s",
            &sector_arg,
        ])
        .output()?;
    if output.status.success() {
        return Ok(());
    }

    Err(io::Error::other(format!(
        "mkfs.xfs failed with status {}\nstdout:\n{}\nstderr:\n{}",
        output.status,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )))
}

struct DiffDisplay<'a>(&'a ManifestDiff);

impl std::fmt::Display for DiffDisplay<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.0 {
            ManifestDiff::Missing { expected } => {
                write!(f, "missing expected {}", SummaryDisplay(expected))
            }
            ManifestDiff::Extra { actual } => write!(f, "extra actual {}", SummaryDisplay(actual)),
            ManifestDiff::Mismatch { expected, actual } => write!(
                f,
                "mismatch at offset {}: expected {}, actual {}",
                expected.offset,
                SummaryDisplay(expected),
                SummaryDisplay(actual)
            ),
        }
    }
}

struct SummaryDisplay<'a>(&'a MetadataExtentSummary);

impl std::fmt::Display for SummaryDisplay<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "len={} crc32c={:08x} magic={}",
            self.0.len,
            self.0.crc32c,
            MagicDisplay(self.0.magic)
        )
    }
}

struct MagicDisplay([u8; 4]);

impl std::fmt::Display for MagicDisplay {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for &byte in &self.0 {
            if byte.is_ascii_graphic() || byte == b' ' {
                write!(f, "{}", char::from(byte))?;
            } else {
                write!(f, "\\x{byte:02x}")?;
            }
        }
        Ok(())
    }
}
