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;

#[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;
    tracing::trace!("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;
    tracing::trace!("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))
}

#[server(
    input = Ron,
    output = Ron,
    custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result<RonEncoded<Vec<models::Problem>>, ServerFnError> {
    use crate::server::db::Database;
    use crate::server::db::DatabaseOperationError;
    use leptos::prelude::expect_context;
    tracing::trace!("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) -> Result<Vec<models::Problem>, Error> {
        let db = expect_context::<Database>();

        let problems = db
            .read(|txn| {
                let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?;
                tracing::debug!("getting wall");
                let wall = walls_table
                    .get(wall_uid)?
                    .ok_or(Error::WallNotFound(wall_uid))
                    .map_err(DatabaseOperationError::custom)?
                    .value();
                tracing::debug!("got wall");
                drop(walls_table);

                tracing::debug!("open problems table");
                let problems_table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
                tracing::debug!("opened problems table");

                let mut problems = Vec::new();
                for &problem_uid in &wall.problems {
                    if let Some(problem) = problems_table.get((wall_uid, problem_uid))? {
                        problems.push(problem.value());
                    }
                }
                Ok(problems)
            })
            .await?;

        Ok(problems)
    }

    let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?;

    Ok(RonEncoded::new(problems))
}

#[server(
    input = Ron,
    output = Ron,
    custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_user_interaction(
    wall_uid: models::WallUid,
    problem_uid: models::ProblemUid,
) -> Result<RonEncoded<Option<models::UserInteraction>>, ServerFnError> {
    use crate::server::db::Database;
    use crate::server::db::DatabaseOperationError;
    use leptos::prelude::expect_context;
    tracing::trace!("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_uid: models::ProblemUid) -> 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_uid))?.map(|guard| guard.value());
                Ok(user_interaction)
            })
            .await?;

        Ok(user_interaction)
    }

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

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

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

    let db = expect_context::<Database>();
    let problem = db
        .read(|txn| {
            let table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
            let problem = table
                .get((wall_uid, problem_uid))?
                .ok_or(Error::NotFound(problem_uid))
                .map_err(DatabaseOperationError::custom)?
                .value();
            Ok(problem)
        })
        .await?;

    Ok(RonEncoded::new(problem))
}

/// 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_uid: models::ProblemUid,
    attempt: Option<models::Attempt>,
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
    use crate::server::db::Database;
    use crate::server::db::DatabaseOperationError;
    use leptos::prelude::expect_context;

    tracing::trace!("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_uid: models::ProblemUid, 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_uid);

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

                // 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_uid, attempt)
        .await
        .map_err(error_reporter::Report::new)
        .map_err(ServerFnError::new)
        .map(RonEncoded::new)
}
