use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use crate::config::{PdfuniteConfig, output_config_hash, resolve_extra_inputs};
use crate::file_index::FileIndex;
use crate::graph::{BuildGraph, Product};
use crate::processors::{ProcessorBase, ProductDiscovery, run_command, check_command_output};

/// Recursively find directories under `base` that contain files with the given extension.
/// Results are sorted for deterministic output.
fn find_dirs_with_ext(base: &Path, ext: &str) -> Vec<PathBuf> {
    let mut result = Vec::new();
    collect_dirs_with_ext(base, ext, &mut result);
    result.sort();
    result
}

fn collect_dirs_with_ext(dir: &Path, ext: &str, result: &mut Vec<PathBuf>) {
    let entries = match fs::read_dir(dir) {
        Ok(e) => e,
        Err(_) => return,
    };
    let mut has_matching_file = false;
    let mut subdirs = Vec::new();
    for entry in entries.flatten() {
        let ft = match entry.file_type() {
            Ok(ft) => ft,
            Err(_) => continue,
        };
        if ft.is_dir() {
            subdirs.push(entry.path());
        } else if !has_matching_file && ft.is_file() {
            if entry.path().extension().is_some_and(|e| e == ext) {
                has_matching_file = true;
            }
        }
    }
    if has_matching_file {
        result.push(dir.to_path_buf());
    }
    for subdir in subdirs {
        collect_dirs_with_ext(&subdir, ext, result);
    }
}

pub struct PdfuniteProcessor {
    base: ProcessorBase,
    config: PdfuniteConfig,
}

impl PdfuniteProcessor {
    pub fn new(config: PdfuniteConfig) -> Self {
        Self {
            base: ProcessorBase::generator(
                crate::processors::names::PDFUNITE,
                "Merge PDFs from subdirectories into course bundles",
            ),
            config,
        }
    }
}

impl ProductDiscovery for PdfuniteProcessor {
    delegate_base!(generator_no_auto_detect);

    fn auto_detect(&self, _file_index: &FileIndex) -> bool {
        let base = Path::new(&self.config.source_dir);
        if !base.exists() {
            return false;
        }
        let ext = self.config.source_ext.strip_prefix('.').unwrap_or(&self.config.source_ext);
        !find_dirs_with_ext(base, ext).is_empty()
    }

    fn required_tools(&self) -> Vec<String> {
        vec![self.config.pdfunite_bin.clone()]
    }

    fn discover(&self, graph: &mut BuildGraph, _file_index: &FileIndex, instance_name: &str) -> Result<()> {
        let base = Path::new(&self.config.source_dir);
        if !base.exists() {
            return Ok(());
        }

        let hash = Some(output_config_hash(&self.config, &[]));
        let extra = resolve_extra_inputs(&self.config.extra_inputs)?;
        let ext = self.config.source_ext.strip_prefix('.').unwrap_or(&self.config.source_ext);

        let dirs = find_dirs_with_ext(base, ext);

        // Compute upstream scan dir once
        let upstream_scan_dir = Path::new(&self.config.source_dir)
            .components()
            .next()
            .map(|c| c.as_os_str().to_string_lossy().into_owned())
            .unwrap_or_default();
        let upstream_scan_dirs = [upstream_scan_dir];

        for dir_path in dirs {
            // Find all source files in this directory
            let mut source_files: Vec<PathBuf> = fs::read_dir(&dir_path)?
                .filter_map(|e| e.ok())
                .map(|e| e.path())
                .filter(|p| p.extension().is_some_and(|e| e == ext))
                .collect();
            if source_files.is_empty() {
                continue;
            }
            source_files.sort();

            let inputs: Vec<PathBuf> = source_files.iter().map(|src| {
                super::output_path(src, &upstream_scan_dirs, &self.config.source_output_dir, "pdf")
            }).chain(extra.iter().cloned()).collect();

            // Mirror the directory structure from source_dir into output_dir,
            // naming each merged PDF after its leaf directory.
            let relative = dir_path.strip_prefix(base).unwrap_or(&dir_path);
            let parent = relative.parent().unwrap_or(Path::new(""));
            let leaf = relative.file_name().unwrap_or(relative.as_os_str());
            let outputs = vec![
                Path::new(&self.config.output_dir).join(parent).join(format!("{}.pdf", leaf.to_string_lossy())),
            ];

            graph.add_product(inputs, outputs, instance_name, hash.clone())?;
        }

        Ok(())
    }

    fn execute(&self, product: &Product) -> Result<()> {
        let output = product.primary_output();

        crate::processors::ensure_output_dir(output)?;

        let mut cmd = Command::new(&self.config.pdfunite_bin);
        for arg in &self.config.args {
            cmd.arg(arg);
        }
        // pdfunite takes input files followed by output file
        for input in &product.inputs {
            cmd.arg(input);
        }
        cmd.arg(output);

        let out = run_command(&mut cmd)?;
        check_command_output(&out, format_args!("pdfunite {}", output.display()))
    }

}
