use comunic_server::{cleanup_thread, server};
use comunic_server::constants::admin::{ADMIN_ROLES_LIST, AdminRole};
use comunic_server::data::admin::NewAdmin;
use comunic_server::data::api_client::APIClient;
use comunic_server::data::config::{conf, Config};
use comunic_server::data::error::Res;
use comunic_server::data::user::UserID;
use comunic_server::helpers::{account_helper, admin_account_helper, admin_roles_helper, api_helper, database};
use comunic_server::utils::date_utils::current_year;

type MainActionFunction = Res;

struct Action {
    name: &'static str,
    description: &'static str,
    arguments: Vec<&'static str>,
    function: Box<dyn Fn(Vec<String>) -> MainActionFunction>,
}

fn get_actions() -> Vec<Action> {
    vec![
        // Start server
        Action {
            name: "serve",
            description: "Start the Comunic Server (default action)",
            arguments: vec![],
            function: Box::new(serve),
        },

        // Show help
        Action {
            name: "help",
            description: "Show this help",
            arguments: vec![],
            function: Box::new(help),
        },

        // Get the list of registered clients
        Action {
            name: "clients_list",
            description: "Get the list of registered clients",
            arguments: vec![],
            function: Box::new(list_clients),
        },

        // Register a new API client
        Action {
            name: "register_client",
            description: "Register a new API client",
            arguments: vec!["name", "origin", "comment"],
            function: Box::new(register_client),
        },

        // Reset a user password
        Action {
            name: "reset_password",
            description: "Create a password reset URL for a user",
            arguments: vec!["user_id"],
            function: Box::new(reset_password),
        },

        // Create a new administrator
        Action {
            name: "create_admin",
            description: "Create a new administrator account",
            arguments: vec!["name", "email"],
            function: Box::new(create_admin),
        },

        // Create a reset token for an admin
        Action {
            name: "create_admin_reset_token",
            description: "Create a new reset token to register a new access key to an admin account",
            arguments: vec!["email"],
            function: Box::new(create_admin_reset_token),
        },

        // Get the list of available admin roles
        Action {
            name: "list_admin_roles",
            description: "Get the list of available admin roles",
            arguments: vec![],
            function: Box::new(list_admin_roles),
        },

        // Attribute a role to an admin
        Action {
            name: "grant_admin_role",
            description: "Grant a role to an admin",
            arguments: vec!["mail", "role_id"],
            function: Box::new(grant_admin_role),
        },
    ]
}


fn main() {
    let args: Vec<String> = std::env::args().collect();
    let conf_file = match args.get(1) {
        Some(el) => el.to_string(),
        None => {
            eprintln!("Please specify configuration file as first argument!");
            std::process::exit(-3);
        }
    };

    // Load configuration
    Config::load(&conf_file).expect("Could not load configuration!");

    // Connect to the database
    database::connect(&conf().database).expect("Could not connect to database!");

    // Get selected action
    let action = args
        .get(2)
        .map(|a| a.as_str())
        .unwrap_or("serve")
        .to_string();

    let actions = get_actions();

    let selected_action = actions
        .iter().find(|p| p.name.eq(&action));

    let selected_action = match selected_action {
        None => {
            eprintln!("Action {} invalid! For more information try 'help'!", action);
            std::process::exit(-1);
        }
        Some(a) => a
    };

    if !selected_action.arguments.is_empty() && selected_action.arguments.len() + 3 != args.len() {
        eprintln!("Invalid number of arguments!");
        std::process::exit(-2);
    }

    let args = match args.len() {
        0 | 1 | 2 => vec![],
        _ => (&args[3..]).to_vec()
    };

    let res = (selected_action.function)(args.to_vec());
    res.expect("Failed to execute action!");
}

/// Start Comunic Server (main action)
fn serve(_a: Vec<String>) -> Res {
    // Start cleanup thread
    cleanup_thread::start().expect("Failed to start cleanup thread!");

    // Start the server
    Ok(server::start_server(conf())?)
}

fn help(_a: Vec<String>) -> Res {
    println!("Comunic API v3 Server - (c) Pierre HUBERT 2012 - {}", current_year());


    println!("Usage: {} [conf-file] [action] [args...]", std::env::args().next().unwrap());
    println!("Available actions:");
    for action in get_actions() {
        println!("\t{} {}\t- {}",
                 action.name,
                 action.arguments.iter().map(|s| format!("[{}]", s)).collect::<Vec<String>>().join(" "),
                 action.description
        );
    }

    Ok(())
}

fn list_clients(_args: Vec<String>) -> Res {
    for client in api_helper::get_clients()? {
        println!(
            "Client {}\n* Name: {}\n* Domain: {}\n* Comment: {}\n* Default tokens expiration time: {}\n* Firebase project: {}\n\n",
            client.id,
            client.name,
            client.domain.unwrap_or("None".to_string()),
            client.comment.unwrap_or("None".to_string()),
            client.default_expiration_time,
            client.firebase_project_name.unwrap_or("None".to_string())
        );
    }
    Ok(())
}

fn register_client(args: Vec<String>) -> Res {
    let client = APIClient {
        id: 0,
        name: args[0].to_string(),
        domain: match args[1].is_empty() {
            true => None,
            false => Some(args[1].to_string())
        },
        comment: match args[2].is_empty() {
            true => None,
            false => Some(args[2].to_string())
        },
        default_expiration_time: 0,
        firebase_project_name: None,
        firebase_service_account_file: None,
    };

    let id = api_helper::register_client(&client)?;
    println!("Registered client #{}", id);

    Ok(())
}

fn reset_password(args: Vec<String>) -> Res {
    let user_id = UserID::new(args[0].parse::<u64>()?);
    let token = account_helper::generate_password_reset_token(&user_id)?;

    println!("{}", conf().password_reset_url.replace("{TOKEN}", &token));

    Ok(())
}

fn create_admin(args: Vec<String>) -> Res {
    let new_admin = NewAdmin {
        name: args[0].to_string(),
        email: args[1].to_string(),
    };

    if !mailchecker::is_valid(&new_admin.email) {
        eprintln!("Specified email address is not valid!");
        std::process::exit(-1);
    }

    let id = admin_account_helper::create(&new_admin)
        .expect("Failed to create account!");

    println!("* New admin ID: {}", id.id());

    Ok(())
}

fn create_admin_reset_token(args: Vec<String>) -> Res {
    let admin = admin_account_helper::find_admin_by_email(&args[0])
        .expect("Failed to load admin information!");

    println!("Generate a new reset token for {} ({})", admin.name, admin.email);

    let token = admin_account_helper::create_new_reset_token(admin.id)
        .expect("Failed to create admin reset token!");

    println!("Reset token: {}", token.token);

    Ok(())
}

fn list_admin_roles(_a: Vec<String>) -> Res {
    println!("Here are the currently defined roles in the code:\n");

    for role in Vec::from(ADMIN_ROLES_LIST) {
        println!("* {} - {}\n{}\n", role.id, role.name, role.description);
    }

    Ok(())
}

fn grant_admin_role(args: Vec<String>) -> Res {
    let role = AdminRole::from_id(&args[1])
        .expect("Requested role does not exist!");

    let admin = admin_account_helper::find_admin_by_email(&args[0])
        .expect("Failed to load admin information!");

    if admin.roles.contains(&role) {
        eprintln!("The administrator has already this role!");
        std::process::exit(-3);
    }

    admin_roles_helper::add_role(admin.id, role)?;

    println!("Success.");
    Ok(())
}