extern crate chrono;
extern crate clap;
extern crate orizentic;

use chrono::Duration;
use clap::{App, Arg, ArgMatches, SubCommand};
use std::env;

use orizentic::*;

#[derive(Debug)]
enum OrizenticErr {
    ParseError(std::num::ParseIntError),
}

// ORIZENTIC_DB
// ORIZENTIC_SECRET
//
// list
// create
// revoke
// encode
pub fn main() {
    let db_path = env::var_os("ORIZENTIC_DB").map(|str| {
        str.into_string()
            .expect("ORIZENTIC_DB contains invalid Unicode sequences")
    });
    let secret = env::var_os("ORIZENTIC_SECRET").map(|str| {
        Secret(
            str.into_string()
                .map(|s| s.into_bytes())
                .expect("ORIZENTIC_SECRET contains invalid Unicode sequences"),
        )
    });

    let matches = App::new("orizentic cli")
        .subcommand(SubCommand::with_name("list"))
        .subcommand(
            SubCommand::with_name("create")
                .arg(
                    Arg::with_name("issuer")
                        .long("issuer")
                        .takes_value(true)
                        .required(true),
                )
                .arg(
                    Arg::with_name("ttl")
                        .long("ttl")
                        .takes_value(true)
                        .required(true),
                )
                .arg(
                    Arg::with_name("resource")
                        .long("resource")
                        .takes_value(true)
                        .required(true),
                )
                .arg(
                    Arg::with_name("username")
                        .long("username")
                        .takes_value(true)
                        .required(true),
                )
                .arg(
                    Arg::with_name("perms")
                        .long("perms")
                        .takes_value(true)
                        .required(true),
                ),
        )
        .subcommand(
            SubCommand::with_name("revoke").arg(
                Arg::with_name("id")
                    .long("id")
                    .takes_value(true)
                    .required(true),
            ),
        )
        .subcommand(
            SubCommand::with_name("encode").arg(
                Arg::with_name("id")
                    .long("id")
                    .takes_value(true)
                    .required(true),
            ),
        )
        .get_matches();

    match matches.subcommand() {
        ("list", _) => list_tokens(db_path),
        ("create", Some(args)) => create_token(db_path, secret, args),
        ("revoke", Some(args)) => revoke_token(db_path, args),
        ("encode", Some(args)) => encode_token(db_path, secret, args),
        (cmd, _) => {
            println!("unknown subcommand: {}", cmd);
        }
    }
}

fn list_tokens(db_path: Option<String>) {
    let db_path_ = db_path.expect("ORIZENTIC_DB is required for this operation");
    let claimsets = orizentic::filedb::load_claims_from_file(&db_path_);
    match claimsets {
        Ok(claimsets_) => {
            for claimset in claimsets_ {
                println!("[{}]", claimset.id);
                println!("Audience:         {}", String::from(claimset.audience));
                match claimset.expiration {
                    Some(expiration) => println!(
                        "Expiration:       {}",
                        expiration.format("%Y-%m-%d %H:%M:%S")
                    ),
                    None => println!("Expiration:       None"),
                }
                println!("Issuer:           {}", claimset.issuer.0);
                println!(
                    "Issued At:        {}",
                    claimset.issued_at.format("%Y-%m-%d %H:%M:%S")
                );
                println!("Resource Name:    {}", claimset.resource.0);

                let perm_val: String = itertools::Itertools::intersperse(
                    claimset.permissions.0.clone().into_iter(),
                    String::from(", "),
                )
                .collect();
                println!("Permissions:      {}", perm_val);
                println!("")
            }
        }
        Err(err) => {
            println!("claimset failed to load: {}", err);
            std::process::exit(1);
        }
    }
}

fn create_token(db_path: Option<String>, secret: Option<Secret>, args: &ArgMatches) {
    let db_path_ = db_path.expect("ORIZENTIC_DB is required for this operation");
    let secret_ = secret.expect("ORIZENTIC_SECRET is required for this operation");
    let issuer = args
        .value_of("issuer")
        .map(|x| Issuer(String::from(x)))
        .expect("--issuer is a required parameter");
    let ttl: Option<TTL> = args.value_of("ttl").map(|x| {
        x.parse()
            .and_then(|d| Ok(TTL(Duration::seconds(d))))
            .map_err(|err| OrizenticErr::ParseError(err))
            .expect("Failed to parse TTL")
    });
    let resource_name = args
        .value_of("resource")
        .map(|x| ResourceName(String::from(x)))
        .expect("--resource is a required parameter");
    let username = args
        .value_of("username")
        .map(|x| Username::from(x))
        .expect("--username is a required parameter");
    let perms: Permissions = args
        .value_of("perms")
        .map(|str| Permissions(str.split(',').map(|s| String::from(s)).collect()))
        .expect("--permissions is a required parameter");

    let claimsets = orizentic::filedb::load_claims_from_file(&db_path_);
    match claimsets {
        Err(err) => {
            println!("claimset failed to load: {}", err);
            std::process::exit(1);
        }
        Ok(claimsets_) => {
            let new_claimset = ClaimSet::new(issuer, ttl, resource_name, username, perms);
            let mut ctx = orizentic::OrizenticCtx::new(secret_, claimsets_);
            ctx.add_claimset(new_claimset.clone());
            match orizentic::filedb::save_claims_to_file(&ctx.list_claimsets(), &db_path_) {
                Err(err) => {
                    println!("Failed to write claimset to file: {:?}", err);
                    std::process::exit(1);
                }
                Ok(_) => match ctx.encode_claimset(&new_claimset) {
                    Ok(token) => println!("{}", token.text),
                    Err(err) => {
                        println!("token could not be encoded: {:?}", err);
                        std::process::exit(1);
                    }
                },
            }
        }
    }
}

fn revoke_token(db_path: Option<String>, args: &ArgMatches) {
    let db_path_ = db_path.expect("ORIZENTIC_DB is required for this operation");
    let claimsets = orizentic::filedb::load_claims_from_file(&db_path_);

    match claimsets {
        Err(err) => {
            println!("claimset failed to load: {}", err);
            std::process::exit(1);
        }
        Ok(claimsets_) => {
            let id = args
                .value_of("id")
                .map(String::from)
                .expect("--id is a required parameter");
            let mut ctx =
                orizentic::OrizenticCtx::new(Secret(String::from("").into_bytes()), claimsets_);
            ctx.revoke_by_uuid(&id);
            match orizentic::filedb::save_claims_to_file(&ctx.list_claimsets(), &db_path_) {
                Err(err) => {
                    println!("Failed to write claimset to file: {:?}", err);
                    std::process::exit(1);
                }
                Ok(_) => {}
            }
        }
    }
}

fn encode_token(db_path: Option<String>, secret: Option<Secret>, args: &ArgMatches) {
    let db_path_ = db_path.expect("ORIZENTIC_DB is required for this operation");
    let secret_ = secret.expect("ORIZENTIC_SECRET is required for this operation");
    let id = args
        .value_of("id")
        .map(String::from)
        .expect("--id is a required parameter");

    let claimsets = orizentic::filedb::load_claims_from_file(&db_path_);
    match claimsets {
        Err(err) => {
            println!("claimset failed to load: {}", err);
            std::process::exit(1);
        }
        Ok(claimsets_) => {
            let ctx = orizentic::OrizenticCtx::new(secret_, claimsets_);
            let claimset = ctx.find_claimset(&id);
            match claimset {
                Some(claimset_) => match ctx.encode_claimset(&claimset_) {
                    Ok(token) => println!("{}", token.text),
                    Err(err) => {
                        println!("token could not be encoded: {:?}", err);
                        std::process::exit(1);
                    }
                },
                None => {
                    println!("No claimset found");
                    std::process::exit(1);
                }
            }
        }
    }
}
