use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::models;
use crate::models::UserInteraction;
use derive_more::Display;
use derive_more::Error;
use derive_more::From;
use leptos::prelude::*;
use leptos::server;
use server_fn::ServerFnError;
use std::collections::BTreeMap;

#[server(
    input = Ron,
    output = Ron,
    custom = RonEncoded
)]
#[tracing::instrument(skip_all, err(Debug))]
pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError> {
    use crate::server::db::Database;
    use leptos::prelude::expect_context;
    use redb::ReadableTable;
    crate::tracing::on_enter!();

    let db = expect_context::<Database>();

    let walls = db
        .read(|txn| {
            let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?;
            let walls: Vec<models::Wall> = walls_table.iter()?.map(|r| r.map(|(_, v)| v.value())).collect::<Result<_, _>>()?;
            Ok(walls)
        })
        .await?;

    Ok(RonEncoded::new(walls))
}

#[server(
    input = Ron,
    output = Ron,
    custom = RonEncoded
)]
#[tracing::instrument(skip_all, err(Debug))]
pub(crate) async fn get_wall_by_uid(wall_uid: models::WallUid) -> Result<RonEncoded<models::Wall>, ServerFnError> {
    use crate::server::db::Database;
    use crate::server::db::DatabaseOperationError;
    use leptos::prelude::expect_context;
    crate::tracing::on_enter!();

    #[derive(Debug, derive_more::Error, derive_more::Display)]
    enum Error {
        #[display("Wall not found: {_0:?}")]
        NotFound(#[error(not(source))] models::WallUid),
    }

    let db = expect_context::<Database>();

    let wall = db
        .read(|txn| {
            let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?;
            let wall = walls_table
                .get(wall_uid)?
                .ok_or(Error::NotFound(wall_uid))
                .map_err(DatabaseOperationError::custom)?
                .value();
            Ok(wall)
        })
        .await?;

    Ok(RonEncoded::new(wall))
}

/// Returns user interaction for a single wall problem
#[server(
    input = Ron,
    output = Ron,
    custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_user_interaction(
    wall_uid: models::WallUid,
    problem: models::Problem,
) -> Result<RonEncoded<Option<models::UserInteraction>>, ServerFnError> {
    use crate::server::db::Database;
    use crate::server::db::DatabaseOperationError;
    use leptos::prelude::expect_context;
    crate::tracing::on_enter!();

    #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
    enum Error {
        #[display("Wall not found: {_0:?}")]
        WallNotFound(#[error(not(source))] models::WallUid),

        DatabaseOperation(DatabaseOperationError),
    }

    async fn inner(wall_uid: models::WallUid, problem: models::Problem) -> Result<Option<UserInteraction>, Error> {
        let db = expect_context::<Database>();

        let user_interaction = db
            .read(|txn| {
                let user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
                let user_interaction = user_table.get(&(wall_uid, problem))?.map(|guard| guard.value());
                Ok(user_interaction)
            })
            .await?;

        Ok(user_interaction)
    }

    let user_interaction = inner(wall_uid, problem)
        .await
        .map_err(error_reporter::Report::new)
        .map_err(ServerFnError::new)?;
    Ok(RonEncoded::new(user_interaction))
}

/// Returns all user interactions for a wall
#[server(
    input = Ron,
    output = Ron,
    custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_user_interactions_for_wall(
    wall_uid: models::WallUid,
) -> Result<RonEncoded<BTreeMap<models::Problem, models::UserInteraction>>, ServerFnError> {
    use crate::server::db::Database;
    use crate::server::db::DatabaseOperationError;
    use leptos::prelude::expect_context;
    use redb::ReadableTable;
    crate::tracing::on_enter!();

    #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
    enum Error {
        DatabaseOperation(DatabaseOperationError),
    }

    async fn inner(wall_uid: models::WallUid) -> Result<BTreeMap<models::Problem, models::UserInteraction>, Error> {
        let db = expect_context::<Database>();

        let user_interactions = db
            .read(|txn| {
                let user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
                let user_interactions = user_table
                    .iter()?
                    .filter(|guard| {
                        guard
                            .as_ref()
                            .map(|(key, _val)| {
                                let (wall_uid, _problem) = key.value();
                                wall_uid
                            })
                            .map(|wall_uid_| wall_uid_ == wall_uid)
                            .unwrap_or(false)
                    })
                    .map(|guard| {
                        guard.map(|(_key, val)| {
                            let val = val.value();
                            (val.problem.clone(), val)
                        })
                    })
                    .collect::<Result<_, _>>()?;
                Ok(user_interactions)
            })
            .await?;

        Ok(user_interactions)
    }

    let user_interaction = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?;
    Ok(RonEncoded::new(user_interaction))
}

/// Inserts or updates today's attempt.
#[server(
    input = Ron,
    output = Ron,
    custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn upsert_todays_attempt(
    wall_uid: models::WallUid,
    problem: models::Problem,
    attempt: Option<models::Attempt>,
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
    use crate::server::db::Database;
    use crate::server::db::DatabaseOperationError;
    use leptos::prelude::expect_context;

    crate::tracing::on_enter!();

    #[derive(Debug, Error, Display, From)]
    enum Error {
        #[display("Wall not found: {_0:?}")]
        WallNotFound(#[error(not(source))] models::WallUid),

        DatabaseOperation(DatabaseOperationError),
    }

    async fn inner(wall_uid: models::WallUid, problem: models::Problem, attempt: Option<models::Attempt>) -> Result<UserInteraction, Error> {
        let db = expect_context::<Database>();

        let user_interaction = db
            .write(|txn| {
                let mut user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;

                let key = (wall_uid, problem.clone());

                // Pop or default
                let mut user_interaction = user_table
                    .remove(&key)?
                    .map(|guard| guard.value())
                    .unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem));

                // If the last entry is from today, remove it
                if let Some(entry) = user_interaction.attempted_on.last_entry() {
                    let today_local_naive = chrono::Local::now().date_naive();

                    let entry_date = entry.key();
                    let entry_date_local_naive = entry_date.with_timezone(&chrono::Local).date_naive();

                    if entry_date_local_naive == today_local_naive {
                        entry.remove();
                    }
                }

                if let Some(attempt) = attempt {
                    user_interaction.attempted_on.insert(chrono::Utc::now(), attempt);
                }

                user_table.insert(key, user_interaction.clone())?;

                Ok(user_interaction)
            })
            .await?;

        Ok(user_interaction)
    }

    inner(wall_uid, problem, attempt)
        .await
        .map_err(error_reporter::Report::new)
        .map_err(ServerFnError::new)
        .map(RonEncoded::new)
}

/// Sets is_favorite field for a problem
#[server(
    input = Ron,
    output = Ron,
    custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn set_is_favorite(
    wall_uid: models::WallUid,
    problem: models::Problem,
    is_favorite: bool,
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
    use crate::server::db::Database;
    use crate::server::db::DatabaseOperationError;
    use leptos::prelude::expect_context;

    crate::tracing::on_enter!();

    #[derive(Debug, Error, Display, From)]
    enum Error {
        #[display("Wall not found: {_0:?}")]
        WallNotFound(#[error(not(source))] models::WallUid),

        DatabaseOperation(DatabaseOperationError),
    }

    async fn inner(wall_uid: models::WallUid, problem: models::Problem, is_favorite: bool) -> Result<UserInteraction, Error> {
        let db = expect_context::<Database>();

        let user_interaction = db
            .write(|txn| {
                let mut user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;

                let key = (wall_uid, problem.clone());

                // Pop or default
                let mut user_interaction = user_table
                    .remove(&key)?
                    .map(|guard| guard.value())
                    .unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem));

                user_interaction.is_favorite = is_favorite;

                user_table.insert(key, user_interaction.clone())?;

                Ok(user_interaction)
            })
            .await?;

        Ok(user_interaction)
    }

    inner(wall_uid, problem, is_favorite)
        .await
        .map_err(error_reporter::Report::new)
        .map_err(ServerFnError::new)
        .map(RonEncoded::new)
}
