use clap::{Parser, Subcommand, ValueEnum};
use dirigent_fermata::core::{project::find_project_root, Decision, Op, Policy};
use std::io::{Read, Write};
use std::path::PathBuf;
use std::process::ExitCode;

#[derive(Parser)]
#[command(name = "fermata", about = "Harness-agnostic policy gate for AI coding agents")]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// Check whether `path` is allowed for the given `--op`.
    Check {
        #[arg(long, value_enum, default_value_t = OpArg::Read)]
        op: OpArg,
        #[arg(long)]
        json: bool,
        paths: Vec<PathBuf>,
    },
    /// Read a harness hook payload from stdin and render the decision.
    Hook {
        #[arg(long)]
        harness: String,
    },
}

#[derive(Copy, Clone, ValueEnum)]
enum OpArg {
    Read,
    Write,
    Execute,
}

impl From<OpArg> for Op {
    fn from(a: OpArg) -> Self {
        match a {
            OpArg::Read => Op::Read,
            OpArg::Write => Op::Write,
            OpArg::Execute => Op::Execute,
        }
    }
}

fn main() -> ExitCode {
    let cli = Cli::parse();
    match cli.cmd {
        Cmd::Check { op, json, paths } => run_check(op.into(), json, &paths),
        Cmd::Hook { harness } => run_hook(&harness),
    }
}

fn run_check(op: Op, json: bool, paths: &[PathBuf]) -> ExitCode {
    let mut worst: Option<Decision> = None;
    for p in paths {
        let root = match find_project_root(p) {
            Some(r) => r,
            None => continue,
        };
        let policy = match Policy::load(&root) {
            Ok(p) => p,
            Err(e) => {
                eprintln!("fermata: load error: {e}");
                return ExitCode::from(2);
            }
        };
        let d = match policy.check(op, p) {
            Ok(d) => d,
            Err(e) => {
                eprintln!("fermata: check error: {e}");
                return ExitCode::from(2);
            }
        };
        worst = Some(merge_worst(worst.take(), d));
    }
    let decision = worst.unwrap_or(Decision::Allow);
    if json {
        let _ = serde_json::to_writer(std::io::stdout().lock(), &decision);
        let _ = writeln!(std::io::stdout().lock());
    } else if let Decision::Deny(ref r) = decision {
        println!("{}", r.message);
    } else if let Decision::Ask(ref r) = decision {
        println!("ASK: {}", r.message);
    }
    match decision {
        Decision::Allow => ExitCode::from(0),
        Decision::Ask(_) => ExitCode::from(0),
        Decision::Deny(_) => ExitCode::from(1),
    }
}

fn run_hook(harness: &str) -> ExitCode {
    let adapter = match dirigent_fermata::harness::lookup(harness) {
        Some(a) => a,
        None => {
            eprintln!("fermata: unknown harness '{harness}'");
            return ExitCode::from(2);
        }
    };
    let mut buf = Vec::new();
    if let Err(e) = std::io::stdin().lock().read_to_end(&mut buf) {
        eprintln!("fermata: stdin: {e}");
        return ExitCode::from(2);
    }
    let call = match adapter.parse_request(&buf) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("fermata: parse: {e}");
            return ExitCode::from(2);
        }
    };

    use dirigent_fermata::harness::{PathKind, ToolOp};
    let decision = match &call.op {
        ToolOp::Path { path, kind } => {
            let root = match find_project_root(path) {
                // No project root → fail-open allow (hook must always exit 0 with a verdict).
                // run_check silently skips these paths; here we must still emit JSON.
                Some(r) => r,
                None => {
                    let out = adapter.render_decision(&call, &Decision::Allow).unwrap_or_default();
                    let _ = std::io::stdout().lock().write_all(&out);
                    return ExitCode::from(0);
                }
            };
            let policy = match Policy::load(&root) {
                Ok(p) => p,
                Err(e) => {
                    eprintln!("fermata: load error: {e}");
                    let out = adapter.render_decision(&call, &Decision::Allow).unwrap_or_default();
                    let _ = std::io::stdout().lock().write_all(&out);
                    return ExitCode::from(0);
                }
            };
            let op = match kind {
                PathKind::Read => Op::Read,
                PathKind::Write => Op::Write,
            };
            match policy.check(op, path) {
                Ok(d) => d,
                Err(e) => {
                    eprintln!("fermata: check error: {e}");
                    let out = adapter.render_decision(&call, &Decision::Allow).unwrap_or_default();
                    let _ = std::io::stdout().lock().write_all(&out);
                    return ExitCode::from(0);
                }
            }
        }
        ToolOp::Command { text } => {
            // For commands, we look up the project from cwd (no path argument).
            let cwd = match std::env::current_dir() {
                Ok(d) => d,
                Err(e) => {
                    eprintln!("fermata: cwd error: {e}");
                    let out = adapter.render_decision(&call, &Decision::Allow).unwrap_or_default();
                    let _ = std::io::stdout().lock().write_all(&out);
                    return ExitCode::from(0);
                }
            };
            match find_project_root(&cwd) {
                // No project root → fail-open allow (see Path branch note above).
                None => Decision::Allow,
                Some(root) => {
                    let policy = match Policy::load(&root) {
                        Ok(p) => p,
                        Err(e) => {
                            eprintln!("fermata: load error: {e}");
                            let out = adapter.render_decision(&call, &Decision::Allow).unwrap_or_default();
                            let _ = std::io::stdout().lock().write_all(&out);
                            return ExitCode::from(0);
                        }
                    };
                    match policy.check_command(text) {
                        Ok(d) => d,
                        Err(e) => {
                            eprintln!("fermata: check error: {e}");
                            let out = adapter.render_decision(&call, &Decision::Allow).unwrap_or_default();
                            let _ = std::io::stdout().lock().write_all(&out);
                            return ExitCode::from(0);
                        }
                    }
                }
            }
        }
    };
    let out = adapter.render_decision(&call, &decision).unwrap_or_default();
    let _ = std::io::stdout().lock().write_all(&out);
    ExitCode::from(0) // hook bins always exit 0; the JSON carries the verdict
}

fn merge_worst(a: Option<Decision>, b: Decision) -> Decision {
    let rank = |d: &Decision| match d {
        Decision::Allow => 0,
        Decision::Ask(_) => 1,
        Decision::Deny(_) => 2,
    };
    match a {
        None => b,
        Some(a) if rank(&a) >= rank(&b) => a,
        Some(_) => b,
    }
}
