#![allow(dead_code)]
use std::net::SocketAddr;
use std::path::PathBuf;

use actix_files::NamedFile;
use actix_web::http::StatusCode;
use actix_web::middleware::ErrorHandlers;
use actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use clap::Parser;
use handlebars::Handlebars;

mod errors;

use errors::{generic_error, UserError};

#[derive(Parser, Debug, Clone)]
struct Config {
    #[clap(long, value_parser)]
    /// The directory from which to serve static content and
    /// handlebars templates
    template_directory: PathBuf,
    #[clap(long, default_value = "127.0.0.1:8000", value_parser)]
    /// The address on which to listen
    address: SocketAddr,
    #[clap(long, action)]
    /// Whether to start the server in dev mode; this enables some
    /// nice handlebars features that are not intended for production
    dev_mode: bool,
}

#[derive(Debug)]
struct SharedData<'a> {
    handlebars: Handlebars<'a>,
    config: Config,
}

#[get(r"/{filename:.*\.html}")]
async fn template(
    shared: web::Data<SharedData<'_>>,
    req: HttpRequest,
) -> actix_web::Result<impl Responder> {
    let path = req
        .match_info()
        .query("filename")
        .strip_suffix(".html")
        .expect("only paths with this suffix should get here");

    if shared.handlebars.has_template(path) {
        let body = shared
            .handlebars
            .render(path, &())
            .map_err(|_| UserError::InternalError)?;
        Ok(HttpResponse::Ok().body(body))
    } else {
        Err(UserError::NotFound)?
    }
}

#[get("/{filename:.*[^/]+}")]
async fn static_file(
    shared: web::Data<SharedData<'_>>,
    req: HttpRequest,
) -> actix_web::Result<impl Responder> {
    let requested = req.match_info().query("filename");

    match shared
        .config
        .template_directory
        .join(requested)
        .canonicalize()
    {
        // We only want to serve paths that are both valid *and* in
        // the template directory.
        //
        // i.e., don't serve up /etc/passwd
        Ok(path) if path.starts_with(&shared.config.template_directory) => {
            let file = NamedFile::open_async(path)
                .await
                .map_err(|_| UserError::NotFound)?;
            Ok(file.use_last_modified(false).respond_to(&req))
        }
        // Any other cases should 404
        _ => Err(UserError::NotFound)?,
    }
}

#[actix_web::main]
async fn main() -> Result<(), std::io::Error> {
    let mut config = Config::parse();
    config.template_directory = config.template_directory.canonicalize()?;

    env_logger::init();

    let mut handlebars = Handlebars::new();
    handlebars
        .register_templates_directory(".html", config.template_directory.clone())
        .expect("templates should compile correctly");
    handlebars.set_dev_mode(config.dev_mode);

    let shared_data = web::Data::new(SharedData {
        handlebars,
        config: config.clone(),
    });

    HttpServer::new(move || {
        App::new()
            .wrap(
                ErrorHandlers::new()
                    .handler(StatusCode::NOT_FOUND, generic_error)
                    .handler(StatusCode::INTERNAL_SERVER_ERROR, generic_error),
            )
            .app_data(shared_data.clone())
            .service(template)
            .service(static_file)
    })
    .bind(config.address)?
    .run()
    .await
}