//! Server-only features

use cli::Cli;
use config::Config;
use confik::Configuration;
use confik::EnvSource;
use state::PersistentState;
use std::path::Path;
use std::sync::Arc;
use tower_http::services::ServeDir;
use tracing::level_filters::LevelFilter;
use type_toppings::ResultExt;

mod cli;
pub mod config;
mod db;
mod migrations;
pub mod operations;
pub mod state;

pub const STATE_FILE: &str = "datastore/private/state.ron";

#[tracing::instrument]
pub async fn main() {
    use crate::server::cli::Cli;
    use crate::server::cli::Command;
    use clap::Parser as _;
    use tracing_subscriber::EnvFilter;

    tracing_subscriber::fmt()
        .without_time()
        .with_file(true)
        .with_line_number(true)
        .with_target(false)
        .with_ansi(true)
        .with_env_filter(EnvFilter::builder().with_default_directive(LevelFilter::DEBUG.into()).from_env_lossy())
        .pretty()
        .init();

    let cli = Cli::parse();

    match cli.command {
        Command::Serve => serve(cli).await.unwrap_or_report(),
    }
}

#[tracing::instrument(skip(cli), err)]
async fn serve(cli: Cli) -> Result<(), Error> {
    use crate::app::App;
    use crate::app::shell;
    use axum::Router;
    use leptos_axum::LeptosRoutes;
    use leptos_axum::generate_route_list;

    tracing::debug!("Creating DB");
    let db = Arc::new(db::create()?);

    migrations::run_migrations(&db).await.map_err(Error::Migration)?;

    // Setting get_configuration(None) means we'll be using cargo-leptos's env values
    // For deployment these variables are:
    // <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
    // Alternately a file can be specified such as Some("Cargo.toml")
    // The file would need to be included with the executable when moved to deployment
    let leptos_conf_file = leptos::config::get_configuration(None).unwrap_or_report();
    let leptos_options = leptos_conf_file.leptos_options;
    let addr = leptos_options.site_addr;
    let routes = generate_route_list(App);

    let config = load_config(cli)?;

    tracing::debug!("Creating app router");
    let app = Router::new()
        .leptos_routes_with_context(
            &leptos_options,
            routes,
            move || {
                leptos::prelude::provide_context(Arc::clone(&db));
                leptos::prelude::provide_context(config.clone())
            },
            {
                let leptos_options = leptos_options.clone();
                move || shell(leptos_options.clone())
            },
        )
        .nest_service("/files", file_service("datastore/public"))
        .fallback(leptos_axum::file_and_error_handler(shell))
        .with_state(leptos_options);

    tracing::debug!("Binding TCP listener");
    let listener = tokio::net::TcpListener::bind(&addr).await?;

    tracing::info!("Listening on http://{addr}");
    axum::serve(listener, app.into_make_service()).await?;

    Ok(())
}

#[tracing::instrument(skip(path))]
fn file_service(path: impl AsRef<Path>) -> ServeDir {
    tracing::debug!("Creating file service for {}", path.as_ref().display());
    ServeDir::new(path)
}

fn load_config(cli: Cli) -> Result<Config, Error> {
    let mut builder = config::Config::builder();
    if cli
        .config
        .try_exists()
        .expect_or_report_with(|| format!("Failed to look up config file at {}", cli.config))
    {
        builder.override_with(confik::FileSource::new(cli.config));
    }
    let config = builder.override_with(EnvSource::new().allow_secrets()).try_build()?;
    Ok(config)
}

#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
#[display("Server crash: {_variant}")]
pub enum Error {
    Io(std::io::Error),

    Parser(moonboard_parser::Error),

    #[display("Failed migration")]
    #[from(ignore)]
    Migration(Box<dyn std::error::Error>),

    Confik(confik::Error),

    Database(redb::DatabaseError),
}
