#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
#[allow(unused_imports)]
#[macro_use(
    slog_o,
    slog_info,
    slog_warn,
    slog_error,
    slog_log,
    slog_record,
    slog_record_static,
    slog_b,
    slog_kv
)]
extern crate slog;
extern crate slog_async;

mod cli;
mod forms;
pub mod models;
mod queries;
pub mod schema;
mod views;

use std::{fmt::Display, path::PathBuf, str::FromStr};

use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{web, App, HttpResponse, HttpServer};

use qrcode::types::QrError;
use tera::Tera;

#[derive(Debug)]
pub enum ServerError {
    Argonautic,
    Diesel(diesel::result::Error),
    Migration(diesel_migrations::RunMigrationsError),
    Environment,
    Template(tera::Error),
    Qr(QrError),
    Io(std::io::Error),
    User(String),
}

impl std::fmt::Display for ServerError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Argonautic => write!(f, "Argonautica Error"),
            Self::Diesel(e) => write!(f, "Diesel Error: {}", e),
            Self::Environment => write!(f, "Environment Error"),
            Self::Template(e) => write!(f, "Template Error: {:?}", e),
            Self::Qr(e) => write!(f, "Qr Code Error: {:?}", e),
            Self::Io(e) => write!(f, "IO Error: {:?}", e),
            Self::Migration(e) => write!(f, "Migration Error: {:?}", e),
            Self::User(data) => write!(f, "{}", data),
        }
    }
}

impl actix_web::error::ResponseError for ServerError {
    fn error_response(&self) -> HttpResponse {
        match self {
            Self::Argonautic => HttpResponse::InternalServerError().json("Argonautica Error"),
            Self::Diesel(e) => {
                HttpResponse::InternalServerError().json(format!("Diesel Error: {:?}", e))
            }
            Self::Environment => HttpResponse::InternalServerError().json("Environment Error"),
            Self::Template(e) => {
                HttpResponse::InternalServerError().json(format!("Template Error: {:?}", e))
            }
            Self::Qr(e) => {
                HttpResponse::InternalServerError().json(format!("Qr Code Error: {:?}", e))
            }
            Self::Io(e) => HttpResponse::InternalServerError().json(format!("IO Error: {:?}", e)),
            Self::Migration(e) => {
                HttpResponse::InternalServerError().json(format!("Migration Error: {:?}", e))
            }
            Self::User(data) => HttpResponse::InternalServerError().json(data),
        }
    }
}

impl From<std::env::VarError> for ServerError {
    fn from(e: std::env::VarError) -> Self {
        eprintln!("Environment error {:?}", e);
        Self::Environment
    }
}

impl From<diesel_migrations::RunMigrationsError> for ServerError {
    fn from(e: diesel_migrations::RunMigrationsError) -> Self {
        Self::Migration(e)
    }
}

impl From<diesel::result::Error> for ServerError {
    fn from(err: diesel::result::Error) -> Self {
        eprintln!("Database error {:?}", err);
        Self::Diesel(err)
    }
}

impl From<argonautica::Error> for ServerError {
    fn from(e: argonautica::Error) -> Self {
        eprintln!("Authentication error {:?}", e);
        Self::Argonautic
    }
}
impl From<tera::Error> for ServerError {
    fn from(e: tera::Error) -> Self {
        eprintln!("Template error {:?}", e);
        Self::Template(e)
    }
}
impl From<QrError> for ServerError {
    fn from(e: QrError) -> Self {
        eprintln!("Template error {:?}", e);
        Self::Qr(e)
    }
}
impl From<std::io::Error> for ServerError {
    fn from(e: std::io::Error) -> Self {
        eprintln!("IO error {:?}", e);
        Self::Io(e)
    }
}

#[derive(Debug, Clone)]
enum Protocol {
    Http,
    Https,
}

impl Display for Protocol {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Http => f.write_str("http"),
            Self::Https => f.write_str("https"),
        }
    }
}

impl FromStr for Protocol {
    type Err = ServerError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "http" => Ok(Self::Http),
            "https" => Ok(Self::Https),
            _ => Err(ServerError::User("Failed to parse Protocol".to_owned())),
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct ServerConfig {
    secret: String,
    db: PathBuf,
    public_url: String,
    internal_ip: String,
    port: u32,
    protocol: Protocol,
    log: slog::Logger,
}

impl ServerConfig {
    pub fn to_env_strings(&self) -> Vec<String> {
        vec![
            format!("PSLINK_DATABASE=\"{}\"\n", self.db.display()),
            format!("PSLINK_PORT={}\n", self.port),
            format!("PSLINK_PUBLIC_URL=\"{}\"\n", self.public_url),
            format!("PSLINK_IP=\"{}\"\n", self.internal_ip),
            format!("PSLINK_PROTOCOL=\"{}\"\n", self.protocol),
            concat!(
                "# The SECRET_KEY variable is used for password encryption.\n",
                "# If it is changed all existing passwords are invalid.\n"
            )
            .to_owned(),
            format!("PSLINK_SECRET=\"{}\"\n", self.secret),
        ]
    }
}

include!(concat!(env!("OUT_DIR"), "/generated.rs"));
embed_migrations!("migrations/");

fn build_tera() -> Tera {
    let mut tera = Tera::default();

    tera.add_raw_templates(vec![
        ("admin.html", include_str!("../templates/admin.html")),
        ("base.html", include_str!("../templates/base.html")),
        (
            "edit_link.html",
            include_str!("../templates/edit_link.html"),
        ),
        (
            "edit_profile.html",
            include_str!("../templates/edit_profile.html"),
        ),
        (
            "index_users.html",
            include_str!("../templates/index_users.html"),
        ),
        ("index.html", include_str!("../templates/index.html")),
        ("login.html", include_str!("../templates/login.html")),
        (
            "not_found.html",
            include_str!("../templates/not_found.html"),
        ),
        ("signup.html", include_str!("../templates/signup.html")),
        (
            "submission.html",
            include_str!("../templates/submission.html"),
        ),
        (
            "view_link.html",
            include_str!("../templates/view_link.html"),
        ),
        (
            "view_profile.html",
            include_str!("../templates/view_profile.html"),
        ),
    ])
    .expect("failed to parse templates");
    tera
}

#[allow(clippy::future_not_send)]
async fn webservice(server_config: ServerConfig) -> std::io::Result<()> {
    let host_port = format!("{}:{}", &server_config.internal_ip, &server_config.port);

    slog_info!(
        server_config.log,
        "Running on: {}://{}/admin/login/",
        &server_config.protocol,
        host_port
    );
    slog_info!(
        server_config.log,
        "If the public url is set up correctly it should be accessible via: {}://{}/admin/login/",
        &server_config.protocol,
        &server_config.public_url
    );

    HttpServer::new(move || {
        let tera = build_tera(); //Tera::new("templates/**/*").expect("failed to initialize the templates");
        let generated = generate();
        App::new()
            .data(server_config.clone())
            .wrap(actix_slog::StructuredLogger::new(
                server_config.log.new(slog_o!("log_type" => "access")),
            ))
            .wrap(IdentityService::new(
                CookieIdentityPolicy::new(&[0; 32])
                    .name("auth-cookie")
                    .secure(false),
            ))
            .data(tera)
            .service(actix_web_static_files::ResourceFiles::new(
                "/static", generated,
            ))
            // directly go to the main page of Freie-Hochschule-Stuttgart
            .route("/", web::get().to(views::redirect_fhs))
            // admin block
            .service(
                web::scope("/admin")
                    // list all links
                    .route("/index/", web::get().to(views::index))
                    // invite users
                    .route("/signup/", web::get().to(views::signup))
                    .route("/signup/", web::post().to(views::process_signup))
                    // logout
                    .route("/logout/", web::to(views::logout))
                    // submit a new url for shortening
                    .route("/submit/", web::get().to(views::create_link))
                    .route("/submit/", web::post().to(views::process_link_creation))
                    // view an existing url
                    .service(
                        web::scope("/view")
                            .service(
                                web::scope("/link")
                                    .route("/{redirect_id}", web::get().to(views::view_link))
                                    .route("/", web::get().to(views::view_link_fhs)),
                            )
                            .service(
                                web::scope("/profile")
                                    .route("/{user_id}", web::get().to(views::view_profile)),
                            )
                            .route("/users/", web::get().to(views::index_users)),
                    )
                    .service(
                        web::scope("/edit")
                            .service(
                                web::scope("/link")
                                    .route("/{redirect_id}", web::get().to(views::edit_link))
                                    .route(
                                        "/{redirect_id}",
                                        web::post().to(views::process_link_edit),
                                    ),
                            )
                            .service(
                                web::scope("/profile")
                                    .route("/{user_id}", web::get().to(views::edit_profile))
                                    .route(
                                        "/{user_id}",
                                        web::post().to(views::process_edit_profile),
                                    ),
                            )
                            .route("/set_admin/{user_id}", web::get().to(views::toggle_admin)),
                    )
                    .service(
                        web::scope("/delete").service(
                            web::scope("/link")
                                .route("/{redirect_id}", web::get().to(views::process_link_delete)),
                        ),
                    )
                    .service(
                        web::scope("/download")
                            .route("/png/{redirect_id}", web::get().to(views::download_png)),
                    )
                    // login to the admin area
                    .route("/login/", web::get().to(views::login))
                    .route("/login/", web::post().to(views::process_login)),
            )
            // redirect to the url hidden behind the code
            .route("/{redirect_id}", web::get().to(views::redirect))
    })
    .bind(host_port)?
    .run()
    .await
}

#[actix_web::main]
async fn main() -> Result<(), std::io::Error> {
    match cli::setup() {
        Ok(Some(server_config)) => webservice(server_config).await,
        Ok(None) => {
            std::thread::sleep(std::time::Duration::from_millis(100));
            std::process::exit(0);
        }
        Err(e) => {
            eprintln!("\nError: {}", e);
            std::thread::sleep(std::time::Duration::from_millis(100));
            std::process::exit(1);
        }
    }
}