use std::marker::PhantomData;

use sqlx::{
    postgres::PgArguments, Arguments, Encode, Executor, FromRow, Postgres, QueryBuilder, Type,
};

use crate::{
    cursor::{Cursor, CursorOrder, CursorType},
    error::QueryError,
    Edge, PageInfo, QueryArgs, QueryResult,
};

pub struct PgQuery<'q, O>
where
    O: for<'r> FromRow<'r, <sqlx::Postgres as sqlx::Database>::Row>,
    O: 'q + std::marker::Send,
    O: 'q + Unpin,
    O: 'q + Cursor,
{
    builder: QueryBuilder<'q, Postgres>,
    phantom: PhantomData<&'q O>,
    cursor: Option<CursorType>,
    cursor_order: CursorOrder,
    is_backward: bool,
    limit: u16,
    bind_pos: usize,
    arguments: PgArguments,
}

impl<'q, O> PgQuery<'q, O>
where
    O: for<'r> FromRow<'r, <sqlx::Postgres as sqlx::Database>::Row>,
    O: 'q + std::marker::Send,
    O: 'q + Unpin,
    O: 'q + Cursor,
{
    pub fn new(sql: impl Into<String>) -> Self {
        Self {
            builder: QueryBuilder::new(sql),
            phantom: PhantomData,
            cursor: None,
            cursor_order: CursorOrder::Asc,
            is_backward: false,
            limit: 0,
            bind_pos: 1,
            arguments: PgArguments::default(),
        }
    }

    pub fn bind<T: 'q + Send + Encode<'q, Postgres> + Type<Postgres>>(mut self, value: T) -> Self {
        self.arguments.add(value);
        self.bind_pos += 1;
        self
    }

    pub fn cursor_order(mut self, value: CursorOrder) -> Self {
        self.cursor_order = value;
        self
    }

    pub fn backward(self, last: u16, before: Option<CursorType>) -> Self {
        self.build(QueryArgs::backward(last, before))
    }

    pub fn backward_desc(self, last: u16, before: Option<CursorType>) -> Self {
        self.cursor_order(CursorOrder::Desc)
            .build(QueryArgs::backward(last, before))
    }

    pub fn forward(self, first: u16, after: Option<CursorType>) -> Self {
        self.build(QueryArgs::forward(first, after))
    }

    pub fn forward_desc(self, first: u16, after: Option<CursorType>) -> Self {
        self.cursor_order(CursorOrder::Desc)
            .build(QueryArgs::forward(first, after))
    }

    pub fn build_desc(self, args: QueryArgs) -> Self {
        self.cursor_order(CursorOrder::Desc).build(args)
    }

    pub fn build(mut self, args: QueryArgs) -> Self {
        let (limit, cursor) = if args.is_backward() {
            (args.last.unwrap_or(40), args.before.as_ref())
        } else {
            (args.first.unwrap_or(40), args.after.as_ref())
        };

        if cursor.is_some() {
            let filter = O::to_pg_filter_opts(
                &self.cursor_order,
                args.is_backward(),
                None,
                Some(self.bind_pos),
            );

            let filter = if self.builder.sql().contains(" WHERE ") {
                format!(" AND ({filter})")
            } else {
                format!(" WHERE {filter}")
            };

            self.builder.push(format!(" {filter}"));
        }

        let order = O::to_pg_order(&self.cursor_order, args.is_backward());
        self.builder
            .push(format!(" ORDER BY {order} LIMIT {}", limit + 1));

        self.cursor = cursor.cloned();
        self.is_backward = args.is_backward();
        self.limit = limit;

        self
    }

    pub async fn fetch_all<E>(self, executor: E) -> Result<QueryResult<O>, QueryError>
    where
        E: 'q + Executor<'q, Database = Postgres>,
    {
        let mut query = sqlx::query_as_with::<_, O, _>(self.builder.sql(), self.arguments);

        if let Some(cursor) = &self.cursor {
            let cursor = O::from_cursor(cursor)?;
            query = cursor.bind(query);
        }

        let mut rows = query.fetch_all(executor).await?;
        let has_more = rows.len() > self.limit as usize;

        if has_more {
            rows.pop();
        };

        let edges_iter = rows.into_iter().map(|node| Edge {
            cursor: node.to_cursor(),
            node,
        });

        let edges: Vec<_> = if self.is_backward {
            edges_iter.rev().collect()
        } else {
            edges_iter.collect()
        };

        let page_info = if self.is_backward {
            let start_cursor = edges.first().map(|edge| edge.cursor.clone());

            PageInfo {
                has_previous_page: has_more,
                has_next_page: false,
                start_cursor,
                end_cursor: None,
            }
        } else {
            let end_cursor = edges.last().map(|edge| edge.cursor.clone());

            PageInfo {
                has_previous_page: false,
                has_next_page: has_more,
                start_cursor: None,
                end_cursor,
            }
        };

        Ok(QueryResult { edges, page_info })
    }
}
