use anyhow::{Context, Result, anyhow};
use std::io::Write;

use crate::common::PIPESIZE;
use crate::common::is_binary::is_binary;
use crate::config;
use crate::filter_plugin::FilterChain;
use crate::services::item_service::ItemService;
use clap::Command;
use is_terminal::IsTerminal;
use std::io::Read;
use std::path::PathBuf;

/// Handles the get mode: retrieves and streams item content to stdout, applying filters if specified.
///
/// # Arguments
///
/// * `cmd` - Clap command for error handling.
/// * `settings` - Global settings, including force output flag.
/// * `ids` - List of item IDs (at most one).
/// * `tags` - List of tags to match (mutually exclusive with IDs).
/// * `conn` - Database connection.
/// * `data_path` - Path to data directory.
/// * `filter_chain` - Optional pre-parsed filter chain to apply to content.
///
/// # Returns
///
/// `Result<()>` on success, or an error if item not found or output fails.
pub fn mode_get(
    cmd: &mut Command,
    settings: &config::Settings,
    ids: &mut [i64],
    tags: &mut [String],
    conn: &mut rusqlite::Connection,
    data_path: PathBuf,
    filter_chain: Option<FilterChain>,
) -> Result<()> {
    if !ids.is_empty() && !tags.is_empty() {
        cmd.error(
            clap::error::ErrorKind::InvalidValue,
            "Both ID and tags given, you must supply either IDs or tags when using --get",
        )
        .exit();
    } else if ids.len() > 1 {
        cmd.error(
            clap::error::ErrorKind::InvalidValue,
            "More than one ID given, you must supply exactly one ID when using --get",
        )
        .exit();
    }
    // If both are empty, find_item will find the last item

    let item_service = ItemService::new(data_path.clone());
    let meta_filter: std::collections::HashMap<String, Option<String>> = settings
        .meta
        .iter()
        .map(|(k, v)| (k.clone(), v.clone()))
        .collect();
    let item_with_meta = item_service
        .find_item(conn, ids, tags, &meta_filter)
        .map_err(|e| anyhow!("Unable to find matching item in database: {}", e))?;

    let item_id = item_with_meta.item.id.context("Item missing ID")?;

    // Determine if we should detect binary data
    let mut detect_binary = !settings.force && std::io::stdout().is_terminal();

    if detect_binary {
        let meta_map = item_with_meta.meta_as_map();
        if let Some(text_val) = meta_map.get("text") {
            if text_val == "true" {
                detect_binary = false;
            } else if text_val == "false" {
                return Err(anyhow!(
                    "Refusing to output binary data to TTY, use --force to override"
                ));
            }
        }
    }

    if detect_binary {
        // Binary detection: sample first 8KB, then create a fresh reader for the full output.
        let (mut sample_reader, _, _) = item_service
            .get_item_content_info_streaming_with_item(item_with_meta, filter_chain.as_ref())?;
        let mut sample_buffer = vec![0; PIPESIZE];
        let bytes_read = sample_reader.read(&mut sample_buffer)?;
        if is_binary(&sample_buffer[..bytes_read]) {
            return Err(anyhow!(
                "Refusing to output binary data to TTY, use --force to override"
            ));
        }
        // Create fresh reader for actual output (sampling consumed the first reader)
        let (reader, _, _) = item_service.get_item_content_info_streaming_with_chain(
            conn,
            item_id,
            filter_chain.as_ref(),
        )?;
        stream_to_stdout(reader)?;
    } else {
        // No binary detection needed, use the already-fetched item with meta
        let (reader, _, _) = item_service
            .get_item_content_info_streaming_with_item(item_with_meta, filter_chain.as_ref())?;
        stream_to_stdout(reader)?;
    }

    Ok(())
}

fn stream_to_stdout(mut reader: Box<dyn Read + Send>) -> Result<()> {
    let mut stdout = std::io::stdout();
    crate::common::stream_copy(&mut reader, |chunk| {
        stdout.write_all(chunk)?;
        Ok(())
    })?;
    Ok(())
}
