//! Command-line interface definition (clap derive).

use std::path::PathBuf;

use clap::{Args, Parser, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};

#[derive(Parser)]
#[command(
    name = "dependable",
    version,
    about = "Check dependency versions and known vulnerabilities"
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,
}

#[derive(Subcommand)]
pub enum Command {
    /// Check dependencies against the registry and OSV.
    Check(CheckArgs),
    /// List discovered dependencies without checking versions.
    List(ListArgs),
    /// Render the workspace dependency tree (Rust; offline, from Cargo.lock).
    Tree(TreeArgs),
    /// Update versions in place to the latest compatible.
    Fix(FixArgs),
}

impl Cli {
    /// Whether verbose logging was requested on the chosen subcommand.
    #[must_use]
    pub fn verbose(&self) -> bool {
        match &self.command {
            Command::Check(args) => args.verbose,
            Command::List(args) => args.verbose,
            Command::Tree(args) => args.verbose,
            Command::Fix(args) => args.verbose,
        }
    }
}

#[derive(Args)]
pub struct CheckArgs {
    /// Project directory to scan (default: current directory).
    pub path: Option<PathBuf>,
    /// Check a single manifest file instead of discovering them.
    #[arg(long)]
    pub manifest: Option<PathBuf>,
    /// Config file path.
    #[arg(long, default_value = ".dependable.toml")]
    pub config: PathBuf,
    /// Pre-release filter: `exclude` (default), `include-always`, or
    /// `include-if-current`. Overrides `[global] unstable`.
    #[arg(long, value_enum)]
    pub unstable: Option<UnstableFilter>,
    /// Ignore `Cargo.lock`.
    #[arg(long)]
    pub no_lock_file: bool,
    /// Skip vulnerability scanning.
    #[arg(long)]
    pub no_vuln: bool,
    /// Ignore the on-disk registry cache (always fetch fresh).
    #[arg(long)]
    pub no_cache: bool,
    /// Include GHSA advisories in the vulnerability scan.
    #[arg(long)]
    pub include_ghsa: bool,
    /// Output format.
    #[arg(long, value_enum, default_value_t = Format::Table)]
    pub format: Format,
    /// Exit non-zero when results match this level.
    #[arg(long, value_enum, default_value_t = FailOn::None)]
    pub fail_on: FailOn,
    /// How many directories deep to search.
    #[arg(long, default_value_t = 3)]
    pub depth: usize,
    /// Max concurrent HTTP requests (overrides config).
    #[arg(long)]
    pub concurrency: Option<usize>,
    /// Only print errors.
    #[arg(short, long)]
    pub quiet: bool,
    /// Verbose logging (HTTP request details).
    #[arg(short, long)]
    pub verbose: bool,
    /// Restrict to ecosystem(s) (reserved; V1 only checks Rust).
    #[arg(long)]
    pub ecosystem: Option<String>,
}

#[derive(Args)]
pub struct ListArgs {
    pub path: Option<PathBuf>,
    #[arg(long)]
    pub manifest: Option<PathBuf>,
    #[arg(long, value_enum, default_value_t = Format::Table)]
    pub format: Format,
    #[arg(long, default_value_t = 3)]
    pub depth: usize,
    /// Show each crate's available feature flags (Rust only; fetches the
    /// crates.io sparse index, so this makes `list` hit the network).
    #[arg(long)]
    pub features: bool,
    #[arg(short, long)]
    pub verbose: bool,
}

#[derive(Args)]
pub struct TreeArgs {
    /// Project directory to analyze (default: current directory).
    pub path: Option<PathBuf>,
    /// Root the tree at a single crate instead of all workspace members.
    #[arg(short = 'p', long)]
    pub package: Option<String>,
    /// Invert the tree: show what depends on each root (downstream impact).
    #[arg(long)]
    pub invert: bool,
    /// Maximum depth to display (default: unlimited; `0` = roots only).
    #[arg(long)]
    pub depth: Option<usize>,
    /// Show every occurrence of a crate instead of collapsing repeats to `(*)`.
    #[arg(long)]
    pub no_dedupe: bool,
    /// Output format.
    #[arg(long, value_enum, default_value_t = TreeFormat::Tree)]
    pub format: TreeFormat,
    /// Verbose logging.
    #[arg(short, long)]
    pub verbose: bool,
}

#[derive(Args)]
pub struct FixArgs {
    pub path: Option<PathBuf>,
    #[arg(long)]
    pub manifest: Option<PathBuf>,
    #[arg(long, default_value = ".dependable.toml")]
    pub config: PathBuf,
    /// Update all, including beyond the declared constraint.
    #[arg(long)]
    pub all: bool,
    /// Print what would change without writing.
    #[arg(long)]
    pub dry_run: bool,
    #[arg(long, default_value_t = 3)]
    pub depth: usize,
    #[arg(long)]
    pub concurrency: Option<usize>,
    #[arg(short, long)]
    pub verbose: bool,
}

/// Output format.
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum Format {
    Table,
    Json,
    Text,
}

/// Output format for the `tree` command.
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum TreeFormat {
    /// cargo-tree-style ASCII tree (default).
    Tree,
    /// A JSON graph of nodes and edges (for tooling / IDEs).
    Json,
    /// Graphviz DOT for a visual graph (`… --format dot | dot -Tsvg`).
    Dot,
}

/// The result level that triggers a non-zero exit (for CI).
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FailOn {
    None,
    Outdated,
    Vulnerable,
    Any,
}

impl FailOn {
    /// Parse from an environment-variable string (`DEPENDABLE_FAIL_ON`).
    #[must_use]
    pub fn from_env(value: &str) -> Option<Self> {
        match value.trim().to_lowercase().as_str() {
            "none" => Some(FailOn::None),
            "outdated" => Some(FailOn::Outdated),
            "vulnerable" => Some(FailOn::Vulnerable),
            "any" => Some(FailOn::Any),
            _ => None,
        }
    }
}

/// Pre-release filtering mode, selectable via `--unstable` or `[global] unstable`.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum UnstableFilter {
    /// Hide pre-releases (default).
    #[default]
    Exclude,
    /// Always consider pre-releases.
    IncludeAlways,
    /// Consider pre-releases only when the current version is a pre-release.
    IncludeIfCurrent,
}

impl From<UnstableFilter> for dependable_fetch::UnstableFilter {
    fn from(value: UnstableFilter) -> Self {
        match value {
            UnstableFilter::Exclude => dependable_fetch::UnstableFilter::Exclude,
            UnstableFilter::IncludeAlways => dependable_fetch::UnstableFilter::IncludeAlways,
            UnstableFilter::IncludeIfCurrent => dependable_fetch::UnstableFilter::IncludeIfCurrent,
        }
    }
}
