//! Helpers for interacting with search results.
use anyhow::{Context, Result};
use indicatif::ProgressBar;
use omry_service::irpc::channel::mpsc;
use omry_service::search::{Hit, SearchResponse};
use omry_service::{MetaHit, SearchRespMeta};
use std::cmp;

/// Search hits for the given page.
#[derive(Debug)]
pub struct SearchPage {
    /// The original search response data.
    response: SearchResponse,
}

impl SearchPage {
    /// Returns a reference to the full original search response.
    pub fn full_response(&self) -> &SearchResponse {
        &self.response
    }

    /// Returns a 0-based index of the first hit on this page,
    /// accounting for the previous pages.
    #[must_use]
    pub(crate) fn start_idx(&self) -> usize {
        let prev_page_idx = self.response.page - 1;
        let per_page = self.response.request_params.per_page;
        prev_page_idx * per_page
    }

    /// Returns the length on this page,
    /// offset by the lengths of the previous pages.
    ///
    /// Returns `None` if there are no hits on this page.
    fn len_offset(&self) -> Option<usize> {
        if self.response.hits.is_empty() {
            return None;
        }

        // We won't get underflow because we checked that we have hits.
        Some(self.start_idx() + self.response.hits.len())
    }

    /// Total number of hits for all pages, as reported by Typesense.
    pub fn total(&self) -> usize {
        self.response.found
    }

    /// Returns `true` if this is the first page of the search results.
    pub fn is_first(&self) -> bool {
        self.page_idx() == 1
    }

    /// Returns `true` if this is the last page of search hits;
    /// `false` if there are more pages available from Typesense.
    pub fn is_last(&self) -> bool {
        // If we have no hits, we either got no results, or we've somehow
        // gone beyond the last page.
        let Some(len) = self.len_offset() else {
            return true;
        };

        len == self.total()
    }

    /// Returns the index of the current page, as reported by Typesense.
    ///
    /// 1-indexed.
    pub fn page_idx(&self) -> usize {
        self.response.page
    }

    /// Returns the number of hits on this page.
    #[must_use]
    pub(crate) fn len(&self) -> usize {
        self.response.hits.len()
    }

    /// Returns a reference to the hits for the current page of search results,
    /// along with the indexes of the hits, accounting for previous pages.
    pub fn hits(&self) -> Vec<(&Hit, usize)> {
        let end_idx = self.len_offset().unwrap_or_default();
        let idx_range = self.start_idx()..end_idx;
        self.response.hits.iter().zip(idx_range).collect()
    }
}

impl From<SearchResponse> for SearchPage {
    fn from(response: SearchResponse) -> Self {
        Self { response }
    }
}

/// Assembles a "unified" search response from a receiver of individual hits.
///
/// Shows a progress bar while receiving them.
pub(crate) async fn resp_from_stream(mut rx: mpsc::Receiver<MetaHit>) -> Result<SearchResponse> {
    // Adapted from
    // <https://forgejo.e37a.net/flora/omry/src/commit/ff742500269fdc86e81ef8350f0d4dd3206c8937/integration/src/bridge.rs#L155>
    let pb = ProgressBar::no_length();
    let mut metahits = vec![];
    while let Some(item) = rx.recv().await.context("error receiving hits")? {
        if pb.length().is_none() {
            let n_hits = item.meta.found;
            let per_page = item.meta.request_params.per_page;
            let n_items = u64::try_from(cmp::min(n_hits, per_page))?;
            pb.set_length(n_items);
        }

        metahits.push(item);
        pb.inc(1);
    }

    metahits.sort_by_key(|hit| hit.index);
    let last_mh = metahits
        .pop()
        .context("The search stream produced no hits")?;

    let MetaHit {
        meta,
        index: _,
        hit: last_hit,
    } = last_mh;

    let SearchRespMeta {
        found,
        out_of,
        page,
        request_params,
        search_time,
    } = meta;

    let mut hits = Vec::with_capacity(metahits.len());
    for mh in metahits {
        let Some(hit) = mh.hit else {
            // any hit being None means there aren't any hits,
            // just one item with metadata.
            break;
        };
        hits.push(hit);
    }

    if let Some(hit) = last_hit {
        hits.push(hit);
    }

    Ok(SearchResponse {
        found,
        out_of,
        page,
        request_params,
        search_time,
        hits,
    })
}
