use super::super::EXTENSION_ID;
use super::super::config::FileSearchConfig;
use super::super::config::SearchBy;
use super::should_be_filtered_out;
use crate::common::document::{DataSourceReference, Document};
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
use crate::extension::OnOpened;
use crate::util::file::sync_get_file_icon;
use futures::stream::Stream;
use futures::stream::StreamExt;
use std::os::fd::OwnedFd;
use std::path::Path;
use tokio::io::AsyncBufReadExt;
use tokio::io::BufReader;
use tokio::process::Child;
use tokio::process::Command;
use tokio_stream::wrappers::LinesStream;

/// `mdfind` won't return scores, we use this score for all the documents.
const SCORE: f64 = 1.0;

pub(crate) async fn hits(
    query_string: &str,
    from: usize,
    size: usize,
    config: &FileSearchConfig,
) -> Result<Vec<(Document, f64)>, String> {
    let (mut iter, _mdfind_child_process) =
        execute_mdfind_query(&query_string, from, size, &config)?;

    // Convert results to documents
    let mut hits: Vec<(Document, f64)> = Vec::new();
    while let Some(res_file_path) = iter.next().await {
        let file_path = res_file_path.map_err(|io_err| io_err.to_string())?;

        let icon = sync_get_file_icon(&file_path);
        let file_path_of_type_path = camino::Utf8Path::new(&file_path);
        let r#where = file_path_of_type_path
            .parent()
            .unwrap_or_else(|| {
                panic!(
                    "expect path [{}] to have a parent, but it does not",
                    file_path
                );
            })
            .to_string();

        let file_name = file_path_of_type_path.file_name().unwrap_or_else(|| {
            panic!(
                "expect path [{}] to have a file name, but it does not",
                file_path
            );
        });
        let on_opened = OnOpened::Document {
            url: file_path.clone(),
        };

        let doc = Document {
            id: file_path.clone(),
            title: Some(file_name.to_string()),
            source: Some(DataSourceReference {
                r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()),
                name: Some(EXTENSION_ID.into()),
                id: Some(EXTENSION_ID.into()),
                icon: Some(String::from("font_Filesearch")),
            }),
            category: Some(r#where),
            on_opened: Some(on_opened),
            url: Some(file_path),
            icon: Some(icon.to_string()),
            ..Default::default()
        };

        hits.push((doc, SCORE));
    }

    Ok(hits)
}

/// Return an array containing the `mdfind` command and its arguments.
fn build_mdfind_query(query_string: &str, config: &FileSearchConfig) -> Vec<String> {
    let mut args = vec!["mdfind".to_string()];

    match config.search_by {
        SearchBy::Name => {
            // The tailing char 'c' makes the search case-insensitive.
            //
            // According to [1], we should use this syntax "kMDItemFSName ==[c] '*{}*'",
            // but it does not work on my machine (macOS 26 beta 7), and you
            // can find similar complaints as well [2].
            //
            // [1]: https://developer.apple.com/library/archive/documentation/Carbon/Conceptual/SpotlightQuery/Concepts/QueryFormat.html
            // [2]: https://apple.stackexchange.com/q/263671/394687
            args.push(format!("kMDItemFSName == '*{}*'c", query_string));
        }
        SearchBy::NameAndContents => {
            // Do not specify any File System Metadata Attribute Keys to search
            // all of them, it is case-insensitive by default.
            //
            // Previously, we use:
            //
            //    "kMDItemFSName == '*{}*' || kMDItemTextContent == '{}'"
            //
            // But the kMDItemTextContent attribute does not work as expected.
            // For example, if a PDF document contains both "Waterloo" and
            // "waterloo", it is only matched by "Waterloo".
            args.push(query_string.to_string());
        }
    }

    // Add search paths using -onlyin
    for path in &config.search_paths {
        if Path::new(path).exists() {
            args.extend_from_slice(&["-onlyin".to_string(), path.to_string()]);
        }
    }

    args
}

/// Spawn the `mdfind` child process and return an async iterator over its output,
/// allowing us to collect the results asynchronously.
///
/// # Return value:
///
/// * impl Stream: an async iterator that will yield the matched files
/// * Child: The handle to the mdfind process.  The child process will be killed
///   when this handle gets dropped, we need to keep it alive until we exhaust
///   all the query results.
fn execute_mdfind_query(
    query_string: &str,
    from: usize,
    size: usize,
    config: &FileSearchConfig,
) -> Result<(impl Stream<Item = std::io::Result<String>>, Child), String> {
    let args = build_mdfind_query(query_string, &config);
    let (rx, tx) = std::io::pipe().unwrap();
    let rx_owned = OwnedFd::from(rx);
    let async_rx = tokio::net::unix::pipe::Receiver::from_owned_fd(rx_owned).unwrap();
    let buffered_rx = BufReader::new(async_rx);
    let lines = LinesStream::new(buffered_rx.lines());

    let child = Command::new(&args[0])
        .args(&args[1..])
        .stdout(tx)
        .stderr(std::process::Stdio::null())
        .kill_on_drop(true)
        .spawn()
        .map_err(|e| format!("Failed to spawn mdfind: {}", e))?;
    let config_clone = config.clone();
    let iter = lines
        .filter(move |res_path| {
            std::future::ready({
                match res_path {
                    Ok(path) => !should_be_filtered_out(&config_clone, path, false, true, true),
                    Err(_) => {
                        // Don't filter out Err() values
                        true
                    }
                }
            })
        })
        .skip(from)
        .take(size);

    Ok((iter, child))
}

pub(crate) fn apply_config(_: &FileSearchConfig) -> Result<(), String> {
    // By default, macOS indexes all the files within a volume if indexing is
    // enabled. So, to ensure our search paths are indexed by Spotlight,
    // theoretically, we can do the following things:
    //
    // 1. Ensure indexing is enabled on the volumes where our search paths reside.
    //    However, we cannot do this as doing so requires `sudo`.
    //
    // 2. Ensure the search paths are not excluded from indexing scope. Users can
    //    stop Spotlight from indexing a directory by:
    //    1. adding it to the "Privacy" list in 'System Settings'. Coco cannot
    //       modify this list, since the only way to change it is manually
    //       through System Settings.
    //    2. Renaming directory name, adding a `.noindex` file extension to it.
    //       I don't want to use this trick, users won't feel comfortable and it
    //       could break at any time.
    //    3. Creating a `.metadata_never_index` file within the directory (no longer works
    //       since macOS Mojave)
    //
    // There is nothing we can do.
    Ok(())
}
