//! Background text-index syncer infrastructure.
//!
//! [`TextSyncerBundle`] holds the handles for a running `TextIndexSyncer`
//! thread; [`spawn_text_syncer`] creates one; [`shutdown_text_syncer`] joins it.

use std::sync::Arc;

use crate::text::{PendingWrite, TextIndex, TextIndexConfig, TextIndexSyncer};

use super::config::Config;

/// Result from spawning a background text syncer thread.
pub struct TextSyncerBundle {
    pub index: Option<Arc<TextIndex>>,
    pub write_tx: Option<crossbeam::channel::Sender<PendingWrite>>,
    pub flush_tx: Option<crossbeam::channel::Sender<crossbeam::channel::Sender<()>>>,
    pub thread: std::sync::Mutex<Option<std::thread::JoinHandle<crate::Result<()>>>>,
}

/// Spawn a background text syncer thread for a text index.
///
/// Creates a `TextIndex` (on-disk if `config.data_dir` is set, ephemeral otherwise),
/// a crossbeam channel for pending writes, an optional flush channel for synchronous
/// commit requests, and a syncer thread that commits batches.
pub fn spawn_text_syncer(
    text_fields: &[crate::schema::TextFieldDef],
    config: &Config,
    index_name: &str,
    thread_name: &str,
) -> TextSyncerBundle {
    if text_fields.is_empty() {
        return TextSyncerBundle {
            index: None,
            write_tx: None,
            flush_tx: None,
            thread: std::sync::Mutex::new(None),
        };
    }

    let text_config = config
        .data_dir
        .as_ref()
        .map_or_else(TextIndexConfig::default, |dir| TextIndexConfig {
            index_dir: dir.join(index_name),
            ..TextIndexConfig::default()
        });

    let index = match &config.data_dir {
        Some(_) => TextIndex::open(text_config.clone(), text_fields),
        None => TextIndex::ephemeral(text_fields),
    };

    let index = match index {
        Ok(idx) => Arc::new(idx),
        Err(_) => {
            return TextSyncerBundle {
                index: None,
                write_tx: None,
                flush_tx: None,
                thread: std::sync::Mutex::new(None),
            };
        }
    };

    let (tx, rx) = crossbeam::channel::unbounded();
    let (flush_tx, flush_rx) = crossbeam::channel::bounded::<crossbeam::channel::Sender<()>>(4);
    let idx_clone = Arc::clone(&index);
    let commit_n = text_config.commit_every_n_docs;
    let commit_secs = text_config.commit_every_secs;
    let tname = thread_name.to_owned();

    let handle = std::thread::Builder::new()
        .name(tname)
        .spawn(move || {
            TextIndexSyncer::new(idx_clone, rx, commit_n, commit_secs)
                .with_flush_rx(flush_rx)
                .run()
        })
        .ok();

    TextSyncerBundle {
        index: Some(index),
        write_tx: Some(tx),
        flush_tx: Some(flush_tx),
        thread: std::sync::Mutex::new(handle),
    }
}

/// Drop the write-channel sender then join the syncer thread.
///
/// Used by `shutdown_inner` for both item and creator text syncers.
pub fn shutdown_text_syncer(
    tx_mutex: &std::sync::Mutex<Option<crossbeam::channel::Sender<PendingWrite>>>,
    thread_mutex: &std::sync::Mutex<Option<std::thread::JoinHandle<crate::Result<()>>>>,
    label: &str,
) {
    // Drop the sender to disconnect the channel -- the syncer flushes and exits.
    let sender = match tx_mutex.lock() {
        Ok(mut g) => g.take(),
        Err(poisoned) => poisoned.into_inner().take(),
    };
    drop(sender);

    // Join the syncer thread.
    let thread = match thread_mutex.lock() {
        Ok(mut g) => g.take(),
        Err(poisoned) => poisoned.into_inner().take(),
    };
    if let Some(handle) = thread {
        match handle.join() {
            Ok(Ok(())) => {}
            Ok(Err(e)) => tracing::error!(error = %e, "{label} syncer thread returned an error"),
            Err(_) => tracing::error!("{label} syncer thread panicked"),
        }
    }
}
