use crate::core::botignore::{BotignoreError, BotignoreSet};
use crate::core::decision::{Decision, Reason, Rule};
use crate::core::op::Op;
use crate::core::toml_config::{BotignoreToml, TomlConfigError};
use std::path::{Path, PathBuf};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum PolicyError {
    #[error(transparent)]
    Botignore(#[from] BotignoreError),
    #[error(transparent)]
    Toml(#[from] TomlConfigError),
    #[error("invalid pattern in botignore.toml: {0}")]
    BadPattern(String),
}

pub struct Policy {
    root: PathBuf,
    botignore: BotignoreSet,
    toml: BotignoreToml,
    read_globs: globset::GlobSet,
    write_globs: globset::GlobSet,
    read_patterns: Vec<String>,
    write_patterns: Vec<String>,
}

impl Policy {
    pub fn load(root: &Path) -> Result<Self, PolicyError> {
        let botignore = BotignoreSet::load(root)?;
        let toml = BotignoreToml::load(root)?;

        let (read_globs, read_patterns) = compile_globs(
            toml.read.as_ref().map(|r| r.patterns.as_slice()).unwrap_or(&[]),
        )?;
        let (write_globs, write_patterns) = compile_globs(
            toml.write.as_ref().map(|r| r.patterns.as_slice()).unwrap_or(&[]),
        )?;

        Ok(Self {
            root: root.to_path_buf(),
            botignore,
            toml,
            read_globs,
            write_globs,
            read_patterns,
            write_patterns,
        })
    }

    pub fn check_command(&self, command: &str) -> Result<Decision, PolicyError> {
        let bash = match self.toml.bash.as_ref() {
            Some(b) => b,
            None => return Ok(Decision::Allow),
        };

        // 1. Deny wins over everything else.
        if let Some(pat) = match_command(command, &bash.deny)? {
            return Ok(Decision::Deny(Reason {
                message: format!("blocked by botignore.toml [bash.deny]: {}", pat),
                rule: Some(Rule {
                    source: self.root.join("botignore.toml"),
                    pattern: pat,
                }),
            }));
        }

        // 2. Allow prefixes — if any matches, allow.
        for prefix in &bash.allow_prefixes {
            if command_matches_prefix(command, prefix) {
                return Ok(Decision::Allow);
            }
        }

        // 3. Ask patterns.
        if let Some(pat) = match_command(command, &bash.ask)? {
            return Ok(Decision::Ask(Reason {
                message: format!("requires confirmation [bash.ask]: {}", pat),
                rule: Some(Rule {
                    source: self.root.join("botignore.toml"),
                    pattern: pat,
                }),
            }));
        }

        Ok(Decision::Allow)
    }

    pub fn check(&self, op: Op, path: &Path) -> Result<Decision, PolicyError> {
        // 1. .botignore is path-only and applies to read+write equally.
        if matches!(op, Op::Read | Op::Write) {
            if let Some(rule) = self.botignore.matched(path)? {
                return Ok(Decision::Deny(Reason {
                    message: format!("blocked by .botignore: {}", rule.pattern),
                    rule: Some(rule),
                }));
            }
        }

        // 2. botignore.toml namespace-specific rules.
        let (set, patterns) = match op {
            Op::Read => (&self.read_globs, &self.read_patterns),
            Op::Write => (&self.write_globs, &self.write_patterns),
            Op::Execute => return Ok(Decision::Allow), // path-based check_command handles bash
        };

        let rel = path.strip_prefix(&self.root).unwrap_or(path);
        let matches = set.matches(rel);
        if let Some(idx) = matches.first() {
            let pattern = patterns[*idx].clone();
            return Ok(Decision::Deny(Reason {
                message: format!("blocked by botignore.toml [{:?}]: {}", op, pattern),
                rule: Some(Rule {
                    source: self.root.join("botignore.toml"),
                    pattern,
                }),
            }));
        }

        Ok(Decision::Allow)
    }
}

/// Substring-or-glob match of `command` against `patterns`.
/// Patterns containing glob metachars (`*`, `?`, `[`) are treated as globs;
/// others are matched as literal substrings.
fn match_command(command: &str, patterns: &[String]) -> Result<Option<String>, PolicyError> {
    for pat in patterns {
        if is_glob(pat) {
            let g = globset::Glob::new(pat)
                .map_err(|e| PolicyError::BadPattern(e.to_string()))?
                .compile_matcher();
            if g.is_match(command) {
                return Ok(Some(pat.clone()));
            }
        } else if command.contains(pat.as_str()) {
            return Ok(Some(pat.clone()));
        }
    }
    Ok(None)
}

fn is_glob(pat: &str) -> bool {
    pat.contains('*') || pat.contains('?') || pat.contains('[')
}

/// `prefix` is `"name"` or `"name:*"`. Both treat `name` as a leading word
/// boundary in `command`. Mirrors Claude Code's `Bash(name:*)` style.
fn command_matches_prefix(command: &str, prefix: &str) -> bool {
    let needle = prefix.trim_end_matches(":*");
    command.trim_start().starts_with(needle)
}

fn compile_globs(patterns: &[String]) -> Result<(globset::GlobSet, Vec<String>), PolicyError> {
    let mut builder = globset::GlobSetBuilder::new();
    for pat in patterns {
        let glob = globset::Glob::new(pat).map_err(|e| PolicyError::BadPattern(e.to_string()))?;
        builder.add(glob);
    }
    let set = builder
        .build()
        .map_err(|e| PolicyError::BadPattern(e.to_string()))?;
    Ok((set, patterns.to_vec()))
}
