use std::{
    fs::{self, File},
    io::{self, BufWriter, Read, Seek, Write},
    path::{Path, PathBuf},
    process::ExitCode,
};

use clap::Parser;
use stuff::tar::{TarHeader, TarReader, TarReaderEntry, TarWriter};

#[derive(Debug, Parser)]
struct Args {
    #[clap(short = 'C')]
    chdir: Option<PathBuf>,

    #[clap(short = 'c')]
    create: bool,
    #[clap(short = 't')]
    print: bool,
    #[clap(short = 'x')]
    extract: bool,

    #[clap(short = 'f')]
    archive: PathBuf,

    inputs: Vec<PathBuf>,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Action {
    Create,
    Print,
    Extract,
}

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("{0}: {1}")]
    Io(PathBuf, io::Error),
    #[error("No action specified, use -c/-t/-x")]
    NoAction,
    #[error("Multiple actions specified, use only -c/-t/-x")]
    MultipleActions,
    #[error("No inputs specified, aborting")]
    NoInputs,
}

trait ContextualizeError {
    type Output;

    fn contextualize<P: Into<PathBuf>>(self, p: P) -> Self::Output;
}

impl<T, E: ContextualizeError> ContextualizeError for Result<T, E> {
    type Output = Result<T, <E as ContextualizeError>::Output>;

    fn contextualize<P: Into<PathBuf>>(self, p: P) -> Self::Output {
        match self {
            Ok(val) => Ok(val),
            Err(error) => Err(error.contextualize(p)),
        }
    }
}

impl ContextualizeError for io::Error {
    type Output = Error;

    fn contextualize<P: Into<PathBuf>>(self, p: P) -> Self::Output {
        Error::Io(p.into(), self)
    }
}

fn create_archive(
    output: impl AsRef<Path>,
    inputs: &[PathBuf],
    chdir: Option<&PathBuf>,
) -> Result<(), Error> {
    fn write_path<W: Write>(
        writer: &mut TarWriter<W>,
        fs_path: &Path,
        archive_path: &Path,
    ) -> Result<(), Error> {
        if fs_path.is_file() {
            println!("{}", archive_path.display());
            let archive_path_str = archive_path.to_str().unwrap();
            writer
                .append_file(fs_path, archive_path_str)
                .contextualize(fs_path)?;
            Ok(())
        } else if fs_path.is_dir() {
            let dir = fs::read_dir(fs_path).contextualize(fs_path)?;
            for entry in dir {
                let entry = entry.contextualize(fs_path)?;
                let filename = entry.file_name();
                if filename.eq_ignore_ascii_case(".") || filename.eq_ignore_ascii_case("..") {
                    continue;
                }
                write_path(
                    writer,
                    &fs_path.join(&filename),
                    &archive_path.join(&filename),
                )?;
            }
            Ok(())
        } else {
            eprintln!("Skipping {}: not supported", fs_path.display());
            Ok(())
        }
    }

    if inputs.is_empty() {
        return Err(Error::NoInputs);
    }

    let archive = output.as_ref();
    let mut writer = File::create(archive)
        .map(BufWriter::new)
        .map(TarWriter::new)
        .contextualize(archive)?;

    let chdir = match chdir {
        Some(chdir) => chdir.as_path(),
        None => ".".as_ref(),
    };

    for input in inputs {
        if input.is_absolute() || input.starts_with("/") {
            // fs path == archive path
            write_path(&mut writer, input, input)?;
        } else {
            let fs_path = chdir.join(input);
            write_path(&mut writer, &fs_path, input)?;
        }
    }

    writer.finish().contextualize(archive)?;

    Ok(())
}

fn print_archive(archive: impl AsRef<Path>) -> Result<(), Error> {
    let archive = archive.as_ref();
    let input = File::open(archive).contextualize(archive)?;
    let mut reader = TarReader::new(input);

    loop {
        let Some((header, _)) = reader.next().contextualize(archive)? else {
            break;
        };

        println!("{}", header.name);
    }

    Ok(())
}

fn extract_archive(archive: impl AsRef<Path>, chdir: Option<&PathBuf>) -> Result<(), Error> {
    fn extract_file<R: Read + Seek>(
        entry: &mut TarReaderEntry<R>,
        header: &TarHeader,
        chdir: Option<&PathBuf>,
    ) -> Result<(), Error> {
        // TODO path sanitization
        let Some(raw_path) = header.name.as_str() else {
            eprintln!("Skipping invalid path {}", header.name);
            return Ok(());
        };
        let sanitized_path = raw_path.trim_start_matches('/');
        let sanitized_path = match chdir {
            Some(chdir) => chdir.join(sanitized_path),
            None => sanitized_path.into(),
        };

        println!("{}", sanitized_path.display());

        if header.is_file() {
            if let Some(parent) = sanitized_path.parent() {
                std::fs::create_dir_all(parent).contextualize(parent)?;
            }

            // Write the file
            let mut output = File::create(&sanitized_path)
                .map(BufWriter::new)
                .contextualize(&sanitized_path)?;

            let mut buffer = [0; 4096];
            loop {
                let len = entry.read(&mut buffer).contextualize(raw_path)?;
                if len == 0 {
                    break;
                }

                output
                    .write_all(&buffer[..len])
                    .contextualize(&sanitized_path)?;
            }

            Ok(())
        } else if header.is_dir() {
            std::fs::create_dir_all(&sanitized_path).contextualize(sanitized_path)
        } else {
            Ok(())
        }
    }

    let archive = archive.as_ref();
    let input = File::open(archive).contextualize(archive)?;
    let mut reader = TarReader::new(input);

    loop {
        let Some((header, mut entry)) = reader.next().contextualize(archive)? else {
            break;
        };

        extract_file(&mut entry, &header, chdir)
            .inspect_err(|e| eprintln!("{e}"))
            .ok();
    }

    Ok(())
}

fn run(args: &Args) -> Result<(), Error> {
    let action = match (args.create, args.print, args.extract) {
        (false, false, false) => return Err(Error::NoAction),
        (true, false, false) => Action::Create,
        (false, true, false) => Action::Print,
        (false, false, true) => Action::Extract,
        (_, _, _) => return Err(Error::MultipleActions),
    };

    match action {
        Action::Create => create_archive(&args.archive, &args.inputs, args.chdir.as_ref()),
        Action::Print => print_archive(&args.archive),
        Action::Extract => extract_archive(&args.archive, args.chdir.as_ref()),
    }
}

fn main() -> ExitCode {
    let args = Args::parse();
    match run(&args) {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("{error}");
            ExitCode::FAILURE
        }
    }
}
