/*
 * This is the main Synchronik entrypoint for the server
 */

#[macro_use]
extern crate serde_json;

use std::path::PathBuf;

use async_std::sync::{Arc, RwLock};
use dotenv::dotenv;
use gumdrop::Options;
use handlebars::Handlebars;
use log::*;
use sqlx::SqlitePool;
use url::Url;

mod config;
mod models;
mod routes;

use crate::config::*;
use crate::models::Project;

#[derive(Clone, Debug)]
pub struct AppState<'a> {
    pub db: SqlitePool,
    pub config: ServerConfig,
    pub agents: Vec<Agent>,
    hb: Arc<RwLock<Handlebars<'a>>>,
}

impl AppState<'_> {
    fn new(db: SqlitePool, config: ServerConfig) -> Self {
        let mut hb = Handlebars::new();

        #[cfg(debug_assertions)]
        hb.set_dev_mode(true);

        Self {
            db,
            config,
            agents: vec![],
            hb: Arc::new(RwLock::new(hb)),
        }
    }

    pub async fn register_templates(&self) -> Result<(), handlebars::TemplateError> {
        let mut hb = self.hb.write().await;
        hb.clear_templates();
        hb.register_templates_directory(".hbs", "views")
    }

    pub async fn render(
        &self,
        name: &str,
        data: &serde_json::Value,
    ) -> Result<tide::Body, tide::Error> {
        let hb = self.hb.read().await;
        let view = hb.render(name, data)?;
        Ok(tide::Body::from_string(view))
    }
}

#[derive(Debug, Options)]
struct ServerOptions {
    #[options(help = "print help message")]
    help: bool,
    #[options(help = "host:port to bind the server to", default = "0.0.0.0:8000")]
    listen: String,
    #[options(help = "Path to the configuration file")]
    config: Option<PathBuf>,
    #[options(help = "Comma separated list of URLs for agents")]
    agents: Vec<Url>,
}

#[async_std::main]
async fn main() -> Result<(), tide::Error> {
    pretty_env_logger::init();
    dotenv().ok();
    let opts = ServerOptions::parse_args_default_or_exit();
    debug!("Starting with options: {:?}", opts);

    let config = match opts.config {
        Some(path) => {
            let config_file = std::fs::File::open(path).expect("Failed to open config file");
            serde_yaml::from_reader(config_file).expect("Failed to read config file")
        }
        None => ServerConfig::default(),
    };
    debug!("Starting with config: {:?}", config);

    let database_url = std::env::var("DATABASE_URL").unwrap_or(":memory:".to_string());
    let pool = SqlitePool::connect(&database_url).await?;

    /* If synchronik-server is running in memory, make sure the database is set up properly */
    if database_url == ":memory:" {
        sqlx::migrate!().run(&pool).await?;
    }
    let mut state = AppState::new(pool.clone(), config.clone());

    /*
     * Make sure the database has all the projects configured
     */

    for name in config.projects.keys() {
        match Project::by_name(name, &pool).await {
            Ok(_) => {}
            Err(sqlx::Error::RowNotFound) => {
                debug!("Project not found in database, creating: {}", name);
                Project::create(&Project::new(name), &pool).await?;
            }
            Err(e) => {
                return Err(e.into());
            }
        }
    }

    for (name, agent) in config.agents.iter() {
        debug!("Requesting capabilities from agent: {:?}", agent);
        let response: synchronik::CapsResponse =
            reqwest::get(agent.url.join("/api/v1/capabilities")?)
                .await?
                .json()
                .await?;
        state.agents.push(Agent::new(
            name.to_string(),
            agent.url.clone(),
            response.caps,
        ));
    }

    state
        .register_templates()
        .await
        .expect("Failed to register handlebars templates");
    let mut app = tide::with_state(state);

    #[cfg(not(debug_assertions))]
    {
        info!("Activating RELEASE mode configuration");
        app.with(driftwood::ApacheCombinedLogger);
    }

    #[cfg(debug_assertions)]
    {
        info!("Activating DEBUG mode configuration");
        info!("Enabling a very liberal CORS policy for debug purposes");
        use tide::security::{CorsMiddleware, Origin};
        let cors = CorsMiddleware::new()
            .allow_methods(
                "GET, POST, PUT, OPTIONS"
                    .parse::<tide::http::headers::HeaderValue>()
                    .unwrap(),
            )
            .allow_origin(Origin::from("*"))
            .allow_credentials(false);

        app.with(cors);
    }
    /*
     * All builds will have apidocs, since they're handy
     */
    app.at("/apidocs").serve_dir("apidocs/")?;
    app.at("/static").serve_dir("static/")?;

    debug!("Configuring routes");
    app.at("/").get(routes::index);
    app.at("/project/:name").get(routes::project);

    debug!("Configuring API routes");
    app.at("/api/v1/projects/:name")
        .post(routes::api::execute_project);
    app.listen(opts.listen).await?;
    Ok(())
}

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

    #[test]
    fn agent_can_meet_false() {
        let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
        let capabilities = vec![Capability::with_name("rustc")];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert_eq!(false, agent.can_meet(&needs));
    }

    #[test]
    fn agent_can_meet_true() {
        let needs: Vec<String> = vec!["dotnet".into()];
        let capabilities = vec![Capability::with_name("dotnet")];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert!(agent.can_meet(&needs));
    }

    #[test]
    fn agent_can_meet_false_multiple() {
        let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
        let capabilities = vec![Capability::with_name("dotnet")];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert_eq!(false, agent.can_meet(&needs));
    }

    #[test]
    fn agent_can_meet_true_multiple() {
        let needs: Vec<String> = vec!["rspec".into(), "dotnet".into()];
        let capabilities = vec![
            Capability::with_name("dotnet"),
            Capability::with_name("rspec"),
        ];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert!(agent.can_meet(&needs));
    }
}
