use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use sentio_cli::render_human_report;
use sentio_core::{RuleRegistry, ScanOptions, Scanner};
use std::io::{self, IsTerminal};

mod telemetry;

fn main() {
    let exit_code = match run() {
        Ok(code) => code,
        Err(err) => {
            eprintln!("{err}");
            2
        }
    };

    std::process::exit(exit_code);
}

fn run() -> Result<i32> {
    let cli = Cli::parse();

    // `--version`/`-V` is handled manually (instead of clap's built-in flag)
    // so it goes through the same version-check ping as `sentio version`.
    if cli.version {
        return render_version();
    }

    let Some(command) = cli.command else {
        Cli::command()
            .error(
                clap::error::ErrorKind::MissingRequiredArgument,
                "a subcommand is required (try `sentio --help`)",
            )
            .exit();
    };

    match command {
        Commands::Scan {
            path,
            format,
            rule,
            include_tests,
            output,
        } => {
            if output.is_some() && !matches!(format, OutputFormat::Json) {
                anyhow::bail!("--output requires --format json");
            }

            let scanner = Scanner::new();
            let options = ScanOptions {
                include_tests,
                rule_filter: rule,
            };
            let result = scanner.scan_path(&path, &options);

            match format {
                OutputFormat::Human => render_human(&result),
                OutputFormat::Json => {
                    if let Some(ref file_path) = output {
                        let json = serde_json::to_string_pretty(&result)?;
                        std::fs::write(file_path, &json)?;
                        eprintln!("Report written to {file_path}");
                    } else {
                        render_json(&result)?;
                    }
                }
            }

            Ok(if !result.parse_failures.is_empty() {
                2
            } else if result.findings.is_empty() {
                0
            } else {
                1
            })
        }
        Commands::Rules {
            command: RulesCommands::List,
        } => render_rule_list(),
        Commands::Version => render_version(),
    }
}

fn render_version() -> Result<i32> {
    let installed = env!("CARGO_PKG_VERSION");
    println!("sentio {installed}");

    let check = telemetry::check_version(installed);
    if let Some(latest) = check.latest {
        if latest != installed {
            println!(
                "A newer version is available: {latest} (run `cargo install sentio-cli --force`)"
            );
        }
    }

    Ok(0)
}

fn render_rule_list() -> Result<i32> {
    let registry = RuleRegistry::baseline();
    for rule in registry.all() {
        let meta = rule.metadata();
        println!("{}  {}", meta.id, meta.title);
    }
    Ok(0)
}

fn render_human(result: &sentio_core::ScanResult) {
    let registry = RuleRegistry::baseline();
    let stdout = io::stdout();
    let use_color = stdout.is_terminal();
    let mut locked = stdout.lock();
    let _ = render_human_report(result, &registry, &mut locked, use_color);
}

fn render_json(result: &sentio_core::ScanResult) -> Result<()> {
    println!("{}", serde_json::to_string_pretty(result)?);
    Ok(())
}

#[derive(Debug, Parser)]
#[command(name = "sentio")]
#[command(author = "Sentio Security")]
#[command(disable_version_flag = true)]
#[command(about = "AST-based security scanner for Solana/Anchor programs")]
#[command(
    long_about = "sentio scans Rust source files in Solana programs for common vulnerability\n\
patterns using syn — Rust's macro-safe AST parser. It understands\n\
Anchor account constraints, instruction logic, and CPI call graphs to produce\n\
high-signal findings with minimal false positives.\n\n\
Exit codes:\n  \
0  No findings\n  \
1  One or more findings\n  \
2  Parse error in one or more files"
)]
struct Cli {
    /// Print the installed sentio version and check for updates
    #[arg(short = 'V', long = "version")]
    version: bool,

    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// Scan a Solana program directory or file for vulnerabilities
    Scan {
        /// Path to the program directory or Rust source file to scan
        #[arg(default_value = ".", value_name = "PATH")]
        path: String,

        /// Output format
        #[arg(long, value_enum, default_value_t = OutputFormat::Human, value_name = "FORMAT")]
        format: OutputFormat,

        /// Run only a specific rule (e.g. SW001, SW003). Run `sentio rules list` to see all rules.
        #[arg(long, value_name = "RULE_ID")]
        rule: Option<String>,

        /// Include test files in the scan (excluded by default to reduce noise)
        #[arg(long)]
        include_tests: bool,

        /// Write JSON output to a file instead of stdout (requires --format json)
        #[arg(long, value_name = "FILE")]
        output: Option<String>,
    },
    /// Manage and inspect the built-in rule set
    Rules {
        #[command(subcommand)]
        command: RulesCommands,
    },
    /// Print the installed sentio version and check for updates
    Version,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
enum OutputFormat {
    /// Human-readable output with source excerpts and remediation guidance (default)
    Human,
    /// Machine-readable JSON — useful for CI pipelines and tooling integrations
    Json,
}

#[derive(Debug, Subcommand)]
enum RulesCommands {
    /// Print all available rules with their IDs and titles
    List,
}
