use crate::core::decision::Rule;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::path::{Path, PathBuf};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum BotignoreError {
    #[error("failed to read .botignore: {0}")]
    Io(#[from] std::io::Error),
    #[error("failed to compile .botignore: {0}")]
    Compile(#[source] ignore::Error),
}

struct ScopedMatcher {
    /// Path of the source `.botignore` file.
    source: PathBuf,
    /// Directory the matcher is rooted at (parent of `source`).
    dir: PathBuf,
    /// Depth of `dir` (component count) — deeper = more specific.
    depth: usize,
    matcher: Gitignore,
}

/// A collection of `.botignore` matchers, one per file discovered under the
/// project root. Each matcher is rooted at its source file's directory so
/// gitignore-style semantics (anchored vs unanchored patterns, per-directory
/// scope) work correctly. At match time, the deepest applicable matcher
/// wins; whitelist (`!` negation) at any depth overrides an ignore at
/// shallower depth.
pub struct BotignoreSet {
    matchers: Vec<ScopedMatcher>,
}

impl BotignoreSet {
    /// Walk `root` recursively, building a per-file matcher for every
    /// `.botignore` encountered. Empty if none are found.
    pub fn load(root: &Path) -> Result<Self, BotignoreError> {
        let mut matchers = Vec::new();
        for entry in walkdir::WalkDir::new(root).into_iter().filter_map(Result::ok) {
            if !(entry.file_type().is_file() && entry.file_name() == ".botignore") {
                continue;
            }
            let source = entry.path().to_path_buf();
            let dir = source.parent().unwrap_or(root).to_path_buf();
            let mut builder = GitignoreBuilder::new(&dir);
            if let Some(err) = builder.add(&source) {
                return Err(BotignoreError::Compile(err));
            }
            let matcher = builder.build().map_err(BotignoreError::Compile)?;
            let depth = dir.components().count();
            matchers.push(ScopedMatcher {
                source,
                dir,
                depth,
                matcher,
            });
        }
        // Shallowest first so iteration applies broader rules then more-specific overrides.
        matchers.sort_by_key(|m| m.depth);
        Ok(Self { matchers })
    }

    /// Returns `Some(Rule)` if `path` is matched (and not negated by a
    /// deeper-scoped whitelist), else `None`. The deepest matcher whose
    /// directory contains `path` wins.
    pub fn matched(&self, path: &Path) -> Result<Option<Rule>, BotignoreError> {
        let is_dir = path.is_dir();
        let mut current: Option<&ScopedMatcher> = None;
        let mut current_pattern: Option<String> = None;

        for sm in &self.matchers {
            if !path.starts_with(&sm.dir) {
                continue;
            }
            let m = sm.matcher.matched(path, is_dir);
            if m.is_ignore() {
                current = Some(sm);
                current_pattern = m.inner().map(|g| g.original().to_string());
            } else if m.is_whitelist() {
                // Deeper whitelist overrides any shallower ignore.
                current = None;
                current_pattern = None;
            }
        }

        Ok(current.map(|sm| Rule {
            source: sm.source.clone(),
            pattern: current_pattern.unwrap_or_default(),
        }))
    }
}
