extern crate db;
extern crate chrono;
extern crate clap;
extern crate dotenv;
extern crate getrandom;
extern crate bcrypt;
extern crate base64;
extern crate serde;
extern crate jsonwebtoken;
#[macro_use] extern crate lazy_static;

use std::net::SocketAddr;
use std::convert::Infallible; // our main dispatcher basically never fails hence why we use this
use std::env::{self, set_var};

use tokio;
use hyper::{
    self, 
    Server,
    Response, Request, Body,
    Method, StatusCode,
    service::{make_service_fn, service_fn}
};
use mysql_async::Pool;

use dotenv::dotenv;
use clap::{Arg, App};
use auth::AuthReason;

mod auth;

mod routes;
mod meta;
mod invites;
mod channels;
mod members;
mod perms;
mod messages;
mod admin;

mod http;
mod testing;

const NO_ERR: u16 = 0;
const CONFIG_ERR: u16 = 1;
const SHUTDOWN_ERR: u16 = 2;

async fn route_dispatcher(pool: &Pool, resp: &mut Response<Body>, meth: &Method, path: &str, params: serde_json::Value) {
    // At some point we should have some way of hiding this obnoxious complexity
    const GET: &Method = &Method::GET;
    const POST: &Method = &Method::POST;
    const DELETE: &Method = &Method::DELETE;
    match (meth, path) {
        /* INVITES */
        (GET, routes::INVITE_CREATE) => invites::create(pool, resp, params).await,
        (GET, routes::INVITE_JOIN) => invites::join(pool, resp, params).await,
        /* CHANNELS */
        (GET, routes::CHANNELS_LIST) => channels::list_channels(pool, resp).await,
        (POST, routes::CHANNELS_CREATE) => channels::create_channel(pool, resp, params).await,
        (DELETE, routes::CHANNELS_DELETE) => channels::delete_channel(pool, resp, params).await,
        /* MESSAGING */
        (POST, routes::MESSAGE_SEND) => messages::send_message(pool, resp, params).await,
        (GET, routes::MESSAGE_TIME_RANGE) => messages::get_by_time(pool, resp, params).await,
        (GET, routes::MESSAGE_FROM_ID) =>messages::from_id(pool, resp, params).await,
        /* ADMIN */
        (POST, routes::SET_PERMS_BY_ADMIN) => admin::set_permissions(pool, resp, params).await,
        /* MEMBERS */
        (GET, routes::GET_ONLINE_MEMBERS) => members::get_online_members(pool, resp).await,
        /* OWNER */
        (POST, routes::SET_NEW_ADMIN) => admin::new_admin(pool, resp, params).await,
        /* META ROUTE */
        (GET, routes::META) => meta::server_meta(resp).await,
        _ => {
            eprintln!("\tNOT FOUND: {}: {}", meth, path);
            *resp.status_mut() = StatusCode::NOT_FOUND
        }
    }
}

async fn main_responder(request: Request<Body>) -> Result<Response<Body>, hyper::Error>{
    use AuthReason::*;
    let mut response = Response::new(Body::empty());

    let (parts, mut body) = request.into_parts();
    let method = parts.method;
    let path = parts.uri.path();

    let params_res = http::parse_json_params(&mut body).await;

    if let Ok(params) = params_res {
        let mysql_pool = Pool::new(&env::var("DATABASE_URL").unwrap());
        match auth::wall_entry(path, &mysql_pool, &params).await {
            OpenAuth | Good =>  route_dispatcher(&mysql_pool, &mut response, &method, path, params).await,
            LoginValid => auth::login_get_jwt(&mysql_pool, &mut response, params).await,
            NoKey | BadKey =>   *response.status_mut() = StatusCode::UNAUTHORIZED,
            ServerIssue(msg) => {
                println!("\tAUTH : 500 [{}]", msg);
                *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
            }
        }
    }
    else {
        println!("\tPARSER: Parameter parsing failed");
        *response.status_mut() = StatusCode::BAD_REQUEST;
    }
    Ok(response)
}

async fn shutdown_signal() {
    tokio::signal::ctrl_c()
        .await
        .expect("Failed to capture ctrl-c signal");
}

async fn start_server(ecode: u16, port: u16) -> u16 {
    println!("Servering on localhost:{}", port);
    let addr = SocketAddr::from(([127,0,0,1], port));
    let service = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(main_responder))
    });
    let server = Server::bind(&addr).serve(service);
    let graceful_shutdown = server.with_graceful_shutdown(shutdown_signal());

    if let Err(e) = graceful_shutdown.await {
        eprintln!("Server shutdown error: {}", e);
        return ecode | SHUTDOWN_ERR;
    }
    else {
        return ecode
    }

}

async fn attempt_owner_creation(name: &str) {
    /*
     * Attempts to create an owner level account 'name' as the name
     * Writes succesful output to stdout
     * Writes error output to stderr
     * NOTE: Messy because there's 0 other places where this kind of direct
     * functionality is required. db-lib is basically built to talk to the api
     * */
    let p = Pool::new(&env::var("DATABASE_URL").unwrap());
    let owner_secret = auth::generate_secret();
    if let Ok(enc_secret) = auth::encrypt_secret(&owner_secret) {
        if let Ok(response) = db::member::Member::add(&p, name, &enc_secret,  perms::OWNER).await {
            match response {
                db::Response::Row(mut owner) =>  {
                    owner.secret = owner_secret; // giving the secret itself back to the user
                    println!("{}", serde_json::to_string(&owner).expect("SQL query passed but serde couldn't parse the data for some reason"))
                },
                db::Response::Empty => {
                    eprintln!("SQL server failed to return owner data, check configs and also the members table to make sure there's nothing there by accident");
                },
                _ => {}
            };
        }
        else {
            eprintln!("Could not communicate with the SQL server, check your configs!");
        }
    }
    else {
        eprintln!("Could not generate a proper secret");
    }
    p.disconnect();
}

#[tokio::main]
async fn main() -> Result<(), u16>{

    let mut main_ret: u16 = 0;
    let d_result = dotenv();

    // check for a database_url before the override we get from the cmd line
    if let Err(_d) = d_result {
        if let Err(_e) = env::var("DATABASE_URL") {
            main_ret |= CONFIG_ERR;
        }
    }

    let args = App::new("Freechat Server")
        .version("0.1")
        .author("shockrah")
        .about("Decentralized chat system")
        .arg(Arg::with_name("db-url")
             .short("d")
             .long("db-url")
             .value_name("DATABASE URL")
             .help("Sets the DATABASE URL via an environment variable")
             .takes_value(true))
        .arg(Arg::with_name("create-owner")
             .short("c")
             .long("create-owner")
             .value_name("Owner")
             .help("Creates an account with full permissions in the SQL database."))
        .arg(Arg::with_name("server")
             .short("s")
             .long("server")
             .help("Starts the API server"))
        .arg(Arg::with_name("port")
             .short("p")
             .long("port")
             .default_value("4536")
             .help("Set the port to use: Default is 4536"))
        .get_matches();


    if args.args.len() == 0 {
		println!("Freechat Server 0.1
shockrah
Decentralized chat system

USAGE:
    freechat-server [FLAGS] [OPTIONS]

FLAGS:
    -h, --help       Prints help information
    -s, --server     Starts the API server
    -V, --version    Prints version information

OPTIONS:
    -c, --create-owner <Owner>     Creates an account with full permissions in the SQL database.
    -d, --db-url <DATABASE URL>    Sets the DATABASE URL via an environment variable");
    }

    if let Some(db_url) = args.value_of("db-url") {
        set_var("DATABASE_URL", db_url);
    }

    // safe because we have a default value set in code
    let port = args.value_of("port").unwrap().to_string();
    let port: u16 = port.parse().unwrap_or(4536);

    if let Some(owner_name) = args.value_of("create-owner") {
        attempt_owner_creation(owner_name).await;
    }

    if args.is_present("server") {
        if main_ret == NO_ERR {
            main_ret = start_server(main_ret, port).await;
        }
    }

    if main_ret != 0 {
        // dumb as heck loggin method here
        if main_ret & CONFIG_ERR != 0 {println!("ERROR: Config was not setup properly => Missing {{DATABASE_URL}}");}
        if main_ret & SHUTDOWN_ERR != 0 {println!("ERROR: Couldn't shutdown gracefully");}
        Err(main_ret)
    }
    else {
        Ok(())
    }
}
