#[macro_use] extern crate failure;
#[macro_use] extern crate nix;
#[macro_use] extern crate serde_derive;


extern crate libc;
extern crate clap;
extern crate serde;
extern crate toml;
extern crate ed25519_dalek;
extern crate sha2;
extern crate rand;
extern crate rustc_serialize;


thread_local! {
    pub static VERBOSE: RefCell<bool> = RefCell::new(false);
    pub static SYSOP: RefCell<bool> = RefCell::new(false);
}

pub fn verbose() -> bool {
    VERBOSE.with(|f| {
        *f.borrow()
    })
}

fn sysop() -> bool {
    SYSOP.with(|f| {
        *f.borrow()
    })
}

macro_rules! info {
    ($e:expr) => { if ::verbose() { println!("[+] {}", $e);} };
    ($fmt:expr, $($arg:tt)+) => { if ::verbose() { println!("[+] {}", format!($fmt, $($arg)+));} };
}
macro_rules! warn {
    ($e:expr) => { if ::verbose() { println!("WARNING: {}", $e);} };
    ($fmt:expr, $($arg:tt)+) => { if ::verbose() { println!("WARNING: {}", format!($fmt, $($arg)+));} };
}

macro_rules! notify {
    ($e:expr) => { println!("[+] {}", $e); };
    ($fmt:expr, $($arg:tt)+) => { println!("[+] {}", format!($fmt, $($arg)+)); };
}

use std::result;
use std::process::exit;

use failure::Error;
use clap::{App,Arg,ArgMatches, SubCommand};
use clap::AppSettings::*;
use sha2::Sha512;
use rand::OsRng;
use ed25519_dalek::Keypair;
use rustc_serialize::hex::ToHex;
use std::cell::RefCell;
use std::env;


pub use config::Config;
pub use metainfo::Metainfo;
pub use blockdev::BlockDev;
pub use partition::{Partition,MAX_METAINFO_LEN};
use unpacker::UpdateImageUnpacker;
use packer::UpdateImagePacker;
use boot::BootSelection;

mod boot;
mod metainfo;
mod partition;
mod blockdev;
mod config;
mod packer;
mod unpacker;
mod util;

pub type Result<T> = result::Result<T,Error>;

fn main() {
    match env::var("CITADEL_SYSOP") {
        Ok(_) => SYSOP.with(|f| *f.borrow_mut() = true),
        _ => {},
    };

    let mut app = App::new("citadel-rootfs")
        .about("Subgraph Citadel rootfs partition management")
        .settings(&[ArgRequiredElseHelp, ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder])
        .arg(Arg::with_name("v")
             .help("Verbose output")
             .short("v")
             .long("verbose"))
        .arg(Arg::with_name("config")
            .help("Optionally specify an alternate config file")
            .takes_value(true)
            .short("c") .long("config"))

        .subcommand(SubCommand::with_name("list")
            .about("Show information about all rootfs partitions"))

        .subcommand(SubCommand::with_name("which-boot")
            .about("Show which rootfs paritition would currently boot according to the boot selection algorithm"))

        .subcommand(SubCommand::with_name("verify-update")
            .about("Verify the signature of an update image"))

        .subcommand(SubCommand::with_name("update")
            .about("Download update if available and install")
            .arg(Arg::with_name("download-only")
                 .help("Only download available update, don't install")
                 .long("download")))

        .subcommand(SubCommand::with_name("install-update")
            .about("Install an update image")
            .arg(Arg::with_name("image")
                 .required(true)));
    if sysop() {

        app = app.subcommand(SubCommand::with_name("build-update")
            .about("Create an update image from a raw citadel-image.ext2 file")
            .arg(Arg::with_name("image")
                 .required(true)))
                    
        .subcommand(SubCommand::with_name("genkeys")
            .about("Generate a new update keypair"));
        
    }

    let matches = app.get_matches();


    let config = load_config(&matches);

    if matches.is_present("v") {
        VERBOSE.with(|f| *f.borrow_mut() = true);
    }

    let result = match matches.subcommand() {
        ("list", Some(_)) => list_cmd(&config),
        ("which-boot", Some(_)) => { which_boot_cmd(&config)},
        ("update", Some(m)) => update_cmd(&config, m),
        ("verify-update", Some(m)) => verify_update_cmd(&config, m),
        ("install-update", Some(m)) => install_update_cmd(&config, m),
        ("build-update", Some(m)) => build_update_cmd(&config, m),
        ("genkeys", Some(_)) => genkeys_cmd(),
        ("mount-rootfs", Some(m)) => mount_rootfs_cmd(&config, m),
        (s, Some(_)) => {info!("subcommand: {}", s); Ok(())},
        _ => Ok(()),
    };

    if let Err(e) = result {
        println!("{}", e);
        exit(1);
    }
}

fn load_config(arg_matches: &ArgMatches) -> Config {
    let config_load = match arg_matches.value_of("config") {
        Some(path) => Config::load(path),
        None => Config::load_default(),
    };
    match config_load {
        Ok(config) => config,
        Err(e) => {
            println!("{}", e);
            exit(1);
        }
    }
}

fn list_cmd(_config: &Config) -> Result<()> {
    println!("{:^30} {:^14} {:^8} {:^8} {:^12}", "DEVICE PATH", "MOUNTED", "CHANNEL", "VERSION", "STATUS");
    for p in Partition::rootfs_partitions()? {
        let info = partition_info(&p);
        println!("{:^30} {:^14} {:^8} {:^8} {:^12}", info.0, info.1, info.2, info.3, info.4);
    }
    Ok(())
}


fn partition_info(part: &Partition) -> (String,String,String,String,String) {
    let mounted = if part.is_mounted() { "[Yes]".to_string() } else { String::new() };
    let status = if part.is_initialized() { part.status_label() } else { "Not Initialized".to_string() };
    let (channel, version) = match part.metainfo() {
        Ok(meta) => (meta.channel().to_string(), meta.version().to_string()),
        _ => (String::new(), String::new()),
    };
    (part.path_str().to_string(), mounted, channel, version, status)
}

fn mount_rootfs_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> {
    // mounting installer rootfs should happen here too?
    // perhaps based on cmd line flag
    // maybe define guid like the gpt generator looks for
    // for: rootfs inside luks, rootfs outside luks
    let bs = BootSelection::load_partitions()?;
    let _p = match bs.choose_boot_partition() {
        Some(p) => p,
        None => bail!("None of the rootfs partitions have a bootable image"),
    };
    Ok(())
}

fn which_boot_cmd(_config: &Config) -> Result<()> {
    let select = BootSelection::load_partitions()?;
    match select.choose_boot_partition() {
        Some(part) => {
            notify!("Next boot will be from partition: {}", part.path_str());
        },
        None => {
            warn!("None of the rootfs partitions are currently in bootable state.");
            warn!("Unless a valid image is installed, computer will fail to boot");
        }
    }
    Ok(())
}

fn verify_update_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> {
    println!("do verify_update");
    Ok(())
}

fn update_cmd(_config: &Config, _matches: &ArgMatches) -> Result<()> {
    // note to self, remember to verify that downloaded update image version matches the version
    // which is expected.
    Ok(())

}

fn install_update_cmd(config: &Config, matches: &ArgMatches) -> Result<()> {
    let image_path = match matches.value_of("image") {
        Some(val) => val,
        None => bail!("install-update requires an image path"),
    };
    let unpack = UpdateImageUnpacker::open(image_path, config)?;
    info!("unpacking image channel: {} version: {}", unpack.metainfo().channel(), unpack.metainfo().version());
    unpack.unpack_disk_image()?;
    info!("decompressing image");
    unpack.decompress_disk_image()?;
    info!("verifying shasum");
    unpack.verify_shasum()?;

    let bs = BootSelection::load_partitions()?;
    let p = match bs.choose_install_partition() {
        Some(p) => p,
        None => bail!("None of the rootfs partitions are available to install update to"),
    };
    info!("installing to {}", p.path_str());
    unpack.write_partition(p)?;
    notify!("Update image successfully installed to {}", p.path_str());


    //update::UpdateImage::new(path);
    // 1) read header, extract metainfo
    // 2) verify signature on metainfo
    // 3) determine if this version/channel makes sense to be installed
    // 4) xtrat image data to temporary file with https://crates.io/crates/lzma-rs
    // 5) verify sha256 on image data
    //
    // 6) choose rootfs partition
    // 7) write partition info block, setting status to INVALID
    // 8) write update image to device
    // 9) run verifyupdate using provided --salt
    // 10) re-rewrite parition info block with status NEW
    Ok(())
}

fn build_update_cmd(config: &Config, matches: &ArgMatches) -> Result<()> {
    let image_path = match matches.value_of("image") {
        Some(val) => val,
        None => bail!("build-update requires an image path"),
    };
    let channel = match config.get_default_channel() {
        Some(ch) => ch,
        None => bail!("Could not determine default channel from config file"),
    };
    let mut builder = UpdateImagePacker::new(config, channel, image_path)?;
    builder.build()?;
    Ok(())
}

fn genkeys_cmd() -> Result<()> {
    let mut rng = OsRng::new()?;
    let keypair = Keypair::generate::<Sha512>(&mut rng);

    println!("pubkey = \"{}\"", keypair.public.to_bytes().to_hex());
    println!("privkey = \"{}\"", keypair.to_bytes().to_hex());

    Ok(())
}