"""Argus Nano CLI — secrets detection from the command line."""

from __future__ import annotations

import cmd
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
from collections import Counter
from dataclasses import asdict
from datetime import datetime
from pathlib import Path

# Ensure UTF-8 output on Windows for box-drawing characters.
if sys.platform == "win32" and not os.environ.get("PYTHONIOENCODING"):
    try:
        sys.stdout.reconfigure(encoding="utf-8")
        sys.stderr.reconfigure(encoding="utf-8")
    except (AttributeError, OSError):
        pass

import click

from argus_nano import __version__

# -- Severity colors for click.style -------------------------------------------

_SEV_COLORS = {
    "critical": "red",
    "high": "yellow",
    "medium": "cyan",
    "low": "white",
}


# -- Formatting helpers --------------------------------------------------------


def _format_finding(r) -> str:
    """Format a single ScanResult as a colored terminal line."""
    sev = click.style(
        f"{r.severity:<10}",
        fg=_SEV_COLORS.get(r.severity, "white"),
        bold=r.severity == "critical",
    )
    det_type = getattr(r, "detection_type", "format")
    key_name = getattr(r, "key_name", "")
    if det_type == "semantic" and key_name:
        tag = click.style(f"[semantic: {key_name}]", fg="magenta")
    else:
        tag = f"[{det_type}]"
    return f"  {sev} {r.file_path}:{r.line_number}  {r.provider:<16} {r.value_masked}  {tag}"


def _format_summary_box(
    results: list,
    elapsed: float | None = None,
    file_count: int | None = None,
    repository: str | None = None,
) -> str:
    """Build a Unicode box-drawing summary of scan results."""
    W = 48  # inner width (visible characters)

    def row(plain: str, styled: str | None = None) -> str:
        """Build a row. *plain* is used for padding, *styled* for display."""
        display = styled if styled is not None else plain
        pad = W - 2 - len(plain)
        if pad < 0:
            pad = 0
        return f"  ║  {display}{' ' * pad}║"

    def sep() -> str:
        return f"  ╠{'═' * W}╣"

    lines: list[str] = []
    lines.append(f"  ╔{'═' * W}╗")

    title = "Argus Nano Scan Results"
    lines.append(row(title, click.style(title, bold=True)))
    lines.append(sep())

    # Repository info for remote scans
    if repository:
        lines.append(row(f"Repository: {repository} (remote)"))

    # Overview stats
    if file_count is not None:
        lines.append(row(f"Files scanned:      {file_count:,}"))
    files_with = len({r.file_path for r in results})
    lines.append(row(f"Files with secrets: {files_with:,}"))
    lines.append(row(f"Total findings:     {len(results):,}"))
    if elapsed is not None:
        lines.append(row(f"Scan time:          {elapsed:.1f}s"))

    # By severity
    sev_counts = Counter(r.severity for r in results)
    if sev_counts:
        lines.append(sep())
        lines.append(row("By severity:"))
        for sev in ("critical", "high", "medium", "low"):
            c = sev_counts.get(sev, 0)
            if c > 0:
                name = sev.title()
                plain = f"  {name:<12} {c}"
                # Pad name first, then colorize
                padded_name = f"{name:<12}"
                styled = f"  {click.style(padded_name, fg=_SEV_COLORS[sev])} {c}"
                lines.append(row(plain, styled))

    # By provider (top 10)
    prov_counts = Counter(r.provider for r in results)
    if prov_counts:
        lines.append(sep())
        lines.append(row("By provider:"))
        for prov, c in prov_counts.most_common(10):
            lines.append(row(f"  {prov:<14} {c}"))
        if len(prov_counts) > 10:
            lines.append(row(f"  ... and {len(prov_counts) - 10} more"))

    lines.append(f"  ╚{'═' * W}╝")
    return "\n".join(lines)


def _write_output_file(results: list, output_path: str) -> None:
    """Write results to a file, format determined by extension."""
    ext = Path(output_path).suffix.lower()
    if ext == ".json":
        content = json.dumps([asdict(r) for r in results], indent=2)
    elif ext == ".sarif":
        content = json.dumps(_to_sarif(results), indent=2)
    else:
        out_lines = []
        for r in results:
            out_lines.append(f"{r.severity:<10} {r.file_path}:{r.line_number}  {r.provider:<16} {r.value_masked}")
        if results:
            out_lines.append("")
            out_lines.append(f"Total: {len(results)} findings")
        content = "\n".join(out_lines)
    Path(output_path).write_text(content, encoding="utf-8")


def _write_report(
    results: list,
    report_path: str,
    target: str,
    file_count: int | None,
    elapsed: float | None,
) -> None:
    """Write a redacted findings report (text or CSV)."""
    ext = Path(report_path).suffix.lower()
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    if ext == ".csv":
        lines = ["severity,file,line,detection_type,provider,masked_value,key_name"]
        for r in results:
            key = getattr(r, "key_name", "")
            det = getattr(r, "detection_type", "format")
            lines.append(f"{r.severity},{r.file_path},{r.line_number},{det},{r.provider},{r.value_masked},{key}")
    else:
        lines = [
            "# Argus Nano Scan Report",
            f"# Date: {now}",
            f"# Target: {target}",
        ]
        if file_count is not None:
            lines.append(f"# Files scanned: {file_count}")
        lines.append(f"# Findings: {len(results)}")
        if elapsed is not None:
            lines.append(f"# Scan time: {elapsed:.1f}s")
        lines.append("SEVERITY\tFILE:LINE\tTYPE\tMASKED_VALUE\tKEY_NAME")
        for r in results:
            det = getattr(r, "detection_type", "format")
            key = getattr(r, "key_name", "")
            lines.append(f"{r.severity}\t{r.file_path}:{r.line_number}\t{det}\t{r.value_masked}\t{key}")

    Path(report_path).write_text("\n".join(lines) + "\n", encoding="utf-8")


def _resolve_output_path(filename: str) -> str:
    """Route bare filenames to the output/ directory."""
    p = Path(filename)
    if p.parent == Path("."):
        out_dir = Path("output")
        out_dir.mkdir(exist_ok=True)
        return str(out_dir / p.name)
    return filename


# -- Remote scanning helpers ---------------------------------------------------


def _is_remote_url(target: str) -> bool:
    """Check if target looks like a git remote URL."""
    return target.startswith(("https://", "http://", "git@"))


def _clone_repo(
    url: str,
    branch: str | None = None,
    yes: bool = False,
) -> Path:
    """Clone a remote repo to a temp directory. Returns temp_dir path.

    Raises click.Abort on user cancel, SystemExit on errors.
    """
    if shutil.which("git") is None:
        click.echo(
            click.style("Error: ", fg="red", bold=True) + "git is required for remote scanning.\n"
            "  Install from https://git-scm.com",
            err=True,
        )
        sys.exit(2)

    tmp = Path(tempfile.mkdtemp(prefix="argus-nano-"))
    branch_display = branch or "default"

    if not yes:
        click.echo()
        click.echo(click.style("  Argus Nano — Remote Scan", bold=True))
        click.echo(f"  Repository: {url}")
        click.echo(f"  Branch: {branch_display}")
        click.echo(f"  Temp directory: {tmp}")
        click.echo()
        if not click.confirm("  Clone and scan?", default=True):
            shutil.rmtree(tmp, ignore_errors=True)
            raise click.Abort()

    cmd = ["git", "clone", "--depth", "1", "--single-branch"]
    if branch:
        cmd.extend(["--branch", branch])
    cmd.extend([url, str(tmp)])

    click.echo("  Cloning... ", nl=False)
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    except subprocess.TimeoutExpired:
        shutil.rmtree(tmp, ignore_errors=True)
        click.echo("failed.")
        click.echo("Error: git clone timed out after 120s", err=True)
        sys.exit(2)

    if result.returncode != 0:
        shutil.rmtree(tmp, ignore_errors=True)
        click.echo("failed.")
        stderr = result.stderr.strip()
        if "Authentication" in stderr or "could not read" in stderr:
            click.echo(
                click.style("Error: ", fg="red", bold=True) + "authentication required. Clone the repo locally first:\n"
                f"  git clone {url}",
                err=True,
            )
        else:
            click.echo(
                click.style("Error: ", fg="red", bold=True) + f"git clone failed:\n  {stderr}",
                err=True,
            )
        sys.exit(2)

    click.echo("done.")
    return tmp


def _cleanup_temp(tmp: Path) -> None:
    """Remove temp directory and confirm."""
    click.echo("  Cleaning up temp directory... ", nl=False)
    shutil.rmtree(tmp, ignore_errors=True)
    click.echo("done.")
    click.echo(f"  Removed: {tmp}")


# -- CLI Commands --------------------------------------------------------------


@click.group(invoke_without_command=True)
@click.version_option(version=__version__, prog_name="argus-nano")
@click.pass_context
def main(ctx):
    """Argus Nano — secrets detection that runs on your machine."""
    if ctx.invoked_subcommand is not None:
        return

    # No subcommand given — check if bare args look like a scan target.
    # click consumes known subcommands, so remaining args are in sys.argv.
    args = sys.argv[1:]
    # Strip flags we own (--version handled above).
    positional = [a for a in args if not a.startswith("-")]

    if positional:
        # Forward to scan: e.g. `argus-nano .` → `argus-nano scan .`
        ctx.invoke(scan, path=positional[0])
        return

    # No args at all — show friendly welcome.
    click.echo()
    click.echo(click.style(f"  Argus Nano v{__version__}", bold=True) + " — secrets detection on your machine.")
    click.echo()
    click.echo("  Quick start:")
    click.echo(click.style("    argus-nano scan .             ", fg="cyan") + " Scan current directory")
    click.echo(click.style("    argus-nano scan ./my-repo     ", fg="cyan") + " Scan a local path")
    click.echo(click.style("    argus-nano scan <github-url>  ", fg="cyan") + " Clone and scan a remote repo")
    click.echo(click.style('    argus-nano check "sk_live_..." ', fg="cyan") + " Check a single value")
    click.echo(click.style("    argus-nano repl               ", fg="cyan") + " Interactive mode")
    click.echo(click.style("    argus-nano info               ", fg="cyan") + " Show version and pattern info")
    click.echo()
    click.echo("  Run " + click.style("argus-nano <command> --help", bold=True) + " for detailed options.")
    click.echo()


@main.command()
@click.argument("path")
@click.option("--patterns-only", is_flag=True, help="Use regex patterns only, skip ML model.")
@click.option("--threshold", type=float, default=0.5, help="Confidence threshold for classification.")
@click.option("--format", "output_format", type=click.Choice(["text", "json", "sarif"]), default="text")
@click.option("--severity", type=click.Choice(["critical", "high", "medium", "low"]), default=None)
@click.option("--exclude", multiple=True, help="Glob patterns to exclude.")
@click.option("--verbose", "-v", is_flag=True, help="Show every finding inline.")
@click.option("--output", "-o", "output_file", default=None, help="Write full results to file (.json, .sarif, .txt).")
@click.option("--report", "-r", "report_file", default=None, help="Write redacted findings report (.txt or .csv).")
@click.option("--no-summary", is_flag=True, help="Show all findings inline, skip summary box.")
@click.option(
    "--no-semantic", is_flag=True, default=False, help="Disable semantic detection (key name context analysis)."
)
@click.option(
    "--config",
    "-c",
    "config_only",
    is_flag=True,
    default=False,
    help="Only scan config files (.env, .yml, .json, .toml, .tf, etc.).",
)
@click.option("--branch", default=None, help="Branch to clone for remote scanning.")
@click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation prompts (for CI/automation).")
@click.option("--jobs", "-j", type=int, default=None, help="Max worker threads for file scanning (default: all cores).")
def scan(
    path,
    patterns_only,
    threshold,
    output_format,
    severity,
    exclude,
    verbose,
    output_file,
    report_file,
    no_summary,
    no_semantic,
    config_only,
    branch,
    yes,
    jobs,
):
    """Scan a file, directory, or remote repository for secrets."""
    from argus_nano.scanner import Scanner, ScanResult

    # -- Remote URL detection and clone --
    remote_url = None
    temp_dir = None

    if _is_remote_url(path):
        temp_dir = _clone_repo(path, branch=branch, yes=yes)
        remote_url = path
        target = temp_dir
    else:
        target = Path(path)
        if not target.exists():
            click.echo(f"Error: path not found: {path}", err=True)
            sys.exit(2)

    is_text = output_format == "text"

    if is_text:
        click.echo("  Loading scanner... ", nl=False)
    try:
        scanner = Scanner(
            patterns_only=patterns_only,
            threshold=threshold,
            semantic=not no_semantic,
            config=config_only,
            jobs=jobs,
        )
    except Exception as exc:
        if is_text:
            click.echo("failed.")
        if temp_dir:
            _cleanup_temp(temp_dir)
        click.echo(f"Error initializing scanner: {exc}", err=True)
        sys.exit(2)
    if is_text:
        click.echo("done.")

    file_count = None
    _file_total = [0]

    def _progress(stage, completed, total):
        if not is_text:
            return
        if stage == "walk":
            click.echo("  Collecting files... ", nl=False)
        elif stage == "files":
            if _file_total[0] == 0 and total > 0:
                click.echo("done.")
            _file_total[0] = total
            msg = f"\r  Scanning files: {completed:,}/{total:,}"
            click.echo(msg, nl=False)
        else:
            msg = f"\r  Classifying candidates: {completed:,}/{total:,}"
            click.echo(msg, nl=False)

    exit_code = 0
    try:
        start = time.monotonic()
        try:
            if target.is_file():
                results = scanner.scan_file(target)
                file_count = 1
            else:
                results = scanner.scan_directory(
                    target,
                    exclude=list(exclude),
                    progress_fn=_progress,
                )
                file_count = _file_total[0] or None
        except Exception as exc:
            click.echo(f"\nError scanning: {exc}", err=True)
            sys.exit(2)
        elapsed = time.monotonic() - start

        # Clear progress line
        if is_text and file_count and file_count > 1:
            click.echo("\r" + " " * 60 + "\r", nl=False)

        # Rewrite file paths for remote scans
        display_target = str(path)
        if remote_url and temp_dir:
            tmp_str = str(temp_dir)
            results = [
                ScanResult(**{**asdict(r), "file_path": r.file_path.replace(tmp_str, remote_url)}) for r in results
            ]

        # Filter by severity
        if severity:
            results = [r for r in results if r.severity == severity]

        # Write report if requested
        if report_file:
            report_file = _resolve_output_path(report_file)
            _write_report(results, report_file, display_target, file_count, elapsed)
            if is_text:
                click.echo(f"  Report written to {report_file}")

        # -- JSON / SARIF: pipe-friendly, no decoration --
        if output_format == "json":
            click.echo(json.dumps([asdict(r) for r in results], indent=2))
            exit_code = 1 if results else 0
        elif output_format == "sarif":
            click.echo(json.dumps(_to_sarif(results), indent=2))
            exit_code = 1 if results else 0
        else:
            # -- Text output with smart summary --
            if output_file:
                output_file = _resolve_output_path(output_file)
                _write_output_file(results, output_file)
                click.echo(f"  Results written to {output_file}")

            if not results:
                click.echo()
                click.echo(click.style("  No secrets found.", fg="green", bold=True))
                click.echo()
                if file_count is not None and file_count > 1:
                    click.echo(f"  Scanned {file_count:,} files in {elapsed:.1f}s")
                    click.echo()
                exit_code = 0
            else:
                show_inline = verbose or no_summary or (len(results) <= 10 and not output_file)
                if show_inline:
                    click.echo()
                    for r in results:
                        click.echo(_format_finding(r))
                    click.echo()

                if not no_summary:
                    repo = remote_url if remote_url else None
                    click.echo(
                        _format_summary_box(
                            results,
                            elapsed=elapsed,
                            file_count=file_count,
                            repository=repo,
                        )
                    )
                    click.echo()

                if not show_inline and not output_file:
                    click.echo("  Use --verbose to see all findings")
                    click.echo("  Use --output <file> to write full results to file")
                    click.echo()

                exit_code = 1

    finally:
        if temp_dir is not None:
            _cleanup_temp(temp_dir)

    sys.exit(exit_code)


@main.command()
@click.argument("value")
@click.option("--context", default=None, help="Surrounding code context.")
def check(value, context):
    """Check a single string for secret patterns."""
    from argus_nano.scanner import Scanner

    scanner = Scanner(patterns_only=True)
    results = scanner.check(value, context=context)

    if not results:
        click.echo("No secret patterns detected.")
        sys.exit(0)

    for r in results:
        click.echo(f"{r.severity:<10} {r.provider:<16} {r.pattern_id}  {r.value_masked}")
    sys.exit(1)


@main.command()
def info():
    """Show model version and pattern count."""
    from argus_nano.patterns import PatternRegistry

    registry = PatternRegistry()
    click.echo(f"Argus Nano v{__version__}")
    click.echo(f"Patterns: {registry.pattern_count} across {registry.provider_count} providers")

    try:
        from argus_nano.model import OnnxModel

        model_path = OnnxModel._default_model_path()
        if model_path.exists():
            size_mb = model_path.stat().st_size / (1024 * 1024)
            click.echo(f"Model: {model_path} ({size_mb:.1f} MB)")
        else:
            click.echo("Model: not found (use --patterns-only mode)")
    except Exception:
        click.echo("Model: not available (install onnxruntime for ML mode)")


@main.command(name="list-providers")
def list_providers():
    """List all supported provider patterns."""
    from argus_nano.patterns import PatternRegistry

    registry = PatternRegistry()

    by_provider: dict[str, list[dict]] = {}
    for pat in registry.providers:
        by_provider.setdefault(pat["provider"], []).append(pat)

    for provider in sorted(by_provider):
        patterns = by_provider[provider]
        click.echo(f"\n  {provider} ({len(patterns)} patterns)")
        for pat in patterns:
            click.echo(f"    {pat['severity']:<10} {pat['id']:<40} {pat['name']}")


# -- Interactive REPL ----------------------------------------------------------


class ArgusREPL(cmd.Cmd):
    """Interactive secrets scanning shell."""

    intro = click.style(
        f"\n  Argus Nano v{__version__} — Interactive Mode\n  Type 'help' for available commands.\n",
        bold=True,
    )
    prompt = click.style("argus> ", fg="blue", bold=True)

    def __init__(self):
        super().__init__()
        from argus_nano.scanner import Scanner

        self._Scanner = Scanner
        self._mode = "full"
        self._threshold = 0.5
        self._severity_filter = None
        self._semantic = True
        self._config = False
        self._scanner = None
        self._last_results = []
        self._last_elapsed = 0.0
        self._last_file_count = 0
        self._init_scanner()

    def _init_scanner(self):
        try:
            self._scanner = self._Scanner(
                patterns_only=(self._mode == "patterns-only"),
                threshold=self._threshold,
                semantic=self._semantic,
                config=self._config,
            )
            if self._scanner._model is not None:
                prov = self._scanner._model.provider
                click.echo(f"  Scanner ready: {self._mode} mode ({prov})")
            else:
                click.echo("  Scanner ready: patterns-only mode")
        except Exception as exc:
            click.echo(click.style(f"  Error initializing scanner: {exc}", fg="red"))

    def _apply_severity(self, results):
        if self._severity_filter:
            return [r for r in results if r.severity == self._severity_filter]
        return results

    def _parse_args(self, arg: str) -> tuple[str, dict]:
        """Parse a REPL argument string into (positional, flags).

        Returns (path_str, options_dict).
        """
        import shlex

        try:
            tokens = shlex.split(arg, posix=(sys.platform != "win32"))
        except ValueError:
            tokens = arg.strip().split()

        opts: dict = {
            "patterns_only": False,
            "severity": None,
            "verbose": False,
            "exclude": [],
            "no_semantic": False,
            "config": False,
            "output": None,
            "report": None,
            "format": "text",
            "no_summary": False,
            "jobs": None,
        }
        positional = []
        i = 0
        while i < len(tokens):
            tok = tokens[i]
            if tok in ("--patterns-only", "-p"):
                opts["patterns_only"] = True
            elif tok == "--no-semantic":
                opts["no_semantic"] = True
            elif tok in ("--config", "-c"):
                opts["config"] = True
            elif tok in ("--verbose", "-v"):
                opts["verbose"] = True
            elif tok == "--no-summary":
                opts["no_summary"] = True
            elif tok == "--severity" and i + 1 < len(tokens):
                i += 1
                opts["severity"] = tokens[i]
            elif tok == "--exclude" and i + 1 < len(tokens):
                i += 1
                opts["exclude"].append(tokens[i])
            elif tok in ("--output", "-o") and i + 1 < len(tokens):
                i += 1
                opts["output"] = tokens[i]
            elif tok in ("--report", "-r"):
                if i + 1 < len(tokens) and not tokens[i + 1].startswith("-"):
                    i += 1
                    opts["report"] = tokens[i]
                else:
                    opts["report"] = True  # signal: prompt interactively
            elif tok == "--format" and i + 1 < len(tokens):
                i += 1
                if tokens[i] in ("json", "sarif", "text"):
                    opts["format"] = tokens[i]
                else:
                    click.echo(f"  Unknown format: {tokens[i]} (use json, sarif, or text)")
            elif tok in ("--jobs", "-j") and i + 1 < len(tokens):
                i += 1
                try:
                    opts["jobs"] = int(tokens[i])
                except ValueError:
                    click.echo(f"  Invalid --jobs value: {tokens[i]} (must be integer)")
            elif not tok.startswith("--") and not tok.startswith("-"):
                positional.append(tok)
            else:
                click.echo(f"  Unknown flag: {tok} (type 'h' for help)")
            i += 1
        path = " ".join(positional)
        return path, opts

    def do_scan(self, arg):
        """scan <path|url> [options]

        Scan a file, directory, or remote repo URL for secrets.

        Options:
          -p, --patterns-only      Regex only, skip ML model
          -c, --config             Only scan config files
          -v, --verbose            Show every finding inline
          -o, --output <file>      Write full results to file
              --report <file>      Write redacted report (.txt or .csv)
              --format <fmt>       Output format: text, json, sarif
              --severity <level>   Filter by severity level
              --no-semantic        Disable key name analysis
              --no-summary         Skip summary box
              --exclude <glob>     Exclude files matching glob
          -j, --jobs <N>       Max worker threads (default: all cores)
        """
        from dataclasses import asdict as _asdict

        from argus_nano.scanner import ScanResult

        path, opts = self._parse_args(arg)
        if not path:
            click.echo(
                "  Usage: scan <path|url> [-p] [-c] [-v] [-j N] [-o file] [-r file] [--format fmt] [--severity lvl]"
            )
            return

        # Remote URL handling
        remote_url = None
        temp_dir = None
        if _is_remote_url(path):
            try:
                temp_dir = _clone_repo(path, yes=True)
            except (click.Abort, SystemExit):
                return
            remote_url = path
            target = temp_dir
        else:
            target = Path(path)
            if not target.exists():
                click.echo(click.style(f"  Path not found: {path}", fg="red"))
                return

        # Build scanner — use temp if flags differ from defaults
        scanner = self._scanner
        want_semantic = self._semantic and not opts["no_semantic"]
        want_config = self._config or opts["config"]
        need_po = opts["patterns_only"] and self._mode != "patterns-only"
        need_no_sem = opts["no_semantic"] and self._semantic
        need_config = opts["config"] and not self._config
        need_jobs = opts["jobs"] is not None
        if need_po or need_no_sem or need_config or need_jobs:
            scanner = self._Scanner(
                patterns_only=need_po or (self._mode == "patterns-only"),
                threshold=self._threshold,
                semantic=want_semantic,
                config=want_config,
                jobs=opts["jobs"],
            )

        _file_total = [0]

        def _progress(stage, completed, total):
            if stage == "walk":
                click.echo("  Collecting files... ", nl=False)
            elif stage == "files":
                if _file_total[0] == 0 and total > 0:
                    click.echo("done.")
                _file_total[0] = max(_file_total[0], total)
                msg = f"\r  Scanning files: {completed:,}/{total:,}"
                click.echo(msg, nl=False)
            else:
                msg = f"\r  Classifying candidates: {completed:,}/{total:,}"
                click.echo(msg, nl=False)

        try:
            start = time.monotonic()
            try:
                if target.is_file():
                    results = scanner.scan_file(target)
                    self._last_file_count = 1
                else:
                    exclude = opts["exclude"]
                    results = scanner.scan_directory(
                        target,
                        exclude=exclude,
                        progress_fn=_progress,
                    )
                    self._last_file_count = _file_total[0]
            except KeyboardInterrupt:
                click.echo("\n  Scan cancelled.")
                return
            except Exception as exc:
                click.echo(click.style(f"\n  Error: {exc}", fg="red"))
                return
            self._last_elapsed = time.monotonic() - start

            # Clear progress line
            if self._last_file_count > 1:
                click.echo("\r" + " " * 60 + "\r", nl=False)

            # Rewrite file paths for remote scans
            if remote_url and temp_dir:
                tmp_str = str(temp_dir)
                results = [
                    ScanResult(**{**_asdict(r), "file_path": r.file_path.replace(tmp_str, remote_url)}) for r in results
                ]

            # Apply severity filter (from flag or global setting)
            sev = opts["severity"] or self._severity_filter
            if sev:
                results = [r for r in results if r.severity == sev]
            self._last_results = results

            # Write report if requested (interactive prompts in REPL)
            report_path = opts["report"]
            if report_path is not None:
                if report_path is True:
                    # -r with no filename: prompt for one
                    report_path = input("  Report filename: ").strip()
                    if not report_path:
                        click.echo("  Skipping report.")
                        report_path = None
                if report_path and Path(report_path).suffix.lower() not in (".txt", ".csv"):
                    click.echo("  Report format:")
                    click.echo("    1) TXT  (tab-separated, human-readable)")
                    click.echo("    2) CSV  (spreadsheet-friendly)")
                    choice = input("  Choose [1/2]: ").strip()
                    if choice == "2":
                        report_path = report_path + ".csv"
                    else:
                        report_path = report_path + ".txt"
                if report_path:
                    report_path = _resolve_output_path(report_path)
                    _write_report(results, report_path, str(path), self._last_file_count, self._last_elapsed)
                    click.echo(f"  Report written to {report_path}")

            # Write output file if requested
            if opts["output"]:
                opts["output"] = _resolve_output_path(opts["output"])
                _write_output_file(results, opts["output"])
                click.echo(f"  Results written to {opts['output']}")

            # JSON / SARIF output
            if opts["format"] == "json":
                click.echo(json.dumps([_asdict(r) for r in results], indent=2))
                return
            elif opts["format"] == "sarif":
                click.echo(json.dumps(_to_sarif(results), indent=2))
                return

            # Text output
            if not results:
                click.echo(click.style("  No secrets found.", fg="green", bold=True))
                if self._last_file_count > 1:
                    click.echo(f"  Scanned {self._last_file_count:,} files in {self._last_elapsed:.1f}s")
                return

            show_inline = opts["verbose"] or opts["no_summary"] or len(results) <= 10
            if show_inline:
                click.echo()
                for r in results:
                    click.echo(_format_finding(r))
            click.echo()
            if not opts["no_summary"]:
                repo = remote_url if remote_url else None
                click.echo(
                    _format_summary_box(
                        results,
                        elapsed=self._last_elapsed,
                        file_count=self._last_file_count,
                        repository=repo,
                    )
                )
            if not show_inline:
                click.echo("  Use -v to see all findings, -o <file> to save")

        finally:
            if temp_dir is not None:
                _cleanup_temp(temp_dir)

    def do_inspect(self, arg):
        """inspect <file>  — Show per-line annotated view of a single file."""
        path = arg.strip()
        if not path:
            click.echo("  Usage: inspect <file>")
            return
        target = Path(path)
        if not target.is_file():
            click.echo(click.style(f"  Not a file: {path}", fg="red"))
            return

        try:
            text = target.read_text(encoding="utf-8", errors="replace")
        except (OSError, PermissionError) as exc:
            click.echo(click.style(f"  Error reading: {exc}", fg="red"))
            return

        lines = text.splitlines()
        registry = self._scanner.registry

        # Gather regex matches per line
        line_matches: dict[int, list] = {}
        for line_num_0, line in enumerate(lines):
            line_num = line_num_0 + 1
            matches = registry.scan_line(line, line_num)
            if matches:
                line_matches[line_num] = matches

        secrets_count = 0
        benign_count = 0
        clean_count = 0

        click.echo()
        for line_num_0, line in enumerate(lines):
            line_num = line_num_0 + 1
            matches = line_matches.get(line_num, [])

            if not matches:
                clean_count += 1
                if len(lines) <= 50:
                    prefix = click.style(".", fg="green")
                    label = click.style("clean", fg="green")
                    display = line[:70].rstrip()
                    click.echo(f"  Line {line_num:<4} {prefix} {label:<12} {display}")
                continue

            for m in matches:
                if registry.is_benign(m.value):
                    benign_count += 1
                    prefix = click.style(".", fg="cyan")
                    label = click.style("benign", fg="cyan")
                    click.echo(f"  Line {line_num:<4} {prefix} {label:<12} {m.value[:40]:<40}  ({m.pattern_id})")
                else:
                    is_secret = True
                    if self._scanner._model is not None:
                        from argus_nano.scanner import _build_context

                        ctx = _build_context(lines, line_num_0, str(target))
                        label_id, conf = self._scanner._model.predict(ctx)
                        is_secret = label_id == 1 and conf >= self._threshold

                    if is_secret:
                        secrets_count += 1
                        sev_upper = m.severity.upper()
                        prefix = click.style(
                            "X",
                            fg=_SEV_COLORS.get(m.severity, "red"),
                            bold=True,
                        )
                        sev_styled = click.style(
                            f"{sev_upper:<10}",
                            fg=_SEV_COLORS.get(m.severity, "red"),
                        )
                        from argus_nano.scanner import _mask_value

                        masked = _mask_value(m.value)
                        click.echo(f"  Line {line_num:<4} {prefix} {sev_styled} {masked:<40}  ({m.pattern_id})")
                    else:
                        benign_count += 1
                        prefix = click.style(".", fg="cyan")
                        label = click.style("benign(ML)", fg="cyan")
                        click.echo(f"  Line {line_num:<4} {prefix} {label:<12} {m.value[:40]:<40}  ({m.pattern_id})")

        click.echo()
        secrets_str = click.style(
            str(secrets_count),
            fg="red" if secrets_count else "green",
        )
        click.echo(
            f"  {len(lines)} lines analyzed, {secrets_str} secrets found, {benign_count} benign, {clean_count} clean"
        )

    def do_check(self, arg):
        """check <string>  — Check if a string matches secret patterns."""
        value = arg.strip()
        if not value:
            click.echo("  Usage: check <string>")
            return

        results = self._scanner.check(value)
        if not results:
            click.echo(click.style("  No secret patterns detected.", fg="green"))
            return

        for r in results:
            sev = click.style(
                f"{r.severity:<10}",
                fg=_SEV_COLORS.get(r.severity, "white"),
            )
            click.echo(f"  {sev} {r.provider:<16} {r.pattern_id}  {r.value_masked}")

    def do_set(self, arg):
        """set <option> <value>  — Change scanner settings.

        Options:
          set mode full|patterns-only
          set threshold 0.0-1.0
          set severity critical|high|medium|low|all
          set semantic on|off
          set config on|off
        """
        parts = arg.strip().split(None, 1)
        if len(parts) < 2:
            click.echo("  Usage: set <option> <value>")
            click.echo("  Options: mode, threshold, severity, semantic, config")
            return

        key, val = parts[0].lower(), parts[1].strip().lower()

        if key == "mode":
            if val not in ("full", "patterns-only"):
                click.echo("  Valid modes: full, patterns-only")
                return
            self._mode = val
            self._init_scanner()
        elif key == "threshold":
            try:
                t = float(val)
                if not 0.0 <= t <= 1.0:
                    raise ValueError
                self._threshold = t
                click.echo(f"  Threshold set to {t}")
                self._init_scanner()
            except ValueError:
                click.echo("  Threshold must be a float between 0.0 and 1.0")
        elif key == "severity":
            if val == "all":
                self._severity_filter = None
                click.echo("  Severity filter: all (no filter)")
            elif val in ("critical", "high", "medium", "low"):
                self._severity_filter = val
                click.echo(f"  Severity filter: {val}")
            else:
                click.echo("  Valid severities: critical, high, medium, low, all")
        elif key == "semantic":
            if val in ("on", "true", "1"):
                self._semantic = True
                click.echo("  Semantic detection: on")
                self._init_scanner()
            elif val in ("off", "false", "0"):
                self._semantic = False
                click.echo("  Semantic detection: off")
                self._init_scanner()
            else:
                click.echo("  Valid values: on, off")
        elif key == "config":
            if val in ("on", "true", "1"):
                self._config = True
                click.echo("  Config-only mode: on")
                self._init_scanner()
            elif val in ("off", "false", "0"):
                self._config = False
                click.echo("  Config-only mode: off")
                self._init_scanner()
            else:
                click.echo("  Valid values: on, off")
        else:
            click.echo(f"  Unknown option: {key}")
            click.echo("  Options: mode, threshold, severity, semantic, config")

    def do_results(self, _arg):
        """results  — Show last scan results again."""
        if not self._last_results:
            click.echo("  No previous scan results.")
            return
        click.echo()
        for r in self._last_results:
            click.echo(_format_finding(r))
        click.echo()
        click.echo(
            _format_summary_box(
                self._last_results,
                elapsed=self._last_elapsed,
                file_count=self._last_file_count,
            )
        )

    def do_export(self, arg):
        """export <file>  — Export last results to file (.json, .sarif, .txt)."""
        path = arg.strip()
        if not path:
            click.echo("  Usage: export <file>")
            return
        if not self._last_results:
            click.echo("  No results to export. Run a scan first.")
            return
        path = _resolve_output_path(path)
        try:
            _write_output_file(self._last_results, path)
            click.echo(f"  Exported {len(self._last_results)} findings to {path}")
        except Exception as exc:
            click.echo(click.style(f"  Error writing: {exc}", fg="red"))

    def do_stats(self, _arg):
        """stats  — Show stats from last scan."""
        if not self._last_results:
            click.echo("  No previous scan results.")
            return
        click.echo(
            _format_summary_box(
                self._last_results,
                elapsed=self._last_elapsed,
                file_count=self._last_file_count,
            )
        )

    def do_providers(self, _arg):
        """providers  — List supported providers."""
        registry = self._scanner.registry
        by_provider: dict[str, list] = {}
        for pat in registry.providers:
            by_provider.setdefault(pat["provider"], []).append(pat)
        click.echo()
        for provider in sorted(by_provider):
            click.echo(f"  {provider} ({len(by_provider[provider])} patterns)")
        click.echo(f"\n  {registry.pattern_count} patterns across {registry.provider_count} providers")

    # -- Command aliases (short forms) --
    def do_s(self, arg):
        """Alias for scan."""
        self.do_scan(arg)

    def do_i(self, arg):
        """Alias for inspect."""
        self.do_inspect(arg)

    def do_c(self, arg):
        """Alias for check."""
        self.do_check(arg)

    def do_r(self, _arg):
        """Alias for results."""
        self.do_results(_arg)

    def do_e(self, arg):
        """Alias for export."""
        self.do_export(arg)

    def do_p(self, _arg):
        """Alias for providers."""
        self.do_providers(_arg)

    def do_q(self, _arg):
        """Alias for quit."""
        return self.do_quit(_arg)

    def do_h(self, _arg):
        """Alias for help."""
        self.do_help("")

    # -- Help / quit --
    def do_help(self, arg):
        """help  — Show available commands."""
        if arg:
            # Delegate to built-in for specific command help
            super().do_help(arg)
            return
        click.echo()
        click.echo(click.style("  Argus Nano REPL", bold=True))
        click.echo()
        click.echo("  Commands (short form in parentheses):")
        click.echo("    scan   (s)  <path|url> [flags]   Scan for secrets")
        click.echo("    inspect(i)  <file>               Annotated line-by-line view")
        click.echo("    check  (c)  <string>             Check a single string")
        click.echo("    set         <option> <value>      Change settings")
        click.echo("    results(r)                        Show last scan results")
        click.echo("    export (e)  <file>                Export results to file")
        click.echo("    stats                             Show summary from last scan")
        click.echo("    providers(p)                      List supported providers")
        click.echo("    help   (h)  [command]             Show this help")
        click.echo("    quit   (q)                        Exit")
        click.echo()
        click.echo("  Scan flags:")
        click.echo("    -p  --patterns-only    -c  --config       -v  --verbose")
        click.echo("    -o  --output <file>    --report <file>    --format json|sarif")
        click.echo("    --severity <level>     --no-semantic      --no-summary")
        click.echo("    --exclude <glob>       -j  --jobs <N>")
        click.echo()
        click.echo("  Settings (via 'set'):")
        click.echo("    mode full|patterns-only   threshold 0.0-1.0   severity <level>|all")
        click.echo("    semantic on|off            config on|off")
        click.echo()

    def do_quit(self, _arg):
        """quit  — Exit interactive mode."""
        click.echo("  Goodbye.")
        return True

    def do_exit(self, _arg):
        """exit  — Exit interactive mode."""
        return self.do_quit(_arg)

    def do_EOF(self, _arg):
        click.echo()
        return self.do_quit(_arg)

    def emptyline(self):
        pass

    def default(self, line):
        click.echo(f"  Unknown command: {line.split()[0]}")
        click.echo("  Type 'help' or 'h' for available commands.")


@main.command("repl")
def repl_cmd():
    """Open interactive scanning shell."""
    repl = ArgusREPL()
    try:
        repl.cmdloop()
    except KeyboardInterrupt:
        click.echo("\n  Goodbye.")


# -- SARIF output ---------------------------------------------------------------


def _to_sarif(results) -> dict:
    """Convert results to SARIF v2.1.0 format."""
    sarif_results = []
    for r in results:
        sarif_results.append(
            {
                "ruleId": r.pattern_id,
                "level": "error" if r.severity in ("critical", "high") else "warning",
                "message": {
                    "text": f"Potential {r.provider} secret detected: {r.value_masked}",
                },
                "locations": [
                    {
                        "physicalLocation": {
                            "artifactLocation": {"uri": r.file_path},
                            "region": {"startLine": r.line_number},
                        }
                    }
                ],
                "properties": {
                    "provider": r.provider,
                    "severity": r.severity,
                    "confidence": r.confidence,
                },
            }
        )

    return {
        "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
        "version": "2.1.0",
        "runs": [
            {
                "tool": {
                    "driver": {
                        "name": "argus-nano",
                        "version": __version__,
                        "informationUri": "https://github.com/arc-commander/argus-nano",
                    }
                },
                "results": sarif_results,
            }
        ],
    }


if __name__ == "__main__":
    main()
