//! Server-only features

use crate::models;
use models::HoldPosition;
use models::HoldRole;
use models::Problem;
use persistence::Persistent;
use state::PersistentState;
use state::State;
use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use tower_http::services::ServeDir;
use tracing::level_filters::LevelFilter;
use type_toppings::ResultExt;

pub mod cli {
    //! Server CLI interface

    use std::path::PathBuf;

    #[derive(clap::Parser)]
    #[command(version, about, long_about = None)]
    pub struct Cli {
        #[command(subcommand)]
        pub command: Command,
    }

    #[derive(clap::Subcommand, Default)]
    pub enum Command {
        #[default]
        Serve,

        /// Resets state, replacing it with defaults
        ResetState,

        ImportMiniMoonboardProblems {
            file_path: PathBuf,
        },
    }
}

pub mod state {
    //! Server state

    const STATE_VERSION: u64 = 1;

    use super::persistence::Persistent;
    use crate::models;
    use crate::models::Wall;
    use serde::Deserialize;
    use serde::Serialize;
    use smart_default::SmartDefault;
    use std::collections::BTreeSet;

    #[derive(Clone, Debug)]
    pub struct State {
        pub persistent: Persistent<PersistentState>,
    }

    #[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
    pub struct PersistentState {
        /// State schema version
        #[default(STATE_VERSION)]
        pub version: u64,

        pub wall: Wall,
        pub problems: Problems,
    }

    #[derive(Serialize, Deserialize, Clone, Debug, Default)]
    pub struct Problems {
        pub problems: BTreeSet<models::Problem>,
    }
}

pub mod persistence;

pub const STATE_FILE: &str = "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().await.unwrap_or_report(),
        Command::ImportMiniMoonboardProblems { file_path } => {
            let problems = import_mini_moonboard_problems(&file_path).await.unwrap_or_report();
            let state = load_state().await.unwrap_or_report();
            state
                .persistent
                .update(|s| {
                    s.problems.problems.extend(problems);
                })
                .await
                .unwrap_or_report();
        }
        Command::ResetState => {
            let s = PersistentState::default();
            let p = Path::new(STATE_FILE);
            tracing::info!("Resetting state to default: {}", p.display());
            Persistent::persist(p, &s).await.unwrap_or_report();
        }
    }
}

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

    // 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 conf = get_configuration(None).unwrap_or_report();
    let leptos_options = conf.leptos_options;
    let addr = leptos_options.site_addr;
    let routes = generate_route_list(App);

    let server_state = load_state().await?;

    let app = Router::new()
        .leptos_routes_with_context(&leptos_options, routes, move || provide_context(server_state.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);
    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)
}

#[tracing::instrument]
async fn load_state() -> Result<State, Error> {
    tracing::info!("Loading state");

    let p = PathBuf::from(STATE_FILE);
    let persistent = if p.try_exists()? {
        tracing::info!("No state found at {STATE_FILE}, creating default state");
        Persistent::<PersistentState>::load(&p).await?
    } else {
        Persistent::<PersistentState>::new(PersistentState::default(), p)
    };

    Ok(State { persistent })
}

#[tracing::instrument]
async fn import_mini_moonboard_problems(file_path: &Path) -> Result<Vec<Problem>, Error> {
    let mut problems = Vec::new();

    tracing::info!("Parsing mini moonboard problems from {}", file_path.display());
    let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path).await?;
    for problem in mini_moonboard.problems {
        let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
        for mv in problem.moves {
            let row = mv.description.row();
            let col = mv.description.column();
            let hold_position = HoldPosition { row, col };

            let role = match (mv.is_start, mv.is_end) {
                (true, true) => unreachable!(),
                (true, false) => HoldRole::Start,
                (false, true) => HoldRole::End,
                (false, false) => HoldRole::Normal,
            };
            holds.insert(hold_position, role);
        }
        let route = Problem { holds };
        problems.push(route);
    }
    Ok(problems)
}

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