use clap::{Parser, Subcommand};
use tracing::info;
use std::collections::HashMap;

pub mod parser;
pub mod classifier;
pub mod executor;
pub mod audit;

#[derive(Parser)]
#[command(name = "dd0c-run", version, about = "Runbook automation agent")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Execute a runbook
    Run {
        /// Path to runbook file (YAML/Markdown)
        #[arg(short, long)]
        runbook: String,

        /// dd0c SaaS endpoint
        #[arg(long, default_value = "https://api.dd0c.dev")]
        endpoint: String,

        /// API key
        #[arg(long, env = "DD0C_API_KEY")]
        api_key: String,

        /// Dry run (classify only, don't execute)
        #[arg(long)]
        dry_run: bool,

        /// Variable overrides (key=value)
        #[arg(short, long, value_parser = parse_var)]
        var: Vec<(String, String)>,
    },
    /// Classify a single command
    Classify {
        /// Command to classify
        command: String,
    },
    /// Print version
    Version,
}

fn parse_var(s: &str) -> Result<(String, String), String> {
    let pos = s.find('=').ok_or_else(|| format!("invalid KEY=VALUE: no `=` found in `{s}`"))?;
    Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "dd0c_run=info".into()),
        )
        .json()
        .init();

    let cli = Cli::parse();

    match cli.command {
        Commands::Run { runbook, endpoint, api_key, dry_run, var } => {
            info!(runbook = %runbook, dry_run, "Starting runbook execution");

            // Parse runbook YAML
            let content = std::fs::read_to_string(&runbook)
                .map_err(|e| anyhow::anyhow!("Failed to read runbook '{}': {}", runbook, e))?;
            let rb = parser::parse_yaml(&content)?;

            info!(name = %rb.name, steps = rb.steps.len(), "Runbook parsed");

            // Build variable map from CLI args
            let variables: HashMap<String, String> = var.into_iter().collect();

            // Validate required variables
            for (name, spec) in &rb.variables {
                if spec.required && !variables.contains_key(name) && spec.default.is_none() {
                    anyhow::bail!("Required variable '{}' not provided (use --var {}=VALUE)", name, name);
                }
            }

            // Merge defaults
            let mut merged_vars = HashMap::new();
            for (name, spec) in &rb.variables {
                if let Some(val) = variables.get(name) {
                    merged_vars.insert(name.clone(), val.clone());
                } else if let Some(default) = &spec.default {
                    merged_vars.insert(name.clone(), default.clone());
                }
            }

            // Auto-approve callback for read-only commands; reject everything else in dry-run
            let approval_callback = |cmd: &str, safety: classifier::SafetyLevel| -> bool {
                match safety {
                    classifier::SafetyLevel::ReadOnly => {
                        info!(command = %cmd, "Auto-approved (read-only)");
                        true
                    }
                    _ if dry_run => {
                        info!(command = %cmd, safety = ?safety, "Would require approval (dry run)");
                        false
                    }
                    _ => {
                        // In real mode, this would wait for SaaS approval via Redis pub/sub
                        // For now, prompt on stdin
                        eprintln!("\n⚠️  Command requires approval ({:?}):", safety);
                        eprintln!("    {}", cmd);
                        eprint!("  Approve? [y/N] ");
                        let mut input = String::new();
                        std::io::stdin().read_line(&mut input).unwrap_or(0);
                        input.trim().eq_ignore_ascii_case("y")
                    }
                }
            };

            // Execute steps
            let mut failed = false;
            for step in &rb.steps {
                let command = parser::substitute_variables(&step.command, &merged_vars)?;
                let classification = classifier::classify(&command);

                info!(
                    step = step.index,
                    command = %command,
                    safety = ?classification.safety,
                    description = %step.description,
                    "Executing step"
                );

                let result = executor::execute_step(&command, dry_run, &approval_callback).await;

                match result.status {
                    executor::StepStatus::Success => {
                        info!(step = step.index, exit_code = result.exit_code, duration_ms = result.duration_ms, "Step completed");
                    }
                    executor::StepStatus::Skipped => {
                        info!(step = step.index, "Step skipped (dry run)");
                    }
                    executor::StepStatus::Rejected => {
                        info!(step = step.index, "Step rejected");
                        match step.on_failure {
                            parser::FailureAction::Abort => {
                                eprintln!("Step {} rejected — aborting runbook", step.index);
                                failed = true;
                                break;
                            }
                            parser::FailureAction::Continue => {
                                eprintln!("Step {} rejected — continuing", step.index);
                            }
                            parser::FailureAction::Retry { .. } => {
                                eprintln!("Step {} rejected — cannot retry rejection, aborting", step.index);
                                failed = true;
                                break;
                            }
                        }
                    }
                    executor::StepStatus::Failed | executor::StepStatus::TimedOut => {
                        eprintln!("Step {} failed (exit code {})", step.index, result.exit_code);
                        if !result.stderr.is_empty() {
                            eprintln!("  stderr: {}", result.stderr.trim());
                        }
                        match &step.on_failure {
                            parser::FailureAction::Abort => {
                                failed = true;
                                break;
                            }
                            parser::FailureAction::Continue => {}
                            parser::FailureAction::Retry { max_attempts } => {
                                let mut retried = false;
                                for attempt in 1..=*max_attempts {
                                    info!(step = step.index, attempt, "Retrying");
                                    let retry = executor::execute_step(&command, dry_run, &approval_callback).await;
                                    if matches!(retry.status, executor::StepStatus::Success) {
                                        retried = true;
                                        break;
                                    }
                                }
                                if !retried {
                                    eprintln!("Step {} failed after {} retries — aborting", step.index, max_attempts);
                                    failed = true;
                                    break;
                                }
                            }
                        }
                    }
                    _ => {}
                }
            }

            if failed {
                std::process::exit(1);
            }
            info!("Runbook '{}' completed successfully", rb.name);
        }
        Commands::Classify { command } => {
            let result = classifier::classify(&command);
            println!("{}", serde_json::to_string_pretty(&result)?);
        }
        Commands::Version => {
            println!("dd0c/run agent v{}", env!("CARGO_PKG_VERSION"));
        }
    }

    Ok(())
}
