use std::sync::Arc;
use std::time::Duration;

use common::counter::hardware_accumulator::HwMeasurementAcc;
use futures::Future;
use itertools::Itertools;

use super::group_by::{GroupRequest, group_by};
use crate::collection::Collection;
use crate::lookup::lookup_ids;
use crate::lookup::types::PseudoId;
use crate::operations::consistency_params::ReadConsistency;
use crate::operations::shard_selector_internal::ShardSelectorInternal;
use crate::operations::types::{CollectionError, CollectionResult, PointGroup};

/// Builds on top of the group_by function to add lookup and possibly other features
pub struct GroupBy<'a, F, Fut>
where
    F: Fn(String) -> Fut + Clone,
    Fut: Future<Output = Option<Arc<Collection>>>,
{
    group_by: GroupRequest,
    collection: &'a Collection,
    /// `Fn` to get a collection having its name. Obligatory for recommend and lookup
    collection_by_name: F,
    read_consistency: Option<ReadConsistency>,
    shard_selection: ShardSelectorInternal,
    timeout: Option<Duration>,
    hw_measurement_acc: HwMeasurementAcc,
}

impl<'a, F, Fut> GroupBy<'a, F, Fut>
where
    F: Fn(String) -> Fut + Clone,
    Fut: Future<Output = Option<Arc<Collection>>>,
{
    /// Creates a basic GroupBy builder
    pub fn new(
        group_by: GroupRequest,
        collection: &'a Collection,
        collection_by_name: F,
        hw_measurement_acc: HwMeasurementAcc,
    ) -> Self {
        Self {
            group_by,
            collection,
            collection_by_name,
            read_consistency: None,
            shard_selection: ShardSelectorInternal::All,
            timeout: None,
            hw_measurement_acc,
        }
    }

    pub fn set_read_consistency(mut self, read_consistency: Option<ReadConsistency>) -> Self {
        self.read_consistency = read_consistency;
        self
    }

    pub fn set_shard_selection(mut self, shard_selection: ShardSelectorInternal) -> Self {
        self.shard_selection = shard_selection;
        self
    }

    pub fn set_timeout(mut self, timeout: Option<Duration>) -> Self {
        self.timeout = timeout;
        self
    }

    /// Runs the group by operation, optionally with a timeout.
    pub async fn execute(self) -> CollectionResult<Vec<PointGroup>> {
        if let Some(timeout) = self.timeout {
            tokio::time::timeout(timeout, self.run())
                .await
                .map_err(|_| {
                    log::debug!("GroupBy timeout reached: {timeout:?}");
                    CollectionError::timeout(timeout, "GroupBy")
                })?
        } else {
            self.run().await
        }
    }

    /// Does the actual grouping
    async fn run(self) -> CollectionResult<Vec<PointGroup>> {
        let start = std::time::Instant::now();
        let with_lookup = self.group_by.with_lookup.clone();

        let core_group_by = self
            .group_by
            .into_query_group_request(
                self.collection,
                self.collection_by_name.clone(),
                self.read_consistency,
                self.shard_selection.clone(),
                self.timeout,
                self.hw_measurement_acc.clone(),
            )
            .await?;

        let mut groups = group_by(
            core_group_by,
            self.collection,
            self.read_consistency,
            self.shard_selection.clone(),
            self.timeout,
            self.hw_measurement_acc.clone(),
        )
        .await?;

        if let Some(lookup) = with_lookup {
            // update timeout
            let timeout = self
                .timeout
                .map(|timeout| timeout.saturating_sub(start.elapsed()));
            let mut lookups = {
                let pseudo_ids = groups
                    .iter()
                    .map(|group| group.id.clone())
                    .map_into()
                    .collect();

                lookup_ids(
                    lookup,
                    pseudo_ids,
                    self.collection_by_name,
                    self.read_consistency,
                    &self.shard_selection,
                    timeout,
                    self.hw_measurement_acc.clone(),
                )
                .await?
            };

            // Put the lookups in their respective groups
            groups.iter_mut().for_each(|group| {
                group.lookup = lookups
                    .remove(&PseudoId::from(group.id.clone()))
                    .map(api::rest::Record::from);
            });
        }

        Ok(groups)
    }
}
