use af_move_type::MoveInstance;
use af_sui_types::Address;
use enum_as_inner::EnumAsInner;
use futures::Stream;
use graphql_extract::extract;
use sui_gql_client::queries::Error;
use sui_gql_client::queries::model::fragments::{DynamicFieldName, MoveValueGql, PageInfoForward};
use sui_gql_client::{GraphQlClient, GraphQlResponseExt as _, schema};

use crate::orderbook::Order;
use crate::ordered_map::Leaf;

pub(super) fn query<C: GraphQlClient>(
    client: &C,
    package: Address,
    ch: Address,
    at_checkpoint: Option<u64>,
    asks: bool,
) -> impl Stream<Item = Result<(u128, Order), Error<C::Error>>> + '_ {
    let orderbook: DynamicFieldName = crate::keys::Orderbook::new()
        .move_instance(package)
        .try_into()
        .expect("BCS-serializable");

    let map_name: DynamicFieldName = if asks {
        crate::keys::AsksMap::new()
            .move_instance(package)
            .try_into()
            .expect("BCS-serializable")
    } else {
        crate::keys::BidsMap::new()
            .move_instance(package)
            .try_into()
            .expect("BCS-serializable")
    };
    async_stream::try_stream! {
        let mut vars = Variables {
            ch,
            at_checkpoint,
            orderbook,
            map_name,
            first: Some(32),
            after: None,
        };
        let mut has_next_page = true;
        while has_next_page {
            let (page_info, orders) = request(client, vars.clone()).await?;

            vars.after = page_info.end_cursor.clone();
            has_next_page = page_info.has_next_page;

            for value in orders {
                yield value;
            }
        }
    }
}

async fn request<C: GraphQlClient>(
    client: &C,
    vars: Variables,
) -> Result<
    (
        PageInfoForward,
        impl Iterator<Item = (u128, Order)> + 'static,
    ),
    Error<C::Error>,
> {
    let data = client
        .query::<Query, _>(vars)
        .await
        .map_err(Error::Client)?
        .try_into_data()?;
    let MapDfsConnection { nodes, page_info } = extract(data)?;
    Ok((page_info, nodes.into_iter().flat_map(MapDf::into_orders)))
}

fn extract(data: Option<Query>) -> Result<MapDfsConnection, &'static str> {
    extract!(data => {
        clearing_house? {
            orderbook_dof? {
                orderbook? {
                    ... on OrderbookDofValue::MoveObject {
                        map_dof? {
                            map? {
                                ... on MapDofValue::MoveObject {
                                    map_dfs?
                                }
                            }
                        }
                    }
                }
            }
        }
    });
    Ok(map_dfs)
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[test]
fn gql_output() {
    use cynic::QueryBuilder as _;

    let package = Address::ZERO;
    let orderbook: DynamicFieldName = crate::keys::Orderbook::new()
        .move_instance(package)
        .try_into()
        .unwrap();

    let map_name: DynamicFieldName = crate::keys::BidsMap::new()
        .move_instance(package)
        .try_into()
        .unwrap();

    let vars = Variables {
        ch: Address::ZERO,
        at_checkpoint: None,
        orderbook,
        map_name,
        first: Some(10),
        after: None,
    };
    let operation = Query::build(vars);
    insta::assert_snapshot!(operation.query, @r"
    query Query($ch: SuiAddress!, $atCheckpoint: UInt53, $orderbook: DynamicFieldName!, $mapName: DynamicFieldName!, $first: Int, $after: String) {
      clearing_house: object(address: $ch, atCheckpoint: $atCheckpoint) {
        orderbook_dof: dynamicObjectField(name: $orderbook) {
          orderbook: value {
            __typename
            ... on MoveObject {
              map_dof: dynamicObjectField(name: $mapName) {
                map: value {
                  __typename
                  ... on MoveObject {
                    map_dfs: dynamicFields(first: $first, after: $after) {
                      nodes {
                        map_df: value {
                          __typename
                          ... on MoveValue {
                            type {
                              repr
                            }
                            bcs
                          }
                        }
                      }
                      pageInfo {
                        hasNextPage
                        endCursor
                      }
                    }
                    __typename
                  }
                }
              }
              __typename
            }
          }
        }
      }
    }
    ");
}

#[derive(cynic::QueryVariables, Clone, Debug)]
struct Variables {
    ch: Address,
    at_checkpoint: Option<u64>,
    orderbook: DynamicFieldName,
    map_name: DynamicFieldName,
    first: Option<i32>,
    after: Option<String>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(variables = "Variables")]
struct Query {
    #[arguments(address: $ch, atCheckpoint: $at_checkpoint)]
    #[cynic(alias, rename = "object")]
    clearing_house: Option<Object>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Object", variables = "Variables")]
struct Object {
    #[arguments(name: $orderbook)]
    #[cynic(alias, rename = "dynamicObjectField")]
    orderbook_dof: Option<OrderbookDof>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "DynamicField", variables = "Variables")]
struct OrderbookDof {
    #[cynic(alias, rename = "value")]
    orderbook: Option<OrderbookDofValue>,
}

#[derive(cynic::InlineFragments, Debug, EnumAsInner)]
#[cynic(graphql_type = "DynamicFieldValue", variables = "Variables")]
enum OrderbookDofValue {
    MoveObject(OrderbookObject),
    #[cynic(fallback)]
    Unknown,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "MoveObject", variables = "Variables")]
struct OrderbookObject {
    #[arguments(name: $map_name)]
    #[cynic(alias, rename = "dynamicObjectField")]
    map_dof: Option<MapDof>,
    __typename: String,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "DynamicField", variables = "Variables")]
struct MapDof {
    #[cynic(alias, rename = "value")]
    map: Option<MapDofValue>,
}

#[derive(cynic::InlineFragments, Debug, EnumAsInner)]
#[cynic(graphql_type = "DynamicFieldValue", variables = "Variables")]
enum MapDofValue {
    MoveObject(MapObject),
    #[cynic(fallback)]
    Unknown,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "MoveObject", variables = "Variables")]
struct MapObject {
    #[arguments(first: $first, after: $after)]
    #[cynic(alias, rename = "dynamicFields")]
    map_dfs: Option<MapDfsConnection>,
    __typename: String,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "DynamicFieldConnection")]
struct MapDfsConnection {
    nodes: Vec<MapDf>,
    page_info: PageInfoForward,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "DynamicField")]
struct MapDf {
    #[cynic(alias, rename = "value")]
    map_df: Option<MapDfValue>,
}

impl MapDf {
    fn into_orders(self) -> impl Iterator<Item = (u128, Order)> {
        self.map_df
            .into_iter()
            .map(MapDfValue::into_move_value)
            .filter_map(Result::ok)
            // If deser. fails, we just assume it was a `Branch` instead of a `Leaf`
            .filter_map(|raw| MoveInstance::<Leaf<Order>>::try_from(raw).ok())
            .flat_map(|leaf| Vec::from(leaf.value.keys_vals).into_iter())
            .map(|pair| (pair.key, pair.val))
    }
}

#[derive(cynic::InlineFragments, Debug, EnumAsInner)]
#[cynic(graphql_type = "DynamicFieldValue")]
enum MapDfValue {
    MoveValue(MoveValueGql),
    #[cynic(fallback)]
    Unknown,
}
