//! Command-line argument definitions using clap derive.

use clap::Parser;

/// Semantic code search — like ripgrep but for meaning.
#[derive(Parser, Clone, Debug)]
#[command(name = "ripvec", version, about)]
#[expect(
    clippy::struct_excessive_bools,
    reason = "CLI flags are naturally boolean"
)]
pub struct Args {
    /// Natural language query to search for.
    ///
    /// Required for one-shot mode; ignored when `--interactive` is set.
    #[arg(default_value = "")]
    pub query: String,

    /// Root directory to search (defaults to current directory).
    #[arg(default_value = ".")]
    pub path: String,

    /// Launch interactive TUI mode (embed once, then search as you type).
    #[arg(short = 'i', long)]
    pub interactive: bool,

    /// Number of results to show (0 = all matches above threshold).
    #[arg(short = 'n', long, default_value_t = 0)]
    pub top_k: usize,

    /// Search engine. Default `ripvec` runs cacheless on CPU using a
    /// composite of Model2Vec 32M bi-encoder, L-12 cross-encoder
    /// rerank, path-enriched BM25, and PageRank. The `modernbert` and
    /// `bert` engines use BERT-family transformers with a persistent
    /// on-disk index. Engine-specific flags (`--index`, `--backend`,
    /// `--rerank-model`, etc.) only apply under their respective
    /// engines; passing one to the wrong engine warns and is ignored.
    #[arg(long, default_value = "ripvec")]
    pub model: ModelArg,

    /// Legacy alias: `--fast` maps to `--model bert`.
    #[arg(long)]
    pub fast: bool,

    /// Legacy alias: `--text` maps to `--model bert`.
    #[arg(long, hide = true)]
    pub text: bool,

    /// Legacy alias: `--modern` maps to `--model modernbert` (the default).
    #[arg(long, hide = true)]
    pub modern: bool,

    /// Override `HuggingFace` model repository (advanced; bypasses `--model`).
    #[arg(long)]
    pub model_repo: Option<String>,

    /// `HuggingFace` repo for the rerank model. Defaults to
    /// `cross-encoder/ms-marco-MiniLM-L-12-v2` (33MB, ~10ms/pair on CPU).
    /// Ripvec engine only; warned-and-ignored on transformer engines.
    #[arg(long)]
    pub rerank_model: Option<String>,

    /// Cap on candidates the reranker sees. Larger = better quality,
    /// linearly more latency. Default 100 is the standard from the
    /// retrieve-then-rerank literature.
    #[arg(long, default_value_t = 100)]
    pub rerank_candidates: usize,

    /// Output format.
    #[arg(short, long, default_value = "color")]
    pub format: OutputFormat,

    /// Filter files by type (e.g., rust, python, js). Uses ripgrep's type definitions.
    #[arg(short = 't', long = "type")]
    pub file_type: Option<String>,

    /// Exclude files by extension during indexing/search walks (comma-separated, e.g. jsonl,md).
    #[arg(long, value_delimiter = ',', value_name = "EXTS")]
    pub exclude_extensions: Vec<String>,

    /// Include only files with these extensions (comma-separated, e.g. py,rs,md).
    /// Empty (default) means "no whitelist" — other filters still apply.
    /// When set, overrides the implicit allow-list from `--scope`.
    #[arg(long, value_delimiter = ',', value_name = "EXTS")]
    pub include_extensions: Vec<String>,

    /// Intent-shaped scope for the search corpus.
    ///
    /// - `code` (no rerank): only code files; skips markdown/rst/txt.
    /// - `docs` (rerank on): only prose; cross-encoder fires on NL queries.
    /// - `all` (default): everything; rerank fires when the indexed
    ///   corpus is at least 30% prose chunks.
    #[arg(long, default_value = "all")]
    pub scope: ScopeArg,

    /// Minimum similarity threshold on normalized [0, 1] scores.
    /// 0.5 = above midpoint of the score range (default, model-agnostic).
    /// 0 = return all results.
    #[arg(short = 'T', long, default_value_t = 0.0)]
    pub threshold: f32,

    /// Number of threads for parallel processing (0 = cores).
    #[arg(short = 'j', long, default_value_t = 0)]
    pub threads: usize,

    /// Inference device: cpu, metal (macOS GPU), or cuda (NVIDIA GPU).
    #[arg(long, default_value = "cpu")]
    pub device: DeviceArg,

    /// Embedding backend implementation (auto = detect all available).
    #[arg(long, default_value = "auto")]
    pub backend: BackendArg,

    /// Batch size for embedding inference (chunks per model forward pass).
    #[arg(short = 'b', long, default_value_t = 64)]
    pub batch_size: usize,

    /// Maximum tokens per chunk fed to the model (0 = unlimited).
    ///
    /// Capping tokens controls inference cost and GPU memory. Tree-sitter
    /// chunks are typically 50-200 tokens; 512 covers virtually all code chunks.
    /// Set to 0 for unlimited (model max, up to 8192 — may OOM on GPU).
    #[arg(long, default_value_t = 512)]
    pub max_tokens: usize,

    /// Maximum chunk size in bytes before splitting into windows.
    #[arg(long, default_value_t = 4096)]
    pub max_chunk_bytes: usize,

    /// Sliding-window size in bytes for the fallback chunker.
    #[arg(long, default_value_t = 2048)]
    pub window_size: usize,

    /// Overlap between adjacent sliding windows in bytes.
    #[arg(long, default_value_t = 512)]
    pub window_overlap: usize,

    /// Treat all files as plain text (sliding-window chunking only).
    /// By default, recognized source files use tree-sitter semantic chunking
    /// and unrecognized files fall back to plain-text windows.
    #[arg(long)]
    pub text_mode: bool,

    /// Search mode: hybrid (semantic + BM25), semantic-only, or keyword-only.
    ///
    /// Hybrid (default) fuses results via Reciprocal Rank Fusion.
    #[arg(long, default_value = "hybrid")]
    pub mode: String,

    /// Enable pipeline profiling output to stderr.
    #[arg(long)]
    pub profile: bool,

    /// Profiling report interval in seconds.
    #[arg(long, default_value_t = 10.0)]
    pub profile_interval: f64,

    /// Write a Chrome trace JSON file (open in `chrome://tracing` or Perfetto).
    #[arg(long, value_name = "FILE")]
    pub trace: Option<String>,

    /// Log level: error, warn, info, debug, trace. Overrides `-v`, `--debug`, and `RIPVEC_LOG`.
    ///
    /// `--debug` is a shortcut for `--log-level debug`.
    #[arg(long, value_name = "LEVEL")]
    pub log_level: Option<String>,

    /// Shortcut for `--log-level debug`. Useful for diagnosing indexing/verify hangs.
    #[arg(long)]
    pub debug: bool,

    /// Increase diagnostic verbosity: -v=info, -vv=debug, -vvv=trace.
    ///
    /// Verbose modes print line-oriented phase diagnostics instead of spinners.
    #[arg(short, long, action = clap::ArgAction::Count)]
    pub verbose: u8,

    /// Write logs to a file instead of stderr (one line per event).
    #[arg(long, value_name = "FILE")]
    pub log_file: Option<String>,

    /// Use persistent index: cache embeddings to disk, re-embed only changed files.
    ///
    /// First run builds the full index. Subsequent runs load from cache and
    /// only re-embed files that changed since last run.
    #[arg(long)]
    pub index: bool,

    /// Force a full rebuild of the persistent index (requires `--index`).
    #[arg(long, requires = "index")]
    pub reindex: bool,

    /// Store the index in `.ripvec/cache/` at the project root (repo-local).
    ///
    /// Creates `.ripvec/config.toml` on first use. The index can then be
    /// committed to git so teammates get instant search without re-embedding.
    /// Requires `--index`.
    #[arg(long, requires = "index")]
    pub repo_level: bool,

    /// Clear the persistent index cache for this project and exit.
    #[arg(long)]
    pub clear_cache: bool,

    /// Override the cache directory (default: `~/.cache/ripvec/`).
    #[arg(long, value_name = "DIR")]
    pub cache_dir: Option<String>,
}

impl Args {
    /// Resolve the active engine, honoring legacy boolean aliases.
    ///
    /// Resolution: `--model` is the canonical flag. The legacy `--fast` /
    /// `--text` boolean flags map to [`ModelArg::Bert`]; `--modern` maps
    /// to [`ModelArg::Modernbert`]. If `--model` was set explicitly to
    /// anything other than the default, it wins; legacy flags are silently
    /// honored only when `--model` is at its default. Conflicts (e.g.
    /// `--model bert --modern`) resolve in favor of `--model` since the
    /// user named the engine explicitly.
    #[must_use]
    pub fn resolved_model(&self) -> ModelArg {
        // Explicit non-default --model always wins.
        if self.model != ModelArg::default() {
            return self.model;
        }
        // Default --model (Ripvec): let legacy flags promote.
        if self.fast || self.text {
            return ModelArg::Bert;
        }
        if self.modern {
            return ModelArg::Modernbert;
        }
        self.model
    }

    /// Resolve the HuggingFace model repo to load, honoring `--model_repo`.
    ///
    /// `--model_repo` is the advanced escape hatch and wins outright.
    /// Otherwise the resolved model family's `default_repo` is returned.
    #[must_use]
    pub fn resolved_model_repo(&self) -> String {
        if let Some(repo) = &self.model_repo {
            return repo.clone();
        }
        self.resolved_model().default_repo().to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn verbose_flag_counts_repeated_occurrences() {
        let args = Args::try_parse_from(["ripvec", "-vvv", "query"]).unwrap();
        assert_eq!(args.verbose, 3);
    }

    #[test]
    fn exclude_extensions_accepts_comma_delimited_values() {
        let args =
            Args::try_parse_from(["ripvec", "--exclude-extensions=jsonl,md,.ipynb", "query"])
                .unwrap();
        assert_eq!(args.exclude_extensions, ["jsonl", "md", ".ipynb"]);
    }

    #[test]
    fn cli_model_parses_bert() {
        let args = Args::try_parse_from(["ripvec", "--model", "bert", "q"]).unwrap();
        assert_eq!(args.model, ModelArg::Bert);
        assert_eq!(args.resolved_model(), ModelArg::Bert);
        assert_eq!(args.resolved_model_repo(), "BAAI/bge-small-en-v1.5");
    }

    #[test]
    fn cli_model_parses_modernbert() {
        let args = Args::try_parse_from(["ripvec", "--model", "modernbert", "q"]).unwrap();
        assert_eq!(args.model, ModelArg::Modernbert);
        assert_eq!(args.resolved_model(), ModelArg::Modernbert);
        assert_eq!(args.resolved_model_repo(), "nomic-ai/modernbert-embed-base");
    }

    #[test]
    fn cli_model_parses_ripvec() {
        let args = Args::try_parse_from(["ripvec", "--model", "ripvec", "q"]).unwrap();
        assert_eq!(args.model, ModelArg::Ripvec);
        assert_eq!(args.resolved_model(), ModelArg::Ripvec);
        assert_eq!(args.resolved_model_repo(), "minishlab/potion-base-32M");
    }

    #[test]
    fn cli_model_defaults_ripvec() {
        let args = Args::try_parse_from(["ripvec", "q"]).unwrap();
        assert_eq!(args.model, ModelArg::Ripvec);
        assert_eq!(args.resolved_model(), ModelArg::Ripvec);
    }

    #[test]
    fn cli_legacy_fast_maps_to_bert() {
        let args = Args::try_parse_from(["ripvec", "--fast", "q"]).unwrap();
        assert_eq!(args.resolved_model(), ModelArg::Bert);
        assert_eq!(args.resolved_model_repo(), "BAAI/bge-small-en-v1.5");
    }

    #[test]
    fn cli_legacy_text_maps_to_bert() {
        let args = Args::try_parse_from(["ripvec", "--text", "q"]).unwrap();
        assert_eq!(args.resolved_model(), ModelArg::Bert);
    }

    #[test]
    fn cli_explicit_model_overrides_legacy_fast() {
        // --model bert + --fast: --model wins (it's explicit).
        let args = Args::try_parse_from(["ripvec", "--model", "bert", "--fast", "q"]).unwrap();
        assert_eq!(args.resolved_model(), ModelArg::Bert);
    }

    #[test]
    fn cli_model_repo_override_wins() {
        let args = Args::try_parse_from(["ripvec", "--model-repo", "custom/repo", "q"]).unwrap();
        assert_eq!(args.resolved_model_repo(), "custom/repo");
    }
}

/// Output format for search results.
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum OutputFormat {
    /// Plain text without color.
    Plain,
    /// JSON output.
    Json,
    /// Colored terminal output (default).
    Color,
}

/// Intent-shaped scope for the search corpus.
///
/// Mirrors [`ripvec_core::embed::Scope`] in the CLI surface. The
/// `--scope` flag accepts these as kebab-case keywords.
#[derive(clap::ValueEnum, Clone, Copy, Debug, Default)]
pub enum ScopeArg {
    /// Only code files. Cross-encoder rerank skipped.
    Code,
    /// Only prose / documentation files. Rerank fires on NL queries.
    Docs,
    /// No extension whitelist. Rerank decided by corpus mix at index
    /// time (>=30% prose -> rerank).
    #[default]
    All,
}

impl From<ScopeArg> for ripvec_core::embed::Scope {
    fn from(value: ScopeArg) -> Self {
        match value {
            ScopeArg::Code => Self::Code,
            ScopeArg::Docs => Self::Docs,
            ScopeArg::All => Self::All,
        }
    }
}

/// Inference device selection.
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum DeviceArg {
    /// CPU inference (default, works everywhere).
    Cpu,
    /// Apple Metal GPU (macOS).
    Metal,
    /// NVIDIA CUDA GPU.
    Cuda,
}

/// Search engine. See `--model` on [`Args`].
///
/// Resolution order at dispatch:
/// 1. `--model_repo` override wins outright (advanced escape hatch).
/// 2. `--model` explicit value.
/// 3. Legacy `--fast`/`--text` boolean → `Bert`.
/// 4. Legacy `--modern` boolean → `Modernbert`.
/// 5. Default: `Ripvec`.
#[derive(clap::ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ModelArg {
    /// Composite engine: Model2Vec 32M bi-encoder + L-12 rerank + BM25
    /// + PageRank. Cacheless and CPU-only. Default.
    #[default]
    Ripvec,
    /// BGE-small-en-v1.5 (384-dim, ClassicBERT, CLS pooling). Transformer.
    Bert,
    /// ModernBERT-embed-base (768-dim, mean pooling). Transformer.
    Modernbert,
}

impl ModelArg {
    /// Default HuggingFace model repo for this engine's bi-encoder.
    ///
    /// Used by [`Args::resolved_model_repo`] when `--model_repo` is unset.
    #[must_use]
    pub fn default_repo(self) -> &'static str {
        match self {
            Self::Ripvec => "minishlab/potion-base-32M",
            Self::Bert => "BAAI/bge-small-en-v1.5",
            Self::Modernbert => "nomic-ai/modernbert-embed-base",
        }
    }
}

/// Embedding backend implementation.
#[derive(clap::ValueEnum, Clone, Debug, Default)]
pub enum BackendArg {
    /// Auto-detect all available backends (default).
    #[default]
    Auto,
    /// CPU (ndarray + system BLAS).
    Cpu,
    /// CUDA (cudarc, NVIDIA GPU via cuBLAS).
    Cuda,
    /// MLX (Apple Silicon, macOS only).
    Mlx,
    /// Metal (Apple Silicon, direct Metal GPU).
    Metal,
}
