use ::app::get_router;
use axum::body::Body;
use axum::http::Request;
use listenfd::ListenFd;
use notify::Watcher;
use std::path::Path;
use std::{env, io};
use thiserror::Error;
use tokio::net::TcpListener;
use tower_livereload::predicate::Predicate;
use tower_livereload::LiveReloadLayer;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Unable to bind to TCP listener")]
    TCPListener(#[from] std::io::Error),
    #[error("Error with the notify watcher")]
    NotifyWatcher(#[from] notify::Error),
    #[error("Missing environment variable {var}")]
    MissingEnvVar { var: &'static str },
}

/// Nous filtrons les requêtes de `htmx` pour ne pas inclure le script _JS_ qui gère le rechargement
/// Voir https://github.com/leotaku/tower-livereload/pull/3
#[derive(Copy, Clone)]
struct NotHtmxPredicate;
impl<T> Predicate<Request<T>> for NotHtmxPredicate {
    fn check(&mut self, req: &Request<T>) -> bool {
        !(req.headers().contains_key("hx-request"))
    }
}

const DEFAULT_LISTENER: &str = "localhost:3000";
async fn get_tcp_listener() -> Result<TcpListener, io::Error> {
    let mut listenfd = ListenFd::from_env();

    match listenfd.take_tcp_listener(0)? {
        // if we are given a tcp listener on listen fd 0, we use that one
        Some(listener) => {
            listener.set_nonblocking(true)?;
            Ok(TcpListener::from_std(listener)?)
        }
        // otherwise fall back to local listening
        None => Ok(TcpListener::bind(DEFAULT_LISTENER).await?),
    }
}

fn get_livereload_layer(
    templates_path: &Path,
) -> Result<LiveReloadLayer<NotHtmxPredicate>, notify::Error> {
    let livereload = LiveReloadLayer::new();
    let reloader = livereload.reloader();
    let mut watcher = notify::recommended_watcher(move |_| reloader.reload())?;
    watcher.watch(templates_path, notify::RecursiveMode::Recursive)?;
    Ok(livereload.request_predicate::<Body, NotHtmxPredicate>(NotHtmxPredicate))
}

#[tokio::main]
async fn main() -> Result<(), AppError> {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| AppError::MissingEnvVar {
        var: "CARGO_MANIFEST_DIR",
    })?;
    let assets_path = Path::new(&manifest_dir).join("assets");
    let templates_path = Path::new(&manifest_dir).join("templates");

    let livereload_layer =
        get_livereload_layer(&templates_path).map_err(AppError::NotifyWatcher)?;
    let router = get_router(assets_path.as_path()).layer(livereload_layer);

    let listener: TcpListener = get_tcp_listener().await.map_err(AppError::TCPListener)?;
    let local_addr = listener.local_addr().map_err(AppError::TCPListener)?;
    println!("Listening on: http://{}", local_addr);

    // Run the server with the router
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}