use anyhow::Result;
use async_session::{async_trait, Session, SessionStore};
use diesel::prelude::*;

use crate::{models::BrowserSession, schema};

#[derive(Clone)]
pub struct PgStore {
    // TODO: reference instead of clone
    pool: deadpool_diesel::postgres::Pool,
}

impl PgStore {
    pub fn new(pool: deadpool_diesel::postgres::Pool) -> PgStore {
        Self { pool }
    }
}

impl std::fmt::Debug for PgStore {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "PgStore {{ pool }}")?;
        Ok(()).into()
    }
}

#[async_trait]
impl SessionStore for PgStore {
    async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
        let conn = self.pool.get().await?;
        let session_id = Session::id_from_cookie_value(&cookie_value)?;
        let rows = conn
            .interact(move |conn| {
                schema::browser_sessions::table
                    .filter(schema::browser_sessions::id.eq(session_id))
                    .select(BrowserSession::as_select())
                    .load(conn)
            })
            .await
            .unwrap()?;
        if rows.len() == 0 {
            Ok(None)
        } else {
            Ok(Some(serde_json::from_str::<Session>(
                rows[0].serialized.as_str(),
            )?))
        }
    }

    async fn store_session(&self, session: Session) -> Result<Option<String>> {
        let serialized = serde_json::to_string(&session)?;
        let conn = self.pool.get().await?;
        let session_id = session.id().to_string();
        conn.interact(move |conn| {
            diesel::insert_into(schema::browser_sessions::table)
                .values((
                    schema::browser_sessions::id.eq(session_id),
                    schema::browser_sessions::serialized.eq(serialized.clone()),
                ))
                .on_conflict(schema::browser_sessions::id)
                .do_update()
                .set(schema::browser_sessions::serialized.eq(serialized.clone()))
                .execute(conn)
        })
        .await
        .unwrap()?;
        session.reset_data_changed();
        Ok(session.into_cookie_value())
    }

    async fn destroy_session(&self, session: Session) -> Result<()> {
        let conn = self.pool.get().await?;
        conn.interact(move |conn| {
            diesel::delete(
                schema::browser_sessions::table
                    .filter(schema::browser_sessions::id.eq(session.id().to_string())),
            )
            .execute(conn)
        })
        .await
        .unwrap()?;
        Ok(())
    }

    async fn clear_store(&self) -> Result<()> {
        let conn = self.pool.get().await?;
        conn.interact(move |conn| diesel::delete(schema::browser_sessions::table).execute(conn))
            .await
            .unwrap()?;
        Ok(())
    }
}
