#![forbid(unsafe_code)]

use base64::{
    alphabet,
    engine::{general_purpose, GeneralPurpose},
    Engine,
};
use chrono::{DateTime, NaiveDateTime, Utc};
use harsh::Harsh;
use serde::{Deserialize, Serialize};
#[cfg(feature = "pg")]
use sqlx::{postgres::PgArguments, query::QueryAs, FromRow, Postgres};
use std::{fmt::Debug, str::FromStr};

use crate::error::QueryError;

#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
pub struct CursorType(pub String);

impl From<String> for CursorType {
    fn from(val: String) -> Self {
        CursorType(val)
    }
}

impl AsRef<[u8]> for CursorType {
    fn as_ref(&self) -> &[u8] {
        self.0.as_bytes()
    }
}

#[derive(Debug, Clone)]
pub enum CursorOrder {
    Asc,
    Desc,
}

pub trait Cursor: Sized {
    fn keys() -> Vec<&'static str>;
    #[cfg(feature = "pg")]
    fn bind<'q, O>(
        self,
        query: QueryAs<Postgres, O, PgArguments>,
    ) -> QueryAs<Postgres, O, PgArguments>
    where
        O: for<'r> FromRow<'r, <sqlx::Postgres as sqlx::Database>::Row>,
        O: 'q + std::marker::Send,
        O: 'q + Unpin,
        O: 'q + Cursor;
    fn serialize(&self) -> Vec<String>;
    fn deserialize(values: Vec<&str>) -> Result<Self, QueryError>;

    fn serialize_utc(value: DateTime<Utc>) -> String {
        Harsh::default().encode(&[value.timestamp_micros() as u64])
    }

    fn deserialize_as<F: Into<String>, D: FromStr>(
        field: F,
        value: Option<&&str>,
    ) -> Result<D, QueryError> {
        let field = field.into();
        value
            .ok_or(QueryError::MissingField(field.to_owned()))
            .and_then(|v| {
                v.to_string().parse::<D>().map_err(|_| {
                    QueryError::Unknown(
                        field,
                        v.to_string(),
                        "failed to deserialize_as_string".to_owned(),
                    )
                })
            })
    }

    fn deserialize_as_utc<F: Into<String>>(
        field: F,
        value: Option<&&str>,
    ) -> Result<DateTime<Utc>, QueryError> {
        let field = field.into();
        value
            .ok_or(QueryError::MissingField(field))
            .and_then(|v| {
                Harsh::default()
                    .decode(v)
                    .map(|v| v[0])
                    .map_err(QueryError::Harsh)
            })
            .and_then(|timestamp| {
                NaiveDateTime::from_timestamp_micros(timestamp as i64).ok_or(QueryError::Unknown(
                    "field".to_owned(),
                    "NaiveDateTime::from_timestamp_opt".to_owned(),
                    "none".to_owned(),
                ))
            })
            .map(|datetime| DateTime::from_naive_utc_and_offset(datetime, Utc))
    }

    fn to_cursor(&self) -> CursorType {
        let data = self.serialize().join("|");
        let engine = GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::PAD);

        CursorType(engine.encode(data))
    }

    fn from_cursor(cursor: &CursorType) -> Result<Self, QueryError> {
        let engine = GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::PAD);
        let decoded = engine.decode(cursor)?;
        let data = std::str::from_utf8(&decoded)?;

        Self::deserialize(data.split('|').collect())
    }

    fn to_pg_filter_opts(
        order: &CursorOrder,
        backward: bool,
        keys: Option<Vec<&str>>,
        pos: Option<usize>,
    ) -> String {
        let pos = pos.unwrap_or(1);
        let with_braket = keys.is_some();
        let mut keys = keys.unwrap_or(Self::keys());
        let key = keys.remove(0);

        let sign = match (order, backward) {
            (CursorOrder::Asc, true) | (CursorOrder::Desc, false) => "<",
            (CursorOrder::Asc, false) | (CursorOrder::Desc, true) => ">",
        };
        let filter = format!("{key} {sign} ${pos}");

        if keys.is_empty() {
            return filter;
        }

        let filter = format!(
            "{filter} OR ({key} = ${pos} AND {})",
            Self::to_pg_filter_opts(order, backward, Some(keys), Some(pos + 1))
        );

        if with_braket {
            format!("({filter})")
        } else {
            filter
        }
    }

    fn to_pg_order(order: &CursorOrder, backward: bool) -> String {
        let order = match (order, backward) {
            (CursorOrder::Asc, true) | (CursorOrder::Desc, false) => "DESC",
            (CursorOrder::Asc, false) | (CursorOrder::Desc, true) => "ASC",
        };

        Self::keys()
            .iter()
            .map(|key| format!("{key} {order}"))
            .collect::<Vec<_>>()
            .join(", ")
    }
}
