#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]

use std::path::Path;

use hpk::Hooks;

mod cli;
use {
    clap::ArgMatches,
    cli::cli,
    hpk::{CreationError, Creator, Dependency, InstallMessage, Installer, Message, Specs, Version},
    indicatif::{ProgressBar, ProgressStyle},
    ron::ser::{to_writer_pretty, PrettyConfig},
    std::{
        env,
        error::Error,
        ffi::OsStr,
        fmt,
        fs::File,
        io::{self, BufWriter, ErrorKind},
        path::PathBuf,
        sync::mpsc,
        thread,
    },
};

static TEMPLATE: &str = "[ {prefix} ] {wide_bar}{pos:>5.cyan}/{len:5.green}{msg:>30}";

fn main() -> Result<(), Box<dyn Error>> {
    let matches = cli().get_matches();
    match matches.subcommand() {
        Some(("create", matches)) => create(matches)?,
        Some(("init", matches)) => {
            let specsfile = init(matches)?;
            println!("Created specsfile {}", specsfile.display());
            if matches.get_flag("edit") {
                cli::edit(specsfile.to_str().unwrap())?;
            }
        }
        Some(("search", matches)) => search(matches)?,
        Some(("install", matches)) => install(matches)?,
        Some(("remove", matches)) => remove(matches)?,
        Some(("upgrade", _)) => upgrade()?,
        _ => {}
    }
    Ok(())
}

fn create(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let dir = PathBuf::from(matches.get_one::<String>("directory").unwrap().to_string());
    let outdir = if let Some(d) = matches.get_one::<String>("output") {
        PathBuf::from(&d.to_string())
    } else {
        env::current_dir()?
    };
    let mut specs = if let Some(s) = matches.get_one::<String>("specs") {
        let path = PathBuf::from(s.to_string());
        let fd = File::open(path)?;
        ron::de::from_reader(fd)?
    } else {
        Specs::default()
    };
    if let Some(n) = matches.get_one::<String>("name") {
        specs.name = n.to_string();
    }
    if let Some(v) = matches.get_one::<String>("package-version") {
        specs.version = v.to_string().parse::<Version>()?;
    }
    if let Some(r) = matches.get_one::<u8>("release") {
        specs.release = *r;
    }
    if let Some(deps) = matches.get_many::<String>("dependencies") {
        for d in deps {
            let d = Dependency {
                name: d.to_string(),
                version: (None, None),
            };
            specs.dependencies.push(d);
        }
    }
    let creator = Creator::new(&dir, specs.clone())?;
    let (sender, receiver) = mpsc::channel();
    let len = creator.len();
    let pb = ProgressBar::new(len as u64);
    pb.set_style(ProgressStyle::with_template(TEMPLATE).unwrap());
    pb.set_prefix("Adding files");
    pb.println(format!(
        "Creating package {}-{}_{}.tar.zst",
        &specs.name, &specs.version, &specs.release
    ));
    let handle = thread::spawn(move || {
        for msg in receiver.iter() {
            match msg {
                Message::MemberAdded(s) => {
                    pb.set_message(s.split('/').last().unwrap().to_string());
                    pb.inc(1);
                }
                Message::Success(_s) => {
                    pb.inc(1);
                    pb.finish_and_clear();
                    break;
                }
            }
        }
        Ok::<(), CreationError>(())
    });
    creator.create(&outdir, &sender)?;
    match handle.join() {
        Ok(_) => {
            println!("Package created successfully");
            Ok(())
        }
        Err(e) => Err(io::Error::new(ErrorKind::Other, format!("{e:?}")).into()),
    }
}

fn install(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let Some(root) = matches.get_one::<String>("root") else {
        unreachable!();
    };
    if let Some(archive) = matches.get_one::<String>("local") {
        install_local(archive, root)?;
    } else if let Some(package) = matches.get_one::<String>("package") {
        unimplemented!();
    }
    Ok(())
}

fn install_local<P: AsRef<OsStr> + fmt::Display>(
    archive: P,
    root: P,
) -> Result<(), Box<dyn Error>> {
    let (sender, receiver) = mpsc::channel();
    let pb = ProgressBar::new(0);
    pb.set_style(ProgressStyle::with_template(TEMPLATE).unwrap());
    pb.println(format!("Installing package {}", archive,));
    let Some(r) = PathBuf::from(&root).to_str().map(|x| x.to_string()) else {
        return Err(io::Error::new(ErrorKind::Other, "bad path").into());
    };
    let mut hooks = Hooks::new(&r);
    let installer = Installer::new_for_file(root, archive)?;
    let handle = thread::spawn(move || {
        for msg in receiver.iter() {
            match msg {
                InstallMessage::ArchiveLen(len) => pb.set_length(len.try_into().unwrap()),
                InstallMessage::LinkCreated(link) => {
                    pb.inc(1);
                }
                InstallMessage::DirectoryCreated(dir) => {
                    pb.inc(1);
                }
                InstallMessage::FileInstalled(file) => {
                    pb.inc(1);
                }
                _ => {}
            }
        }
    });
    installer.install(&mut hooks, sender)?;
    match handle.join() {
        Ok(package) => {
            println!("hooks: {package:?}");
            Ok(())
        }
        Err(_) => Err(io::Error::new(ErrorKind::Other, "Unknown thread error").into()),
    }
}

fn init(matches: &ArgMatches) -> Result<PathBuf, Box<dyn Error>> {
    let specsfile = PathBuf::from("specs.ron");
    let cfg = PrettyConfig::new().struct_names(true);
    let buf = File::create(&specsfile)?;
    let writer = BufWriter::new(buf);
    let specs = create_specs(matches)?;
    to_writer_pretty(writer, &specs, cfg)?;
    Ok(specsfile)
}

fn search(_matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    unimplemented!();
}

fn remove(_matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    unimplemented!();
}

fn upgrade() -> Result<(), Box<dyn Error>> {
    unimplemented!();
}

#[allow(clippy::cast_possible_truncation)]
pub fn create_specs(matches: &ArgMatches) -> Result<Specs, Box<dyn Error>> {
    let mut specs = Specs::default();
    let mut name: Option<String> = None;
    let mut version: Option<Version> = None;
    let cwd = env::current_dir()?;
    if let Some(dir) = cwd.file_name().and_then(OsStr::to_str) {
        if let Some((n, v)) = dir.split_once('-') {
            name = Some(n.to_string());
            if let Ok(v) = v.parse() {
                version = Some(v);
            }
        }
    }
    if let Some(name) = matches.get_one::<String>("name") {
        specs.name = name.to_string();
    } else if let Some(n) = name {
        specs.name = n;
    }
    if let Some(version) = matches.get_one::<String>("package-version") {
        specs.version = version.parse()?;
    } else if let Some(v) = version {
        specs.version = v;
    }
    if let Some(release) = matches.get_one::<usize>("release") {
        specs.release = *release as u8;
    }
    if let Some(deps) = matches.get_many::<String>("dependencies") {
        let deps = deps
            .map(|d| Dependency {
                name: d.to_string(),
                version: (None, None),
            })
            .collect::<Vec<Dependency>>();
        specs.dependencies = deps;
    }
    Ok(specs)
}
