uncomment 3.10.2

A CLI tool to remove comments from code using tree-sitter for accurate parsing
use anyhow::Context;
use clap::{Parser, Subcommand};
use std::path::PathBuf;

/// Usage examples and preservation notes shown under `--help`.
const AFTER_LONG_HELP: &str = "Examples:
  uncomment src/                     Remove comments from every file under src/
  uncomment src/ --dry-run --diff    Preview changes as a diff, write nothing
  uncomment main.rs --remove-doc     Also strip doc comments and docstrings
  uncomment . -j 0                   Process the whole tree using all CPU cores
  uncomment init                     Generate a .uncomment.toml for this project
  uncomment scan src/ --only removable  Inventory the comments a run would remove
  uncomment keep src/ --all-removable  Mark every removable comment with ~keep
  uncomment lint src/                  Check TODO/FIXME tags, removing nothing
  uncomment --check src/               Exit 1 if any comment would be removed
  uncomment --check --changed-only --base origin/main .
                                       Gate only the files a branch changed

Preserved by default: TODO, FIXME, HACK, XXX, NOSONAR, the ~keep marker, doc
comments, and common linting directives (eslint-disable, noqa, @ts-ignore, ...).
Override with the flags above or a .uncomment.toml (see `uncomment init`).";

#[derive(Parser, Debug)]
#[command(
    name = "uncomment",
    version,
    about = "Strip comments from source code — accurately, via tree-sitter.",
    long_about = "uncomment removes comments from source code using tree-sitter AST parsing, so it \
                  is 100% accurate and never touches comment-like text inside strings. It preserves \
                  what matters by default — TODO/FIXME, docs, and linting directives — across 300+ \
                  languages, with parallel processing and a safe dry-run mode.",
    styles = crate::ui::clap_styles(),
    after_long_help = AFTER_LONG_HELP
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Commands>,

    #[command(flatten)]
    pub args: ProcessArgs,

    #[command(flatten)]
    pub check: crate::check::CheckArgs,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Initialize a configuration file in the current directory
    #[command(about = "Create a template configuration file")]
    Init {
        /// Output file name
        #[arg(short, long, value_name = "FILE", default_value = crate::config::CONFIG_FILE_NAME)]
        output: PathBuf,

        /// Overwrite existing file
        #[arg(short, long)]
        force: bool,

        /// Spell out every global and pattern option, commented. The `[languages.*]` sections it
        /// writes are examples of overriding a built-in, not the full list of them.
        #[arg(
            long,
            help = "Generate a fully commented config with every global and pattern option"
        )]
        comprehensive: bool,

        /// Interactive mode to select languages
        #[arg(short, long, help = "Interactive mode to select languages and options")]
        interactive: bool,
    },

    /// Inventory every comment and the verdict a real run would reach
    #[command(
        about = "Report every comment with the verdict a run would reach",
        long_about = "Reports every comment a run would see — removed and preserved alike, with the \
                      reason each one is preserved — as JSONL, JSON or text, and writes no source \
                      file. Each record carries an id that survives edits elsewhere in the file, so \
                      the report can be filtered and handed to `uncomment keep`. \
                      `--group-identical` collapses repeated comments into one record per distinct \
                      text, which is what makes a large repository decidable."
    )]
    Scan(crate::scan::command::ScanArgs),

    /// Write `~keep` markers into the comments a decision selected
    #[command(
        about = "Apply ~keep markers to selected comments",
        long_about = "Writes a decision taken over a comment inventory back into the source: a line \
                      comment gets ` ~keep` appended, a block, doc or docstring comment gets a plain \
                      marker line directly above it. Select comments by id, by substring, or take \
                      every comment a default run would remove."
    )]
    Keep(crate::keep::KeepArgs),

    /// Check tag comments (TODO, FIXME, ...) against a configured convention
    #[command(
        about = "Lint tag comments — TODO, FIXME, HACK, XXX — removing nothing",
        long_about = "Checks tag comments against the convention configured under `[lint]`: that the \
                      tag is the canonical one, that it carries an issue key, and that the key is not \
                      the issue the current branch is working on — that one closes when the branch \
                      merges, which would leave the TODO pointing at a dead ticket. Removes nothing, \
                      and exits 1 when anything failed, so it works as its own pre-commit hook."
    )]
    Lint(crate::lint::LintArgs),
}

#[derive(Parser, Debug)]
pub struct ProcessArgs {
    /// Files or directories to process (supports glob patterns)
    #[arg(value_name = "PATH", help = "Files, directories, or glob patterns to process")]
    pub paths: Vec<String>,

    /// Remove TODO comments (normally preserved)
    #[arg(
        short = 'r',
        long,
        help = "Remove TODO comments (normally preserved)",
        help_heading = "Comment selection"
    )]
    pub remove_todo: bool,

    /// Remove FIXME comments (normally preserved)
    #[arg(
        short = 'f',
        long,
        help = "Remove FIXME comments (normally preserved)",
        help_heading = "Comment selection"
    )]
    pub remove_fixme: bool,

    /// Remove documentation comments (normally preserved)
    #[arg(
        short = 'd',
        long,
        help = "Remove documentation comments and docstrings",
        help_heading = "Comment selection"
    )]
    pub remove_doc: bool,

    /// Additional patterns to preserve (beyond defaults)
    #[arg(
        short = 'i',
        long = "ignore",
        value_name = "PATTERN",
        help = "Additional patterns to preserve (can be used multiple times)",
        help_heading = "Comment selection"
    )]
    pub ignore_patterns: Vec<String>,

    /// Disable automatic preservation of linting directives
    #[arg(
        long = "no-default-ignores",
        help = "Disable built-in preservation patterns (ESLint, Clippy, etc.)",
        help_heading = "Comment selection"
    )]
    pub no_default_ignores: bool,

    /// Show what would be changed without modifying files
    #[arg(
        short = 'n',
        long,
        help = "Show changes without modifying files",
        help_heading = "Output"
    )]
    pub dry_run: bool,

    /// Show line-by-line diffs of removed comments
    #[arg(
        long = "diff",
        help = "Show a diff of the removed comments for each modified file",
        help_heading = "Output"
    )]
    pub diff: bool,

    /// Show detailed processing information
    #[arg(
        short = 'v',
        long,
        help = "Show detailed processing information (per-comment previews)",
        help_heading = "Output"
    )]
    pub verbose: bool,

    /// Suppress per-file output; print only the summary and errors
    #[arg(
        short = 'q',
        long,
        help = "Suppress per-file output; print only the summary and errors",
        help_heading = "Output",
        conflicts_with = "verbose"
    )]
    pub quiet: bool,

    /// Paths never collected, as globs, on top of `[global] exclude`
    #[arg(
        long = "exclude",
        value_name = "GLOB",
        help = "Skip paths matching GLOB (can be used multiple times)",
        help_heading = "File selection"
    )]
    pub exclude: Vec<String>,

    /// Ignore .gitignore rules when finding files
    #[arg(
        long = "no-gitignore",
        help = "Process files ignored by .gitignore",
        help_heading = "File selection"
    )]
    pub no_gitignore: bool,

    /// Process files in nested git repositories
    #[arg(
        long = "traverse-git-repos",
        help = "Traverse into other git repositories (useful for monorepos)",
        help_heading = "File selection"
    )]
    pub traverse_git_repos: bool,

    /// Number of parallel threads (0 = number of CPU cores)
    #[arg(
        short = 'j',
        long = "threads",
        value_name = "N",
        help = "Number of parallel threads (0 = auto-detect)",
        default_value = "1",
        help_heading = "Performance"
    )]
    pub threads: usize,

    /// Path to configuration file
    #[arg(
        short = 'c',
        long = "config",
        value_name = "FILE",
        help = "Path to configuration file (overrides automatic discovery)",
        help_heading = "File selection"
    )]
    pub config: Option<PathBuf>,
}

impl ProcessArgs {
    pub fn processing_options(&self) -> crate::processor::ProcessingOptions {
        crate::processor::ProcessingOptions {
            remove_todo: self.remove_todo,
            remove_fixme: self.remove_fixme,
            remove_doc: self.remove_doc,
            custom_preserve_patterns: self.ignore_patterns.clone(),
            use_default_ignores: !self.no_default_ignores,
            dry_run: self.dry_run,
            show_diff: self.diff,
            respect_gitignore: !self.no_gitignore,
            traverse_git_repos: self.traverse_git_repos,
        }
    }
}

impl Cli {
    /// Handle the init command
    pub fn handle_init_command(
        output: &PathBuf,
        force: bool,
        comprehensive: bool,
        interactive: bool,
    ) -> anyhow::Result<()> {
        if output.exists() && !force {
            return Err(anyhow::anyhow!(
                "Configuration file already exists: {}. Use --force to overwrite.",
                output.display()
            ));
        }

        let (template, detected_info) = if comprehensive {
            (crate::config::Config::comprehensive_template_clean(), None)
        } else if interactive {
            (crate::config::Config::interactive_template_clean()?, None)
        } else {
            let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
            let (template, info) = crate::config::Config::smart_template_with_info(&current_dir)?;
            (template, Some(info))
        };

        std::fs::write(output, template)
            .with_context(|| format!("Failed to write configuration file to {}", output.display()))?;

        use crate::ui;
        anstream::println!(
            "{} {} {}",
            ui::success(ui::CHECK),
            ui::success("Created configuration file:"),
            ui::path(output)
        );

        if comprehensive {
            anstream::println!(
                "{} Generated comprehensive config with 15+ language configurations",
                ui::dim(ui::BULLET)
            );
        } else if interactive {
            anstream::println!(
                "{} Generated customized config based on your selections",
                ui::dim(ui::BULLET)
            );
        } else if let Some(info) = detected_info {
            if !info.detected_languages.is_empty() {
                anstream::println!(
                    "{} Detected {} file types in your project:",
                    ui::dim(ui::BULLET),
                    ui::accent(info.detected_languages.len())
                );
                for (lang, count) in &info.detected_languages {
                    anstream::println!("    {}", ui::dim(format!("{count} ({lang} files)")));
                }
                anstream::println!(
                    "{} Configured {} languages with appropriate settings",
                    ui::dim(ui::BULLET),
                    ui::accent(info.configured_languages)
                );
            } else {
                anstream::println!(
                    "{} No supported files detected, generated basic template",
                    ui::dim(ui::BULLET)
                );
            }
            if info.total_files > 0 {
                anstream::println!(
                    "{} Scanned {} files total",
                    ui::dim(ui::BULLET),
                    ui::accent(info.total_files)
                );
            }
        } else {
            anstream::println!(
                "{} Generated smart config based on detected files in your project",
                ui::dim(ui::BULLET)
            );
        }

        Ok(())
    }
}