
//! Run 
//! ```not_rust
//! cargo run -p example-static-file-server
//! ```

// use axum_auth::AuthBasic;

#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(html_logo_url = "../images/docserver.svg")]
#[doc = include_str!("../README.md")]
// #![doc(html_no_source)]

// TODO tasks https://docs.rs/async-sqlx-session/latest/async_sqlx_session/struct.SqliteSessionStore.html

use rand_core::{SeedableRng,OsRng, RngCore};
use rand_chacha::ChaCha8Rng;

use axum::{
    extract::Host,
    handler::HandlerWithoutStateExt,
    routing::MethodRouter,
    http::{
        StatusCode,
        Uri,
        header::HeaderValue,
        Method,
    },
    BoxError,
    Extension,
    response::Redirect,
//    http::Request, handler::HandlerWithoutStateExt, http::StatusCode, routing::get, Router,    
    Router,
};
use tower::ServiceBuilder;
use axum_server::tls_rustls::RustlsConfig;
use std::{
    net::SocketAddr, 
    path::PathBuf,
    sync::{Arc, Mutex},
    path::Path, 
};
// use std::net::SocketAddr;
// use tower::ServiceExt;
use tower_http::{
    services::{ServeDir,ServeFile},
    trace::TraceLayer,
    cors::CorsLayer,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use once_cell::sync::Lazy;
// use once_cell::sync::{Lazy,OnceCell};
// use async_session::{Session, SessionStore, MemoryStore};

use tera::Context;
use async_sqlx_session::SqliteSessionStore;
use sqlx::AnyPool;

mod tera_tpls;
mod defs;
mod users;
mod login_password;
mod handlers;
mod tools;

use defs::{
    AppDBs,
    SessionStoreDB,
    FileStore,
    Config,
    load_from_file,
    parse_args,
    AppConnectInfo,
};
use users::UserStore;
use defs::collect_openid_clients;

use tera_tpls::init_tera;
use tower_cookies::CookieManagerLayer;
use handlers::{
    handle_404,
    rewrite_request_uri,
    admin_router_handlers,
    users_router_handlers,
    pages_router_handlers,
};
use crate::tools::get_socket_addr;

pub const USER_AGENT: &str = "user-agent";
pub const SESSION_COOKIE_NAME: &str = "doc_session";
pub const CFG_FILE_EXTENSION: &str  = ".toml";
pub const FILE_SCHEME: &str = "file:///"; 

pub const PKG_NAME: &str = env!("CARGO_PKG_NAME");
// static WEBSERVER: AtomicUsize = AtomicUsize::new(0);
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
// const PKG_VERSION: Option&<&'static str> = option_env!("CARGO_PKG_VERSION");
// const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
// const COOKIE_NAME: &str = "lc_authz";
// const COOKIE_SEP: &str = ":tk:";
const GIT_VERSION: &str = ""; //git_version::git_version!();
static GIT_VERSION_NAME: Lazy<String> = Lazy::new(|| {
    format!("v{} [build: {}]",PKG_VERSION,GIT_VERSION)
});
static PKG_FULLNAME: Lazy<String> = Lazy::new(|| {
    format!("{}: TII CL Rust",PKG_NAME)
});

pub const USERS_TABLENAME: &str = "users";
pub const OPENID_USERS_TABLENAME: &str = "openid_users";
pub const USERS_FILESTORE: &str = "users";
pub const OPENID_USERS_FILESTORE: &str = "openid_users";
pub const DEFAULT_ROLES: &str = "user";


#[derive(Clone, Copy)]
struct Ports {
    http: u16,
    https: u16,
}

pub fn route(path: &str, method_router: MethodRouter) -> Router {
    Router::new().route(path, method_router)
}

#[tokio::main]
async fn main() {
    let config_path = parse_args();
    if config_path.is_empty() {
        eprintln!("No config-file found");
        std::process::exit(2)
    }
    //	pretty_env_logger::init();
    let config = {
        let mut server_cfg: Config = load_from_file(&config_path, "server-config").unwrap_or_else(|e|{
            eprintln!("Settings error: {}",e);
            std::process::exit(2)
        });
        server_cfg.load_items();
        //config.fix_root_path::<ServerConfig>(String::new());
        server_cfg
    };
    if config.verbose > 1 { dbg!("{:?}",&config); }
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "example_static_file_server=debug,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    // you can convert handler function to service

    // let store = FileStore {
    //     sess_path: "store".to_owned(),
    //     ses_file: "data".to_owned(),
    //     // sess_path: sessions_config.session_store_uri.replace(FILE_SCHEME,"").to_owned(),
    //     // ses_file: sessions_config.session_store_file.to_owned(),
    // };
    // let session_store = SessionStoreDB::connect_file_store(store);
    // let session_store = SessionStoreDB::connect_memory_store();
    let session_store = if config.session_store_uri.starts_with(FILE_SCHEME) {
        let store = FileStore {
            sess_path: config.session_store_uri.replace(FILE_SCHEME,"").to_owned(),
            ses_file: config.session_store_file.to_owned(),
        };
        if let Err(e) = store.check_paths() {
            eprintln!("Error creation File Store: {}",e);
            std::process::exit(2)
        }
        SessionStoreDB::connect_file_store(store)
    } else if config.session_store_uri.starts_with("sqlite:") {
        let store = SqliteSessionStore::new(&config.session_store_uri).await.unwrap_or_else(|e|{
            eprintln!("Error session database {}: {}",
                config.session_store_uri,e
            );
            std::process::exit(2)
        });
        let _ = store.migrate().await;
        let _ = store.cleanup().await;
        SessionStoreDB::connect_sqlite_store(store)
    } else if config.session_store_uri.starts_with("memory") {
        SessionStoreDB::connect_memory_store()
    } else {
        SessionStoreDB::None
    }; 
    let user_store = if config.users_store_uri.starts_with(FILE_SCHEME) {
        let users_store_uri = config.users_store_uri.replace(FILE_SCHEME,"").to_owned();
        if ! Path::new(&users_store_uri).exists() {
            if let Err(e) = std::fs::File::create(Path::new(&users_store_uri)) {
                eprintln!("Error creation Users store {}: {}",
                    &users_store_uri,e);
                std::process::exit(2)
            }
        }
        UserStore::File(users_store_uri)
    // } else if config.users_store_uri.starts_with("sqlite:") {
    } else if config.users_store_uri.contains("sql") {
        //let m = Migrator::new(Path::new("./migrations")).await?;
        //}
        let pool = AnyPool::connect(&config.users_store_uri).await.unwrap_or_else(|e|{
            eprintln!("Error pool database {}: {}",
                config.users_store_uri,e
            );
            std::process::exit(2)
        });
        // let pool = SqlitePool::connect(&config.users_store_uri).await.unwrap_or_else(|e|{
        //     eprintln!("Error pool database {}: {}",
        //         config.users_store_uri,e
        //     );
        //     std::process::exit(2)
        // });
        UserStore::Sql(pool)
    } else {
        eprintln!("User store {}: Not defined",&config.users_store_uri);
        std::process::exit(2)
    };
    #[cfg(feature = "casbin")]
    let enforcer = if !config.authz_model_path.is_empty() && ! config.authz_policy_path.is_empty() {
        AppDBs::create_enforcer(
        Box::leak(config.authz_model_path.to_owned().into_boxed_str()),
        Box::leak(config.authz_policy_path.to_owned().into_boxed_str())
        ).await
    } else {
        eprintln!("Error auth enforcer {} + {}",
            config.authz_model_path, config.authz_policy_path
        );
        std::process::exit(2);
    };

    let ports = Ports {
        http: 7878,
        https: 8800,
    };
    // optional: spawn a second server to redirect http requests to this server
    if config.protocol.contains("http") {
        tokio::spawn(redirect_http_to_https(ports));
    }

    let mut origins: Vec<HeaderValue> = Vec::new();
    for itm in config.allow_origin.to_owned() {
        match HeaderValue::from_str(itm.as_str()) {
            Ok(val) => origins.push(val),
            Err(e) =>  println!("error {} with {} header for allow_origin",e,itm),
        }
    }	

    let mut context = Context::new();
    context.insert("server_name","DOC Server");
    context.insert("pkg_name",&PKG_NAME);
    context.insert("pkg_version",&PKG_VERSION);
    context.insert("git_version",&GIT_VERSION);
    context.insert("git_version_name",GIT_VERSION_NAME.as_str());
    context.insert("pkg_fullname",PKG_FULLNAME.as_str());

    // let app = Router::new().route("/", get(handler));
    #[cfg(feature = "authstore")]
    let app_dbs = Arc::new(
        AppDBs::new(&config, session_store, user_store,
            init_tera(&config.templates_path), context
        )
    );
    #[cfg(feature = "casbin")]
    let app_dbs = Arc::new(
        AppDBs::new(&config, session_store, user_store, enforcer,
            init_tera(&config.templates_path), context
        )
    );
    let openid_clients = Arc::new(
        collect_openid_clients(config.to_owned(), "https://tiicl.tls13.io:8800/from_openid?response_type=code").await
    );
    let middleware =
        axum::middleware::from_fn_with_state(app_dbs.clone(),rewrite_request_uri);
    // apply the layer around the whole `Router`
    // this way the middleware will run before `Router` receives the request

    let mut web_router = Router::new();

    // Parse serv_paths to add static paths as service 
    for item in &config.serv_paths {
        // Try to check src_path ...
        let src_path: String;
        if Path::new(&item.src_path).exists() {
            src_path = format!("{}",item.src_path); 
        } else {
            src_path = if item.src_path.starts_with("/") {
                format!("{}",item.src_path)
            } else {
                format!("{}/{}",&config.root_path,item.src_path)
            };
            if ! Path::new(&src_path).exists() {
                eprintln!("File path {}: not found", &src_path);
                continue;
            } 
        }    
        // Add ServeDir with not_found page ...
        if item.not_found.is_empty() {
            web_router = web_router.nest_service(
                &item.url_path,
                ServeDir::new(&src_path).not_found_service(handle_404.into_service())
            );
            println!("Added path {} => {}", &src_path,&item.url_path);
        } else {
            web_router = web_router.nest_service(
                &item.url_path,
                ServeDir::new(&src_path).not_found_service(ServeFile::new(&item.not_found))
            );
            println!("Added path {} => {} ({})", &src_path,&item.url_path,&item.not_found);
        }
    }
    let mut key = [0u8; 16];
    let mut os_rng = OsRng{};
    os_rng.fill_bytes(&mut key);
    let random = ChaCha8Rng::seed_from_u64(OsRng.next_u64());
    
    web_router = web_router
        .merge(users_router_handlers())
        .merge(admin_router_handlers())
        .merge(pages_router_handlers())
        .layer(ServiceBuilder::new().layer(middleware))
        .layer(CookieManagerLayer::new())
        .layer(Extension(app_dbs))
        .layer(Extension(openid_clients))
        .layer(Extension(Arc::new(Mutex::new(random))))
        .fallback_service(handle_404.into_service())
        ;

    // if !config.html_path.is_empty() && !config.html_url.is_empty() {
    //     MAIN_URL.set(config.server.html_url.to_owned()).unwrap_or_default();
    //     println!("SpaRoutert local path {} to {}",&config.server.html_path,&config.server.html_url);
    //     web_router = web_router.merge(
    //     )
    //     .fallback(fallback);
    // }

    if config.verbose > 2 { dbg!("{:?}",&origins); }
    if config.allow_origin.len() > 0 {
         web_router = web_router.layer(CorsLayer::new()
            .allow_origin(origins)
            .allow_methods(vec![Method::GET, Method::POST])
            .allow_headers(tower_http::cors::Any)
        );
    }
    let addr = get_socket_addr(&config.bind,config.port);
    tracing::debug!("listening on {} url: {}://{}", addr, config.protocol, config.hostport);
    println!("listening on {} url: {}://{}", addr, config.protocol, config.hostport);
    if config.protocol.as_str() == "http" {
    // let app_with_middleware = middleware.layer(app);
    // run https server
        //let addr = SocketAddr::from(([127, 0, 0, 1], ports.http));
        //tracing::debug!("listening on {}", addr);
        // .into_make_service_with_connect_info::<SocketAddr>()
            //.into_make_service_with_connect_info::<AppConnectInfo>()
        if config.verbose > 0 {
            web_router = web_router.layer(TraceLayer::new_for_http());
        }
        let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
        //    web_router.into_make_service_with_connect_info::<SocketAddr>()
        axum::serve(listener, web_router
               .into_make_service_with_connect_info::<AppConnectInfo>()
           )
          .await
          .unwrap();
    } else {
        // configure certificate and private key used by https
        // let tls_config = RustlsConfig::from_pem_file(
        //     PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        //         .join("self_signed_certs")
        //         .join("cert.pem"),
        //     PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        //         .join("self_signed_certs")
        //         .join("key.pem"),
        // )
        let tls_config = RustlsConfig::from_pem_file(
            PathBuf::from(&config.cert_file),
            PathBuf::from(&config.key_file)
        )
        .await
        .unwrap_or_else(|e|{
            eprintln!("Error TLS config: {}",e);
            std::process::exit(2)
        });
        //let addr = SocketAddr::from(([127, 0, 0, 1], ports.https));
        //tracing::debug!("listening on {}", addr);
        //let app_with_middleware = middleware.layer(app);
        // apply the layer around the whole `Router`middleware.layer(app);
//           .serve(web_router.into_make_service())
        if config.verbose > 0 {
            web_router = web_router.layer(TraceLayer::new_for_http());
        }
        axum_server::bind_rustls(addr, tls_config)
          .serve(
            // web_router.layer(TraceLayer::new_for_http())
            web_router
            .into_make_service_with_connect_info::<AppConnectInfo>()
        )
        .await
        .unwrap();
    }
/*
    // let port = 3002;

    // tokio::join!(
    //     serve(using_serve_dir(), 3001),
    //     serve(using_serve_dir_with_assets_fallback(), 3002),
    //     serve(using_serve_dir_only_from_root_via_fallback(), 3003),
    //     serve(using_serve_dir_with_handler_as_service(), 3004),
    //     serve(two_serve_dirs(), 3005),
    //     serve(calling_serve_dir_from_a_handler(), 3006),
    // );

    //let addr = SocketAddr::from(([127, 0, 0, 1], port));
    let addr = SocketAddr::from(([192,168,1,4], port));
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    tracing::debug!("listening on {}", listener.local_addr().unwrap());

        // let _ = axum::Server::bind(&addr)
        //     .serve(
        //         // router.layer(TraceLayer::new_for_http())
        //         //router.into_make_service_with_connect_info::<SocketAddr>()
        //         router.into_make_service()
        //     )
        //     .await
        //     .expect("server failed");
 
    // axum_server::bind(addr)
    //     .serve(router.into_make_service())
    //     .await
    //     .unwrap();
     axum::serve(listener, router.layer(TraceLayer::new_for_http()))
         .await
        .unwrap();
    // let _ = axum::Server::bind(&addr)
    //     .serve(
    //             app.layer(TraceLayer::new_for_http()).into_make_service()
    //            //    web_router.into_make_service_with_connect_info::<SocketAddr>()
    //             // web_router.into_make_service()
    //     )
    //     .await
    //     .unwrap();
*/
}

async fn redirect_http_to_https(ports: Ports) {
    fn make_https(host: String, uri: Uri, ports: Ports) -> Result<Uri, BoxError> {
        let mut parts = uri.into_parts();

        parts.scheme = Some(axum::http::uri::Scheme::HTTPS);

        if parts.path_and_query.is_none() {
            parts.path_and_query = Some("/".parse().unwrap());
        }

        let https_host = host.replace(&ports.http.to_string(), &ports.https.to_string());
        parts.authority = Some(https_host.parse()?);

        Ok(Uri::from_parts(parts)?)
    }

    let redirect = move |Host(host): Host, uri: Uri| async move {
        match make_https(host, uri, ports) {
            Ok(uri) => Ok(Redirect::permanent(&uri.to_string())),
            Err(error) => {
                tracing::warn!(%error, "failed to convert URI to HTTPS");
                Err(StatusCode::BAD_REQUEST)
            }
        }
    };

    let addr = SocketAddr::from(([127, 0, 0, 1], ports.http));
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    tracing::debug!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, redirect.into_make_service_with_connect_info::<SocketAddr>())
        .await
        .unwrap();
}

/*
fn using_serve_dir() -> Router {
    // serve the file in the "assets" directory under `/assets`
    Router::new().nest_service("/assets", ServeDir::new("assets"))
}

fn using_serve_dir_with_assets_fallback() -> Router {
    // `ServeDir` allows setting a fallback if an asset is not found
    // so with this `GET /assets/doesnt-exist.jpg` will return `index.html`
    // rather than a 404
    let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html"));

    Router::new()
        .route("/foo", get(|| async { "Hi from /foo" }))
        .nest_service("/assets", serve_dir.clone())
        .fallback_service(serve_dir)
}

fn using_serve_dir_only_from_root_via_fallback() -> Router {
    // you can also serve the assets directly from the root (not nested under `/assets`)
    // by only setting a `ServeDir` as the fallback
    let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html"));

    Router::new()
        .route("/foo", get(|| async { "Hi from /foo" }))
        .fallback_service(serve_dir)
}

fn using_serve_dir_with_handler_as_service() -> Router {
    async fn handle_404() -> (StatusCode, &'static str) {
        (StatusCode::NOT_FOUND, "Not found")
    }

    // you can convert handler function to service
    let service = handle_404.into_service();

    let serve_dir = ServeDir::new("assets").not_found_service(service);

    Router::new()
        .route("/foo", get(|| async { "Hi from /foo" }))
        .fallback_service(serve_dir)
}

fn two_serve_dirs() -> Router {
    // you can also have two `ServeDir`s nested at different paths
    let serve_dir_from_assets = ServeDir::new("assets");
    let serve_dir_from_dist = ServeDir::new("dist");

    Router::new()
        .nest_service("/assets", serve_dir_from_assets)
        .nest_service("/dist", serve_dir_from_dist)
}

#[allow(clippy::let_and_return)]
fn calling_serve_dir_from_a_handler() -> Router {
    // via `tower::Service::call`, or more conveniently `tower::ServiceExt::oneshot` you can
    // call `ServeDir` yourself from a handler
    Router::new().nest_service(
        "/foo",
        get(|request: Request<_>| async {
            let service = ServeDir::new("assets");
            let result = service.oneshot(request).await;
            result
        }),
    )
}

async fn serve(app: Router, port: u16) {
    let addr = SocketAddr::from(([127, 0, 0, 1], port));
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    tracing::debug!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app.layer(TraceLayer::new_for_http()))
        .await
        .unwrap();
    // let _ = axum::Server::bind(&addr)
    //     .serve(
    //             app.layer(TraceLayer::new_for_http()).into_make_service()
    //            //    web_router.into_make_service_with_connect_info::<SocketAddr>()
    //             // web_router.into_make_service()
    //     )
    //     .await
    //     .unwrap();
}
*/

