// A Rust Site Engine
// Copyright 2020-2021 Anthony Martinez
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

use std::fs::create_dir_all;
use std::{io::BufRead, usize};
use std::path::Path;

use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};

use serde::{Serialize, Deserialize};
use toml;

use super::auth;
use super::common;

fn args() -> App<'static, 'static> {
    App::new("Caty's Blog")
	.version("1.0")
	.author("Anthony Martinez")
        .setting(AppSettings::ArgRequiredElseHelp)
        .subcommand(SubCommand::with_name("run")
		    .about("Run the blog server")
		    .arg(Arg::with_name("config")
			 .help("Provides the path to the server configuration file.")
			 .required(true)
			 .takes_value(true)
			 .index(1)))
	.subcommand(SubCommand::with_name("new")
		    .about("Generates a base directory structure and configuration file for a new blog")
		    )
}

/// TODO Document this public function
/// And Include an Example of its Use
pub fn load() -> Result<AppConfig, Box<dyn std::error::Error>> {
    let matches = args().get_matches();
    let config: Result<AppConfig, Box<dyn std::error::Error>>;
    if matches.is_present("run") {
	config = runner_config(matches);
    } else if matches.is_present("new") {
	let reader = std::io::stdin();
	let mut reader = reader.lock();
	let current_path = std::env::current_dir()?;
	config = AppConfig::generate(current_path, &mut reader);
    } else {
	let msg = format!("Unable to load configuration");
	config = Err(From::from(msg));
    }

    config
}

fn runner_config(m: ArgMatches) -> Result<AppConfig, Box<dyn std::error::Error>> {
    if let Some(run) = m.subcommand_matches("run") {
	let value = run.value_of("config").unwrap();
	let config = AppConfig::new(value)?;
	Ok(config)
    } else {
	let msg = format!("Failed to read arguments for 'run' subcommand");
	Err(From::from(msg))
    }
}

fn get_input<R: BufRead>(prompt: &str, reader: &mut R) -> Result<String, Box<dyn std::error::Error>> {
    let mut buf = String::new();
    println!("{}", prompt);
    reader.read_line(&mut buf)?;
    let buf = String::from(buf
			   .trim_start_matches(char::is_whitespace)
			   .trim_end_matches(char::is_whitespace));
    Ok(buf)
}

fn csv_to_vec(csv: &str) -> Vec<String> {
    let val_vec: Vec<String> = csv.split(",")
        .map(|s| s
             .trim_start_matches(char::is_whitespace)
             .trim_end_matches(char::is_whitespace)
             .to_string())
        .collect();

    val_vec
}

/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct Blog {
    pub name: String,
    pub author: String,
    pub topics: Vec<String>,
}

impl Blog {
    pub fn new_from_input<R: BufRead>(reader: &mut R) -> Result<Blog, Box<dyn std::error::Error>> {
	let name = get_input("Please enter a name for the blog: ", reader)?;
	let author = get_input("Please enter the blog author's name: ", reader)?;
	let topics = get_input("Please enter comma-separated blog topics: ", reader)?;
	let topics = csv_to_vec(&topics);
	let blog = Blog { name, author, topics };

	Ok(blog)
    }
}

/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct Credentials {
    pub user: String,
    pub password: String,
    pub token: String,
}

impl Credentials {
    pub fn new_from_input<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<Credentials, Box<dyn std::error::Error>> {
	let user = get_input("Please enter an username for the blog admin: ", reader)?;
	const PASSWORD_LEN: usize = 32;
	let password = auth::generate_secret(PASSWORD_LEN)?;
	let password_file = dir.as_ref().join("admin.pass");
	common::str_to_ro_file(&password, password_file)?;
	let password = auth::generate_argon2_phc(&password)?;

	const TOKEN_LEN: usize = 34;
	let token = auth::generate_secret(TOKEN_LEN)?;
	let token = token.as_bytes();
	let token = auth::BASE32_NOPAD.encode(token);
	let token_file = dir.as_ref().join("admin.totp");
	common::str_to_ro_file(&token, token_file)?;
	
	let creds = Credentials { user, password, token };

	Ok(creds)
    }
}

/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct DocPaths {
    pub templates: String,
    pub webroot: String,
}

impl DocPaths {
    pub fn new<P: AsRef<Path>>(dir: P) -> Result<DocPaths, Box<dyn std::error::Error>> {
	let dir = dir.as_ref().display();
	let templates = format!("{}/blog/templates", dir); 
	let webroot = format!("{}/blog/webroot", dir); 
	let docpaths = DocPaths { templates, webroot };

	Ok(docpaths)
    }
}

/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct LogConfig {
    level: String
}

/// TODO Document
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct AppConfig {
    pub blog: Blog,
    pub creds: Credentials,
    pub logging: LogConfig,
    pub docpaths: DocPaths,
}

impl AppConfig {
    pub fn new<T>(config: T) -> Result<AppConfig, Box<dyn std::error::Error>>
    where T: AsRef<Path> {
	let config = std::fs::read_to_string(config)?;
	let app_config: AppConfig = toml::from_str(&config)?;
	Ok(app_config)
    }

    pub fn generate<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<AppConfig, Box<dyn std::error::Error>> {
	let docpaths = DocPaths::new(&dir)?;
	let blog = Blog::new_from_input(reader)?;
	let creds = Credentials::new_from_input(&dir, reader)?;
	let level = format!("INFO");
	let logging = LogConfig { level };
	
	let config = AppConfig {
	    blog,
	    creds,
	    logging,
	    docpaths,
	};

	config.create_paths()?;
	config.write(&dir)?;

	Ok(config)
    }

    fn create_paths(&self) -> Result<(), Box<dyn std::error::Error>> {
	create_dir_all(&self.docpaths.templates)?;
	create_dir_all(format!("{}/static/ext", &self.docpaths.webroot))?;
	create_dir_all(format!("{}/main/ext", &self.docpaths.webroot))?;
	create_dir_all(format!("{}/main/posts", &self.docpaths.webroot))?;

	for topic in &self.blog.topics {
	    let topic = common::slugify(&topic);

	    create_dir_all(format!("{}/{}/ext", &self.docpaths.webroot, &topic))?;
	    create_dir_all(format!("{}/{}/posts", &self.docpaths.webroot, &topic))?;
	}
	Ok(())
    }

    fn write<P: AsRef<Path>>(&self, dir: P) -> Result<(), Box<dyn std::error::Error>> {
	let config = toml::to_string_pretty(&self)?;
	let conf_path = &dir.as_ref().join("config.toml");
	common::str_to_ro_file(&config, &conf_path)?;
	Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile;

    #[test]
    fn build_run_config() {
	let arg_vec = vec!["caty-blog", "run", "./test_files/test-config.toml"];
	let matches = args().get_matches_from(arg_vec);
	let config = runner_config(matches);
	assert!(config.is_ok());
    }

    #[test]
    fn build_config_from_input() {
	let dir = tempfile::tempdir().unwrap();
	// Setup all target fields
	let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
	let config = AppConfig::generate(&dir, &mut src);
	assert!(config.is_ok());

	let tmp_dir = &dir.path();
	let config_path = &tmp_dir.join("config.toml");
	let admin = &tmp_dir.join("admin.pass");
	let token = &tmp_dir.join("admin.totp");
	let blog = &tmp_dir.join("blog");
	let templates = &tmp_dir.join("blog/templates");
	let webroot = &tmp_dir.join("blog/webroot");
	let static_ext = &tmp_dir.join("blog/webroot/static/ext");
	let main_ext = &tmp_dir.join("blog/webroot/main/ext");
	let main_posts = &tmp_dir.join("blog/webroot/main/posts");
	let one_ext = &tmp_dir.join("blog/webroot/one/ext");
	let one_posts = &tmp_dir.join("blog/webroot/one/posts");
	let two_ext = &tmp_dir.join("blog/webroot/two/ext");
	let two_posts = &tmp_dir.join("blog/webroot/two/posts");
	let three_ext = &tmp_dir.join("blog/webroot/three/ext");
	let three_posts = &tmp_dir.join("blog/webroot/three/posts");
	let and_more_ext = &tmp_dir.join("blog/webroot/and-more/ext");
	let and_more_posts = &tmp_dir.join("blog/webroot/and-more/posts");
	let core = vec![config_path, admin, token, blog, templates,
			webroot, static_ext, main_ext, main_posts,
			one_ext, one_posts, two_ext, two_posts,
			three_ext, three_posts, and_more_ext, and_more_posts];
	for p in core {
	    assert!(Path::new(p).exists())
	}
    }

    #[test]
    fn handle_csv_topics() {
	let reference_topics: Vec<String> = vec![format!("One"), format!("Two"), format!("Three"), format!("And More")];
	let topics = format!("One, Two, Three, And More");
	assert_eq!(reference_topics, csv_to_vec(&topics))
    }

}
