"""CLI entry point for mcpaudit.

Usage:
    python -m mcpaudit /path/to/server.py
    python -m mcpaudit /path/to/server.py --format json
    python -m mcpaudit /path/to/server.py --format html --output report.html
    python -m mcpaudit /path/to/dir/ --severity high
    python -m mcpaudit --init
    mcpaudit guard /path/to/config.json
    mcpaudit diff scan1.json scan2.json
    mcpaudit flows /path/to/server.py
    mcpaudit plugins list --plugin-dir ./my_plugins/
    mcpaudit live -- python server.py
    mcpaudit /path/to/server.py  (if installed via pip)
"""

from __future__ import annotations

import argparse
import fnmatch
import sys
from pathlib import Path
from typing import Sequence

from . import __version__
from .config import AuditConfig, create_default_config, load_config
from .formatters import format_summary_json, format_summary_text
from .models import Severity
from .rules import Rule, get_all_rules
from .scanner import MCPSecurityScanner


def build_parser() -> argparse.ArgumentParser:
    """Build the CLI argument parser."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit",
        description="MCP Security Audit Scanner — Static security analysis for MCP servers",
    )
    parser.add_argument(
        "path",
        nargs="?",
        default=None,
        help="Python/TypeScript/JavaScript file or directory to scan",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html", "sarif"],
        default=None,
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--severity", "-s",
        choices=["critical", "high", "medium", "low", "info"],
        default=None,
        help="Minimum severity to report",
    )
    parser.add_argument(
        "--output", "-o",
        help="Output file path (default: stdout)",
    )
    parser.add_argument(
        "--recursive", "-r",
        action="store_true",
        help="Scan directory recursively (default for directories)",
    )
    parser.add_argument(
        "--config", "-c",
        default=None,
        help="Path to .mcpaudit.yml config file",
    )
    parser.add_argument(
        "--init",
        action="store_true",
        help="Create a default .mcpaudit.yml configuration file",
    )
    parser.add_argument(
        "--list-rules",
        action="store_true",
        help="List all available security rules and exit",
    )
    parser.add_argument(
        "--quiet", "-q",
        action="store_true",
        help="Suppress output, exit code only (0=clean, 1=high, 2=critical)",
    )
    parser.add_argument(
        "--explain",
        metavar="RULE_ID",
        default=None,
        help="Show detailed explanation of a rule (e.g. --explain CMD-001)",
    )
    parser.add_argument(
        "--exclude",
        default=None,
        help="Comma-separated rule IDs to skip (e.g. --exclude AUTH-001,RATE-001)",
    )
    parser.add_argument(
        "--baseline",
        default=None,
        help="Path to baseline file — suppress known findings",
    )
    parser.add_argument(
        "--create-baseline",
        default=None,
        metavar="FILE",
        help="Scan and save all findings as a baseline file",
    )
    parser.add_argument(
        "--taxonomy",
        action="store_true",
        help="Show findings grouped by SAFE-MCP threat category",
    )
    parser.add_argument(
        "--remediation",
        action="store_true",
        help="Show auto-remediation suggestions for each finding",
    )
    parser.add_argument(
        "--save-history",
        action="store_true",
        help="Save scan results to history for trend tracking",
    )
    parser.add_argument(
        "--history",
        action="store_true",
        help="Show scan history and trends",
    )
    parser.add_argument(
        "--history-dashboard",
        metavar="FILE",
        default=None,
        help="Generate HTML dashboard from scan history (e.g. --history-dashboard report.html)",
    )
    parser.add_argument(
        "--history-dir",
        default=None,
        help="Directory for scan history files (default: .mcpaudit-history/)",
    )
    parser.add_argument(
        "--history-label",
        default="",
        help="Label for this scan in history (e.g. 'ci-main', 'pre-release')",
    )
    parser.add_argument(
        "--watch",
        action="store_true",
        help="Watch mode: monitor files for changes and re-scan incrementally",
    )
    parser.add_argument(
        "--watch-interval",
        type=float,
        default=2.0,
        help="Polling interval in seconds for --watch mode (default: 2.0)",
    )
    parser.add_argument(
        "--fix",
        action="store_true",
        help="Apply auto-fix suggestions for supported rules",
    )
    parser.add_argument(
        "--fix-dry-run",
        action="store_true",
        help="Show auto-fix diffs without applying them",
    )
    parser.add_argument(
        "--compliance-report",
        nargs="?",
        const="compliance-report.html",
        default=None,
        metavar="FILE",
        help=(
            "Generate OWASP Agentic Top 10 compliance report as HTML "
            "(default: compliance-report.html)"
        ),
    )
    parser.add_argument(
        "--deobfuscate",
        action="store_true",
        default=True,
        help="Enable deobfuscation layer (default: on)",
    )
    parser.add_argument(
        "--no-deobfuscate",
        action="store_true",
        default=False,
        help="Disable deobfuscation layer",
    )
    parser.add_argument(
        "--min-score",
        type=int,
        default=None,
        metavar="N",
        help="Minimum trust score (0-100). Exit code 1 if score < N (for CI/CD gates)",
    )
    parser.add_argument(
        "--plugin-dir",
        default=None,
        help="Directory containing custom plugin rules (.py files)",
    )
    parser.add_argument(
        "--policy",
        default=None,
        metavar="FILE",
        help="Path to policy YAML file for organizational enforcement",
    )
    parser.add_argument(
        "--dashboard",
        default=None,
        metavar="FILE",
        help="Generate self-contained HTML summary dashboard (e.g. --dashboard report.html)",
    )
    parser.add_argument(
        "--profile",
        default=None,
        choices=["strict", "standard", "ci", "development", "owasp-agentic", "mcp-only"],
        help=(
            "Compliance profile: strict (all rules, fail on any), "
            "standard (production, >= MEDIUM), ci (fast, >= HIGH, JSON), "
            "development (relaxed, no MCP rules), owasp-agentic, mcp-only"
        ),
    )
    parser.add_argument(
        "--list-profiles",
        action="store_true",
        help="List all available compliance profiles and exit",
    )
    parser.add_argument(
        "--fix-stats",
        action="store_true",
        help="Show auto-fix coverage statistics after scan",
    )
    parser.add_argument(
        "--version", "-V",
        action="version",
        version=f"mcpaudit {__version__}",
    )
    return parser


def _filter_rules(rules: list[Rule], config: AuditConfig) -> list[Rule]:
    """Filter rules based on config exclude list.

    Args:
        rules: All available rules.
        config: Parsed configuration.

    Returns:
        Filtered list of rules.
    """
    if not config.exclude_rules:
        return rules
    return [r for r in rules if r.rule_id not in config.exclude_rules]


def _is_excluded_path(filepath: Path, exclude_patterns: list[str]) -> bool:
    """Check if a file path matches any exclusion pattern.

    Args:
        filepath: Path to check.
        exclude_patterns: Glob patterns to match against.

    Returns:
        True if the path should be excluded.
    """
    path_str = str(filepath)
    for pattern in exclude_patterns:
        if fnmatch.fnmatch(path_str, pattern):
            return True
        if fnmatch.fnmatch(filepath.name, pattern):
            return True
    return False


def _handle_guard(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit guard' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit guard",
        description="MCP Config Guard — Scan MCP configuration files for security issues",
    )
    parser.add_argument(
        "path",
        help="Path to MCP config file (e.g. claude_desktop_config.json)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "sarif"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .guard import ConfigGuard, format_guard_json, format_guard_sarif, format_guard_text

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    guard = ConfigGuard()
    result = guard.scan_config(target)

    if args.format == "json":
        output = format_guard_json(result)
    elif args.format == "sarif":
        output = format_guard_sarif(result)
    else:
        output = format_guard_text(result)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return result.exit_code


def _handle_diff(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit diff' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit diff",
        description="MCP Audit Diff — Compare two scan results",
    )
    parser.add_argument(
        "file1",
        help="First scan result JSON file (before)",
    )
    parser.add_argument(
        "file2",
        help="Second scan result JSON file (after)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "sarif"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .diff import ScanDiff, format_diff_json, format_diff_sarif, format_diff_text

    before = Path(args.file1)
    after = Path(args.file2)

    if not before.exists():
        print(f"Error: {before} does not exist", file=sys.stderr)
        return 1
    if not after.exists():
        print(f"Error: {after} does not exist", file=sys.stderr)
        return 1

    differ = ScanDiff()
    result = differ.diff_files(before, after)

    if args.format == "json":
        output = format_diff_json(result)
    elif args.format == "sarif":
        output = format_diff_sarif(result)
    else:
        output = format_diff_text(result)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return result.exit_code


def _handle_flows(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit flows' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit flows",
        description="MCP Flow Analysis — Detect toxic data flows in MCP servers",
    )
    parser.add_argument(
        "path",
        help="Python file or directory to analyze",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .flows import FlowAnalyzer, format_flows_json, format_flows_text

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    analyzer = FlowAnalyzer()
    if target.is_dir():
        result = analyzer.analyze_directory(target)
    else:
        result = analyzer.analyze_file(target)

    if args.format == "json":
        output = format_flows_json(result)
    else:
        output = format_flows_text(result)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return result.exit_code


def _handle_plugins(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit plugins' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit plugins",
        description="MCP Audit Plugins — List and manage custom plugin rules",
    )
    parser.add_argument(
        "action",
        choices=["list"],
        help="Action to perform (list: show loaded plugins)",
    )
    parser.add_argument(
        "--plugin-dir",
        required=True,
        help="Directory containing custom plugin rules (.py files)",
    )
    args = parser.parse_args(argv)

    from .plugins import PluginRegistry

    plugin_dir = Path(args.plugin_dir)
    if not plugin_dir.exists():
        print(f"Error: plugin directory not found: {plugin_dir}", file=sys.stderr)
        return 1

    registry = PluginRegistry()
    registry.load_directory(plugin_dir)
    print(registry.format_list())
    return 0


def _handle_live(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit live' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit live",
        description="MCP Live Scanner — Runtime vulnerability probing for MCP servers",
    )
    parser.add_argument(
        "command",
        nargs="*",
        help="Command to start the MCP server (e.g. python server.py)",
    )
    parser.add_argument(
        "--timeout",
        type=int,
        default=30,
        help="Timeout in seconds for each probe (default: 30)",
    )
    parser.add_argument(
        "--probes",
        action="store_true",
        help="List available probes and exit",
    )
    parser.add_argument(
        "--probe",
        action="append",
        default=None,
        help="Run specific probe(s) by ID (can be repeated)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .live import (
        MCPLiveScanner,
        format_live_json,
        format_live_text,
        get_all_probes,
    )

    # --probes: list available probes
    if args.probes:
        probes = get_all_probes()
        print(f"\n{'ID':<12} {'Severity':<10} {'Category':<20} {'Name'}")
        print(f"{'─' * 12} {'─' * 10} {'─' * 20} {'─' * 40}")
        for p in probes:
            print(f"  {p.probe_id:<10} {p.severity:<10} {p.category:<20} {p.name}")
        print(f"\n{len(probes)} probes available")
        return 0

    if not args.command:
        parser.error("command is required (e.g. mcpaudit live -- python server.py)")

    scanner = MCPLiveScanner(
        command=args.command,
        timeout=args.timeout,
        probe_ids=args.probe,
    )
    result = scanner.run()

    if args.format == "json":
        output = format_live_json(result)
    else:
        output = format_live_text(result)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return 0 if result.probes_failed == 0 else 1


def _handle_scan_all(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit scan-all' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit scan-all",
        description="MCP Fleet Scanner — Scan all MCP servers in a directory",
    )
    parser.add_argument(
        "path",
        help="Directory to scan recursively for MCP servers",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    parser.add_argument(
        "--fleet-report",
        default=None,
        metavar="FILE",
        help="Generate executive HTML fleet report (e.g. --fleet-report report.html)",
    )
    args = parser.parse_args(argv)

    from .multi import MultiScanner, format_fleet_html, format_fleet_json, format_fleet_text

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    ms = MultiScanner()
    report = ms.scan_all(target)

    # Executive fleet report (Feature 3: v1.1.0)
    if args.fleet_report:
        from .fleet_report import generate_fleet_report
        scan_results = [
            srv["scan_result"] for srv in report.servers
            if srv.get("scan_result") is not None
        ]
        html_output = generate_fleet_report(scan_results)
        Path(args.fleet_report).write_text(html_output, encoding="utf-8")
        print(f"Fleet executive report written to {args.fleet_report}")

    if args.format == "json":
        output = format_fleet_json(report)
    elif args.format == "html":
        output = format_fleet_html(report)
    else:
        output = format_fleet_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    # Exit code based on fleet grade
    from .multi import FleetGrade
    if report.fleet_grade in (FleetGrade.F, FleetGrade.D):
        return 2
    if report.fleet_grade in (FleetGrade.C,):
        return 1
    return 0


def _handle_probes(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit probes' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit probes",
        description="MCP Injection Probes — List and filter injection probe payloads",
    )
    parser.add_argument(
        "action",
        choices=["list"],
        help="Action to perform (list: show all probes)",
    )
    parser.add_argument(
        "--probes-category",
        default=None,
        help=(
            "Filter by category: CMD_INJECTION, PATH_TRAVERSAL, SQL_INJECTION, "
            "PROMPT_INJECTION, SSRF, DESERIALIZATION, XSS_OUTPUT"
        ),
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .probes import (
        ALL_CATEGORIES,
        get_all_probes as get_injection_probes,
        get_probes_by_category,
    )

    if args.probes_category:
        cat = args.probes_category.lower()
        probes = get_probes_by_category(cat)
        if not probes:
            print(
                f"Error: unknown category '{args.probes_category}'",
                file=sys.stderr,
            )
            print(
                f"Available: {', '.join(c.upper() for c in ALL_CATEGORIES)}",
                file=sys.stderr,
            )
            return 1
    else:
        probes = get_injection_probes()

    if args.format == "json":
        import json as _json
        output = _json.dumps(
            [p.to_dict() for p in probes],
            indent=2,
            ensure_ascii=False,
        )
    else:
        lines: list[str] = []
        lines.append("")
        lines.append(
            f"{'Category':<20} {'Severity':<10} {'CWE':<10} {'Description'}"
        )
        lines.append(
            f"{'─' * 20} {'─' * 10} {'─' * 10} {'─' * 40}"
        )
        for p in probes:
            lines.append(
                f"  {p.category:<18} {p.severity:<10} {p.cwe:<10} "
                f"{p.description}"
            )
        lines.append("")
        lines.append(f"{len(probes)} injection probes available")
        lines.append("")
        output = "\n".join(lines)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_summary(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit summary' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit summary",
        description="MCP Audit Summary — Aggregate stats and executive summary",
    )
    parser.add_argument(
        "--history-dir",
        default=None,
        help="Directory with scan history files (default: .mcpaudit-history/)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .history import load_history
    from .summary import (
        ScanResult as _SR,
        build_summary,
        format_summary_report_json,
        format_summary_report_text,
    )
    from .models import Finding, ScanResult, Severity

    history_dir = Path(args.history_dir) if args.history_dir else None
    entries = load_history(history_dir)

    if not entries:
        print("No scan history found. Run mcpaudit with --save-history first.")
        return 0

    # Build ScanResult objects from the latest history entry
    latest = entries[-1]
    scan_data = latest.get("scan", {})
    results_data = scan_data.get("results", [])
    scan_results: list[ScanResult] = []

    for rd in results_data:
        findings: list[Finding] = []
        for fd in rd.get("findings", []):
            findings.append(Finding(
                rule_id=fd.get("rule_id", ""),
                severity=Severity.from_string(fd.get("severity", "INFO")),
                title=fd.get("title", ""),
                description=fd.get("description", ""),
                file=fd.get("file", ""),
                line=fd.get("line", 0),
            ))
        scan_results.append(ScanResult(
            file=rd.get("file", ""),
            findings=findings,
            lines_scanned=rd.get("lines_scanned", 0),
        ))

    report = build_summary(scan_results, history=entries[:-1])

    if args.format == "json":
        output = format_summary_report_json(report)
    else:
        output = format_summary_report_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_generate(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit generate' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit generate",
        description="Rule Generation Engine -- List available rule templates",
    )
    parser.add_argument(
        "--category",
        choices=["owasp", "crypto", "network", "mcp", "traversal", "injection", "deserialization", "all"],
        default="all",
        help="Category of rules to list (default: all)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .rule_generator import (
        RULE_CATALOG,
        format_catalog_text,
        get_all_generated_rules,
        get_generated_rules_by_category,
        list_categories,
    )

    category = None if args.category == "all" else args.category

    if args.format == "json":
        import json as _json
        if category and category != "owasp":
            rules = get_generated_rules_by_category(category)
        else:
            rules = get_all_generated_rules()
        output = _json.dumps(
            [{"rule_id": r.rule_id, "name": r.name, "severity": r.severity.value,
              "cwe": r.cwe, "category": r.category, "description": r.description}
             for r in rules],
            indent=2, ensure_ascii=False,
        )
    else:
        output = format_catalog_text(category)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_fingerprint(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit fingerprint' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit fingerprint",
        description="MCP Server Fingerprinting -- Detect framework, transport, tools, and known vulnerabilities",
    )
    parser.add_argument(
        "path",
        help="Path to source file or directory to fingerprint",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--extended",
        action="store_true",
        help="Show extended fingerprint (transport, tools, resources)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    if args.extended:
        from .fingerprint import (
            fingerprint_path,
            format_mcpfingerprint_json,
            format_mcpfingerprint_text,
        )
        fp = fingerprint_path(target)
        if args.format == "json":
            output = format_mcpfingerprint_json(fp)
        else:
            output = format_mcpfingerprint_text(fp)
    else:
        from .fingerprint import (
            fingerprint_directory,
            fingerprint_file,
            format_fingerprint_json,
            format_fingerprint_text,
        )
        if target.is_file():
            info = fingerprint_file(target)
            if args.format == "json":
                output = format_fingerprint_json(info, str(target))
            else:
                output = format_fingerprint_text(info, str(target))
        else:
            results = fingerprint_directory(target)
            if not results:
                print("No MCP servers detected.")
                return 0
            parts: list[str] = []
            for fp_path, info in results:
                if args.format == "json":
                    parts.append(format_fingerprint_json(info, str(fp_path)))
                else:
                    parts.append(format_fingerprint_text(info, str(fp_path)))
            output = "\n".join(parts)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_generate_rule(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit generate-rule' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit generate-rule",
        description="Generate custom security rules from templates or findings",
    )
    parser.add_argument(
        "--template", "-t",
        default=None,
        help=(
            "Template name: command_injection, path_traversal, "
            "auth_check, input_validation, secret_detection"
        ),
    )
    parser.add_argument(
        "--from-finding",
        default=None,
        metavar="FILE",
        help="Path to a finding JSON file to convert into a rule",
    )
    parser.add_argument(
        "--name",
        default=None,
        help="Rule ID (e.g. MY-001)",
    )
    parser.add_argument(
        "--description",
        default=None,
        help="Rule description",
    )
    parser.add_argument(
        "--severity",
        default=None,
        help="Severity: CRITICAL, HIGH, MEDIUM, LOW, INFO",
    )
    parser.add_argument(
        "--output", "-o",
        default="rules/",
        help="Output directory for generated rule (default: rules/)",
    )
    parser.add_argument(
        "--list",
        action="store_true",
        help="List available templates",
    )
    args = parser.parse_args(argv)

    from .rule_gen import (
        generate_rule_from_finding,
        generate_rule_from_template,
        list_templates,
        save_generated_rule,
    )

    if args.list:
        templates = list_templates()
        print("\nAvailable templates:")
        for t in templates:
            print(f"  - {t}")
        print(f"\n{len(templates)} templates available")
        return 0

    if args.from_finding:
        import json as _json
        finding_path = Path(args.from_finding)
        if not finding_path.exists():
            print(f"Error: {finding_path} does not exist", file=sys.stderr)
            return 1
        finding_data = _json.loads(
            finding_path.read_text(encoding="utf-8")
        )
        result = generate_rule_from_finding(finding_data)
    elif args.template:
        if not args.name:
            parser.error("--name is required with --template")
        params: dict[str, str] = {"rule_id": args.name}
        if args.description:
            params["description"] = args.description
        if args.severity:
            params["severity"] = args.severity
        result = generate_rule_from_template(args.template, params)
    else:
        parser.error("Either --template or --from-finding is required")

    output_dir = Path(args.output)
    filepath = save_generated_rule(result, output_dir)
    print(f"Rule generated: {filepath}")
    print(f"  ID: {result.rule_id}")
    print(f"  Severity: {result.severity}")
    return 0


def _handle_deps(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit deps' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit deps",
        description="MCP Dependency Scanner — Scan dependency manifests for known vulnerabilities",
    )
    parser.add_argument(
        "path",
        help=(
            "Path to a manifest file (requirements.txt, package.json, "
            "go.mod, Cargo.toml) or directory to scan"
        ),
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .dep_scanner import DependencyScanner, format_dep_scan_json, format_dep_scan_text

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    scanner = DependencyScanner()

    if target.is_dir():
        results = scanner.scan_directory(target)
        if not results:
            print("No dependency manifests found.")
            return 0
        exit_code = 0
        parts: list[str] = []
        for result in results:
            if args.format == "json":
                parts.append(format_dep_scan_json(result))
            else:
                parts.append(format_dep_scan_text(result))
            exit_code = max(exit_code, result.exit_code)
        output = "\n".join(parts)
    else:
        result = scanner.scan_manifest(target)
        if args.format == "json":
            output = format_dep_scan_json(result)
        else:
            output = format_dep_scan_text(result)
        exit_code = result.exit_code

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return exit_code


def _handle_ci(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit ci' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit ci",
        description="MCP CI/CD Config Generator — Generate pipeline configs for mcpaudit",
    )
    parser.add_argument(
        "platform",
        choices=["github", "gitlab", "pre-commit", "jenkins"],
        help="CI/CD platform to generate config for",
    )
    parser.add_argument(
        "--min-score",
        type=int,
        default=70,
        help="Minimum trust score threshold (default: 70)",
    )
    parser.add_argument(
        "--fail-on",
        choices=["critical", "high", "medium", "low"],
        default="high",
        help="Fail on this severity or higher (default: high)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .ci_configs import CIOptions, format_ci_config

    options = CIOptions(
        min_trust_score=args.min_score,
        fail_on_severity=args.fail_on,
    )
    output = format_ci_config(args.platform, options)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Config written to {args.output}")
    else:
        print(output)

    return 0


def _handle_sbom(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit sbom' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit sbom",
        description="MCP SBOM Generator -- Generate Software Bill of Materials for MCP servers",
    )
    parser.add_argument(
        "path",
        help="Path to MCP server source file or directory",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["spdx", "cyclonedx", "json", "text"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .sbom import (
        export_sbom_cyclonedx,
        export_sbom_json,
        export_sbom_spdx,
        export_sbom_text,
        generate_sbom,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    sbom = generate_sbom(target)

    if args.format == "spdx":
        output = export_sbom_spdx(sbom)
    elif args.format == "cyclonedx":
        output = export_sbom_cyclonedx(sbom)
    elif args.format == "json":
        output = export_sbom_json(sbom)
    else:
        output = export_sbom_text(sbom)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"SBOM written to {args.output}")
    else:
        print(output)

    return 0


def _handle_playbook(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit playbook' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit playbook",
        description="MCP Remediation Playbooks -- Step-by-step remediation guides",
    )
    parser.add_argument(
        "rule_id",
        nargs="?",
        default=None,
        help="Rule ID to show playbook for (e.g. CMD-001)",
    )
    parser.add_argument(
        "--all",
        action="store_true",
        dest="show_all",
        help="Generate playbooks for all rules",
    )
    parser.add_argument(
        "--from-scan",
        default=None,
        metavar="FILE",
        help="Generate playbooks only for findings in scan JSON file",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "markdown", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    import json as _json

    from .playbooks import (
        export_playbooks_html,
        export_playbooks_json,
        export_playbooks_markdown,
        export_playbooks_text,
        generate_all_playbooks,
        generate_playbook_by_id,
        generate_playbooks_from_scan_data,
    )

    # Generate playbooks based on mode
    if args.from_scan:
        scan_path = Path(args.from_scan)
        if not scan_path.exists():
            print(f"Error: {scan_path} does not exist", file=sys.stderr)
            return 1
        scan_data = _json.loads(scan_path.read_text(encoding="utf-8"))
        playbooks = generate_playbooks_from_scan_data(scan_data)
        if not playbooks:
            print("No playbooks available for findings in this scan.")
            return 0
    elif args.show_all:
        playbooks = generate_all_playbooks()
    elif args.rule_id:
        pb = generate_playbook_by_id(args.rule_id)
        if pb is None:
            from .playbooks import get_covered_rule_ids
            covered = get_covered_rule_ids()
            print(f"Error: no playbook for rule '{args.rule_id}'", file=sys.stderr)
            print(f"Available: {', '.join(covered)}", file=sys.stderr)
            return 1
        playbooks = [pb]
    else:
        parser.error("Specify a rule ID, --all, or --from-scan")
        return 1

    # Format output
    if args.format == "json":
        output = export_playbooks_json(playbooks)
    elif args.format == "markdown":
        output = export_playbooks_markdown(playbooks)
    elif args.format == "html":
        output = export_playbooks_html(playbooks)
    else:
        output = export_playbooks_text(playbooks)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Playbooks written to {args.output}")
    else:
        print(output)

    return 0


def _handle_regression(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit regression' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit regression",
        description="MCP Regression Detection -- Compare scans for security regressions",
    )
    parser.add_argument(
        "path",
        nargs="?",
        default=None,
        help="Path to current scan JSON file",
    )
    parser.add_argument(
        "--baseline",
        default=None,
        metavar="FILE",
        help="Path to baseline scan JSON file for comparison",
    )
    parser.add_argument(
        "--history-dir",
        default=None,
        help="Compare against latest entry in scan history directory",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    import json as _json

    from .regression import (
        detect_regressions,
        detect_regressions_from_files,
        detect_regressions_from_history,
        export_regression_html,
        format_regression_json,
        format_regression_text,
    )

    if args.baseline:
        # Compare two files
        if not args.path:
            parser.error("path is required when using --baseline")
        baseline_path = Path(args.baseline)
        current_path = Path(args.path)
        if not baseline_path.exists():
            print(f"Error: {baseline_path} does not exist", file=sys.stderr)
            return 1
        if not current_path.exists():
            print(f"Error: {current_path} does not exist", file=sys.stderr)
            return 1
        result = detect_regressions_from_files(baseline_path, current_path)
    elif args.history_dir:
        # Compare against history
        if not args.path:
            parser.error("path is required when using --history-dir")
        current_path = Path(args.path)
        if not current_path.exists():
            print(f"Error: {current_path} does not exist", file=sys.stderr)
            return 1
        history_dir = Path(args.history_dir)
        current_data = _json.loads(current_path.read_text(encoding="utf-8"))
        result = detect_regressions_from_history(current_data, history_dir)
    else:
        parser.error("Specify --baseline <file> or --history-dir <dir>")
        return 1

    if args.format == "json":
        output = format_regression_json(result)
    elif args.format == "html":
        output = export_regression_html(result)
    else:
        output = format_regression_text(result)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Regression report written to {args.output}")
    else:
        print(output)

    return result.exit_code


def _handle_secrets(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit secrets' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit secrets",
        description="MCP Secret Scanner -- Detect hardcoded secrets, API keys, tokens, and private keys",
    )
    parser.add_argument(
        "path",
        help="Path to file or directory to scan for secrets",
    )
    parser.add_argument(
        "--min-entropy",
        type=float,
        default=None,
        metavar="FLOAT",
        help="Minimum entropy threshold for high-entropy detection (default: 4.5)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .secret_scanner import (
        format_secrets_json,
        format_secrets_text,
        scan_secrets_directory,
        scan_secrets_file,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    if target.is_dir():
        findings = scan_secrets_directory(str(target))
    else:
        findings = scan_secrets_file(str(target))

    # Apply min-entropy filter
    if args.min_entropy is not None:
        findings = [f for f in findings if f.entropy >= args.min_entropy]

    if args.format == "json":
        output = format_secrets_json(findings)
    else:
        output = format_secrets_text(findings)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Secret scan report written to {args.output}")
    else:
        print(output)

    return 1 if findings else 0


def _handle_sarif(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit sarif' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit sarif",
        description="MCP SARIF CI Integration -- Generate CI-native SARIF/SAST reports",
    )
    parser.add_argument(
        "path",
        help="Path to file or directory to scan",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["github", "gitlab", "azure"],
        default="github",
        help="CI platform format (default: github)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .sarif_ci import (
        export_github_annotations,
        export_gitlab_sast_json,
        export_sarif_json,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    scanner = MCPSecurityScanner()
    summary = scanner.scan(target)

    if args.format == "gitlab":
        output = export_gitlab_sast_json(summary.results)
    elif args.format == "azure":
        # Azure DevOps also uses SARIF format
        output = export_sarif_json(summary.results)
    else:
        # GitHub — full SARIF
        output = export_sarif_json(summary.results)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"SARIF report written to {args.output}")
    else:
        print(output)

    return summary.exit_code


def _handle_coverage(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit coverage' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit coverage",
        description="MCP Rule Coverage Analyzer -- Analyze OWASP/CWE/MITRE coverage",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "html", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .rule_coverage import (
        analyze_cwe_coverage,
        analyze_mitre_coverage,
        analyze_owasp_coverage,
        format_coverage_json,
        format_coverage_text,
        generate_coverage_html,
    )

    matrices = [
        analyze_owasp_coverage(),
        analyze_cwe_coverage(),
        analyze_mitre_coverage(),
    ]

    if args.format == "html":
        output = generate_coverage_html(matrices)
    elif args.format == "json":
        output = format_coverage_json(matrices)
    else:
        output = format_coverage_text(matrices)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Coverage report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_dep_vuln(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit dep-vuln' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit dep-vuln",
        description="MCP Dependency Vulnerability Scanner — Classify dependencies by MCP risk",
    )
    parser.add_argument(
        "path",
        help="Path to a manifest file or directory to scan",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .dep_vuln import format_vuln_json, format_vuln_text, scan_deps

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    report = scan_deps(target)

    if args.format == "json":
        output = format_vuln_json(report)
    else:
        output = format_vuln_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return 2 if report.risk_score >= 75 else (1 if report.risk_score >= 25 else 0)


def _handle_drift(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit drift' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit drift",
        description="MCP Configuration Drift Detector — Detect unauthorized config changes",
    )
    parser.add_argument(
        "action",
        choices=["snapshot", "check"],
        help="Action: snapshot (save baseline) or check (compare against baseline)",
    )
    parser.add_argument(
        "path",
        help="Path to MCP config file or directory",
    )
    parser.add_argument(
        "--baseline",
        default=None,
        help="Path to baseline snapshot file (required for 'check')",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (for snapshot: save location, for check: report)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format for check (default: text)",
    )
    args = parser.parse_args(argv)

    from .config_drift import (
        check_drift_from_file,
        create_snapshot,
        format_drift_json,
        format_drift_text,
        save_snapshot,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    if args.action == "snapshot":
        snapshot = create_snapshot(target)
        out_path = Path(args.output) if args.output else Path("mcp_baseline.json")
        save_snapshot(snapshot, out_path)
        print(f"Snapshot saved to {out_path}")
        return 0

    # action == "check"
    if not args.baseline:
        print("Error: --baseline is required for 'check' action", file=sys.stderr)
        return 1

    baseline_path = Path(args.baseline)
    if not baseline_path.exists():
        print(f"Error: baseline file not found: {baseline_path}", file=sys.stderr)
        return 1

    report = check_drift_from_file(baseline_path, target)

    if args.format == "json":
        output = format_drift_json(report)
    else:
        output = format_drift_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Drift report written to {args.output}")
    else:
        print(output)

    return report.exit_code


def _handle_attack_paths(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit attack-paths' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit attack-paths",
        description="MCP Attack Path Analyzer — Identify attack chains in MCP servers",
    )
    parser.add_argument(
        "path",
        help="Path to source file, config file, or directory to analyze",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "dot"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .attack_path import (
        analyze_attack_paths,
        format_attack_dot,
        format_attack_json,
        format_attack_text,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    graph = analyze_attack_paths(target)

    if args.format == "json":
        output = format_attack_json(graph)
    elif args.format == "dot":
        output = format_attack_dot(graph)
    else:
        output = format_attack_text(graph)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Attack path report written to {args.output}")
    else:
        print(output)

    return graph.exit_code


def _handle_supply_chain(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit supply-chain' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit supply-chain",
        description="MCP Supply Chain Scanner — Analyze dependencies for supply chain risks",
    )
    parser.add_argument(
        "path",
        help="Path to a manifest file or directory to scan",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .supply_chain import (
        SupplyChainScanner,
        format_supply_chain_html,
        format_supply_chain_json,
        format_supply_chain_text,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    scanner = SupplyChainScanner()

    if target.is_dir():
        results = scanner.scan_directory(target)
        if not results:
            print("No dependency manifests found.")
            return 0
        exit_code = 0
        parts: list[str] = []
        for result in results:
            if args.format == "json":
                parts.append(format_supply_chain_json(result))
            elif args.format == "html":
                parts.append(format_supply_chain_html(result))
            else:
                parts.append(format_supply_chain_text(result))
            exit_code = max(exit_code, result.exit_code)
        output = "\n".join(parts)
    else:
        result = scanner.scan_manifest(target)
        if args.format == "json":
            output = format_supply_chain_json(result)
        elif args.format == "html":
            output = format_supply_chain_html(result)
        else:
            output = format_supply_chain_text(result)
        exit_code = result.exit_code

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Supply chain report written to {args.output}")
    else:
        print(output)

    return exit_code


def _handle_scorecard(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit scorecard' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit scorecard",
        description="MCP Security Scorecard — Generate executive security scorecard",
    )
    parser.add_argument(
        "path",
        help="Path to file or directory to scan",
    )
    parser.add_argument(
        "--output", "-o",
        default="scorecard.html",
        help="Output HTML file path (default: scorecard.html)",
    )
    args = parser.parse_args(argv)

    from .scorecard import generate_scorecard_from_summary

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    scanner = MCPSecurityScanner()
    summary = scanner.scan(target)

    html_output = generate_scorecard_from_summary(summary, target=str(target))
    Path(args.output).write_text(html_output, encoding="utf-8")
    print(f"Scorecard written to {args.output}")

    return summary.exit_code


def _handle_ci_template(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit ci-template' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit ci-template",
        description="MCP CI/CD Template Generator — Generate pipeline configs for mcpaudit",
    )
    parser.add_argument(
        "--platform", "-p",
        choices=["github", "gitlab", "pre-commit"],
        default="github",
        help="CI/CD platform (default: github)",
    )
    parser.add_argument(
        "--min-score",
        type=int,
        default=70,
        help="Minimum trust score threshold (default: 70)",
    )
    parser.add_argument(
        "--severity",
        choices=["critical", "high", "medium", "low"],
        default="high",
        help="Severity threshold (default: high)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html", "sarif"],
        default="text",
        help="Output format for scans (default: text)",
    )
    parser.add_argument(
        "--supply-chain",
        action="store_true",
        help="Include supply chain scan step",
    )
    parser.add_argument(
        "--secrets",
        action="store_true",
        help="Include secrets scan step",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .ci_templates import CITemplateOptions, generate_ci_template

    options = CITemplateOptions(
        severity_threshold=args.severity,
        min_trust_score=args.min_score,
        output_format=args.format,
        enable_supply_chain=args.supply_chain,
        enable_secrets_scan=args.secrets,
    )
    output = generate_ci_template(args.platform, options)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"CI template written to {args.output}")
    else:
        print(output)

    return 0


def _handle_protocol(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit protocol' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit protocol",
        description="MCP Protocol Validator — Validate MCP server protocol compliance",
    )
    parser.add_argument(
        "path",
        help="Path to MCP definition file (JSON) or directory",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .protocol_validator import (
        format_protocol_json,
        format_protocol_text,
        validate_protocol_directory,
        validate_protocol_file,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    if target.is_dir():
        result = validate_protocol_directory(str(target))
    else:
        result = validate_protocol_file(str(target))

    if args.format == "json":
        output = format_protocol_json(result)
    else:
        output = format_protocol_text(result)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Protocol validation report written to {args.output}")
    else:
        print(output)

    return result.exit_code


def _handle_detect_secrets(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit detect-secrets' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit detect-secrets",
        description="MCP Secrets Detector — Advanced secret detection with low false-positive rate",
    )
    parser.add_argument(
        "path",
        help="Path to file or directory to scan for secrets",
    )
    parser.add_argument(
        "--entropy-threshold",
        type=float,
        default=4.5,
        metavar="FLOAT",
        help="Minimum entropy threshold for high-entropy detection (default: 4.5)",
    )
    parser.add_argument(
        "--allowlist",
        default=None,
        metavar="FILE",
        help="Path to allowlist file with patterns to ignore (one regex per line)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .secrets_detector import (
        detect_secrets_directory,
        detect_secrets_file,
        format_secrets_json,
        format_secrets_text,
        load_allowlist,
        SecretsReport,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    allowlist = load_allowlist(args.allowlist) if args.allowlist else None

    if target.is_dir():
        report = detect_secrets_directory(
            str(target),
            entropy_threshold=args.entropy_threshold,
            allowlist=allowlist,
        )
    else:
        findings = detect_secrets_file(
            str(target),
            entropy_threshold=args.entropy_threshold,
            allowlist=allowlist,
        )
        report = SecretsReport(
            secrets=findings,
            files_scanned=1,
            patterns_used=36,
            entropy_threshold=args.entropy_threshold,
            allowlist_patterns=len(allowlist) if allowlist else 0,
        )

    if args.format == "json":
        output = format_secrets_json(report)
    else:
        output = format_secrets_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Secrets report written to {args.output}")
    else:
        print(output)

    return report.exit_code


def _handle_benchmark(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit benchmark' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit benchmark",
        description="MCP Benchmark Runner — Performance benchmarking for the scanner",
    )
    parser.add_argument(
        "path",
        nargs="?",
        default=".",
        help="Path to file or directory to benchmark (default: current dir)",
    )
    parser.add_argument(
        "--iterations",
        type=int,
        default=10,
        help="Number of scan iterations (default: 10)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .benchmark import (
        format_benchmark_html,
        format_benchmark_json,
        format_benchmark_text,
        run_benchmark,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    result = run_benchmark(str(target), iterations=args.iterations)

    if args.format == "json":
        output = format_benchmark_json(result)
    elif args.format == "html":
        output = format_benchmark_html(result)
    else:
        output = format_benchmark_text(result)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Benchmark report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_threats(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit threats' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit threats",
        description="MCP Threat Intelligence — List and match known threats",
    )
    sub = parser.add_subparsers(dest="action")

    list_p = sub.add_parser("list", help="List all known threats")
    list_p.add_argument(
        "--severity",
        choices=["critical", "high", "medium", "low", "info"],
        default=None,
        help="Filter by severity",
    )
    list_p.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format",
    )

    match_p = sub.add_parser("match", help="Match threats against scan results")
    match_p.add_argument("scan_file", help="Scan result JSON file")
    match_p.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format",
    )
    match_p.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path",
    )

    args = parser.parse_args(argv)

    from .threat_intel import (
        ThreatIntelDB,
        ThreatSeverity,
        format_threat_json,
        format_threat_text,
        format_threats_list_text,
        generate_threat_report,
        match_threats_from_scan,
    )

    if args.action == "list":
        db = ThreatIntelDB()
        threats = db.list_threats()
        if args.severity:
            sev = ThreatSeverity.from_string(args.severity)
            threats = [t for t in threats if t.severity == sev]
        if args.format == "json":
            import json as _json
            print(_json.dumps([t.to_dict() for t in threats], indent=2))
        else:
            print(format_threats_list_text(threats))
        return 0

    if args.action == "match":
        import json as _json
        scan_path = Path(args.scan_file)
        if not scan_path.exists():
            print(f"Error: {scan_path} does not exist", file=sys.stderr)
            return 1
        scan_data = _json.loads(scan_path.read_text(encoding="utf-8"))
        matches = match_threats_from_scan(scan_data)
        report = generate_threat_report(matches)
        if args.format == "json":
            output = format_threat_json(report)
        else:
            output = format_threat_text(report)
        if args.output:
            Path(args.output).write_text(output, encoding="utf-8")
            print(f"Threat report written to {args.output}")
        else:
            print(output)
        return 0

    parser.print_help()
    return 0


def _handle_compliance(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit compliance' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit compliance",
        description="MCP Compliance Validator — Validate against security frameworks",
    )
    parser.add_argument(
        "path",
        help="Scan result JSON file to validate",
    )
    parser.add_argument(
        "--framework",
        choices=["owasp", "nist", "soc2"],
        default="owasp",
        help="Compliance framework (default: owasp)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .compliance_validator import (
        ComplianceFramework,
        format_compliance_html,
        format_compliance_json,
        format_compliance_text,
        validate_compliance_from_file,
    )

    scan_path = Path(args.path)
    if not scan_path.exists():
        print(f"Error: {scan_path} does not exist", file=sys.stderr)
        return 1

    framework = ComplianceFramework.from_string(args.framework)
    result = validate_compliance_from_file(scan_path, framework)

    if args.format == "json":
        output = format_compliance_json(result)
    elif args.format == "html":
        output = format_compliance_html(result)
    else:
        output = format_compliance_text(result)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Compliance report written to {args.output}")
    else:
        print(output)

    return 0 if result.overall_status == "COMPLIANT" else 1


def _handle_cache(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit cache' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit cache",
        description="MCP Audit Scan Cache — Manage scan result cache",
    )
    sub = parser.add_subparsers(dest="action")

    clear_p = sub.add_parser("clear", help="Clear the scan cache")
    clear_p.add_argument(
        "--cache-dir",
        default=None,
        help="Cache directory (default: .mcpaudit/cache/)",
    )

    stats_p = sub.add_parser("stats", help="Show cache statistics")
    stats_p.add_argument(
        "--cache-dir",
        default=None,
        help="Cache directory (default: .mcpaudit/cache/)",
    )
    stats_p.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format",
    )

    args = parser.parse_args(argv)

    from .scan_cache import (
        ScanCache,
        format_cache_stats_json,
        format_cache_stats_text,
    )

    if args.action == "clear":
        cache = ScanCache(cache_dir=args.cache_dir)
        count = cache.clear()
        print(f"Cleared {count} cached scan results")
        return 0

    if args.action == "stats":
        cache = ScanCache(cache_dir=args.cache_dir)
        stats = cache.stats()
        if args.format == "json":
            print(format_cache_stats_json(stats))
        else:
            print(format_cache_stats_text(stats))
        return 0

    parser.print_help()
    return 0


def _handle_risk_score(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit risk-score' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit risk-score",
        description="MCP Risk Scoring Engine — Multi-dimensional risk assessment",
    )
    parser.add_argument(
        "path",
        help="Path to file or directory to scan",
    )
    parser.add_argument(
        "--weights",
        default=None,
        help="Path to custom weights JSON file",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    parser.add_argument(
        "--history",
        default=None,
        help="Path to risk history JSON file for trend analysis",
    )
    parser.add_argument(
        "--save-history",
        default=None,
        help="Save result to this risk history file",
    )
    args = parser.parse_args(argv)

    from .risk_scoring import (
        compute_risk_profile,
        format_risk_html,
        format_risk_json,
        format_risk_text,
        load_history as load_risk_history,
        load_weights,
        save_history as save_risk_history,
    )
    from .trust_score import compute_summary_trust_score

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    scanner = MCPSecurityScanner()
    summary = scanner.scan(target)

    # Gather findings as dicts
    findings = []
    for r in summary.results:
        for f in r.findings:
            findings.append(f.to_dict())

    # Trust score
    trust = compute_summary_trust_score(summary)

    # Weights
    weights = load_weights(args.weights)

    # History
    history = None
    if args.history:
        history = load_risk_history(args.history)

    profile = compute_risk_profile(
        findings=findings,
        weights=weights,
        trust_score=trust.score,
        history=history,
    )

    if args.format == "json":
        output = format_risk_json(profile)
    elif args.format == "html":
        output = format_risk_html(profile)
    else:
        output = format_risk_text(profile)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Risk score report written to {args.output}")
    else:
        print(output)

    if args.save_history:
        save_risk_history(args.save_history, profile)
        print(f"Risk history saved to {args.save_history}")

    return 0 if profile.composite_score >= 50 else 1


def _handle_aggregate(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit aggregate' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit aggregate",
        description="MCP Report Aggregator — Merge multiple scan reports",
    )
    parser.add_argument(
        "reports",
        nargs="+",
        help="Paths to JSON scan report files to merge",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .report_aggregator import (
        format_aggregate_html,
        format_aggregate_json,
        format_aggregate_text,
        merge_report_files,
    )

    paths = [Path(p) for p in args.reports]
    for p in paths:
        if not p.exists():
            print(f"Error: {p} does not exist", file=sys.stderr)
            return 1

    report = merge_report_files(paths)

    if args.format == "json":
        output = format_aggregate_json(report)
    elif args.format == "html":
        output = format_aggregate_html(report)
    else:
        output = format_aggregate_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Aggregated report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_rule_impact(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit rule-impact' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit rule-impact",
        description="MCP Rule Impact Analyzer — Analyze rule effectiveness",
    )
    parser.add_argument(
        "path",
        help="Path to file or directory to scan",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .rule_impact import (
        analyze_rule_impact,
        format_impact_json,
        format_impact_text,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    scanner = MCPSecurityScanner()
    summary = scanner.scan(target)

    findings = []
    total_loc = 0
    for r in summary.results:
        total_loc += r.lines_scanned
        for f in r.findings:
            findings.append(f.to_dict())

    report = analyze_rule_impact(findings, total_loc=total_loc)

    if args.format == "json":
        output = format_impact_json(report)
    else:
        output = format_impact_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Rule impact report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_harden(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit harden' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit harden",
        description="MCP Config Hardener — Generate hardened MCP configurations",
    )
    parser.add_argument(
        "path",
        help="Path to MCP config file (e.g. claude_desktop_config.json)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file for hardened config (default: stdout)",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Show diff without writing output",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "diff"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--rules",
        default=None,
        help="Comma-separated rule IDs to apply (default: all)",
    )
    parser.add_argument(
        "--exclude-rules",
        default=None,
        help="Comma-separated rule IDs to skip",
    )
    args = parser.parse_args(argv)

    from .hardener import (
        format_diff,
        format_hardened_json,
        format_hardening_report_json,
        format_hardening_text,
        harden_file,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    rules = args.rules.split(",") if args.rules else None
    exclude = args.exclude_rules.split(",") if args.exclude_rules else None

    result = harden_file(target, rules=rules, exclude_rules=exclude)

    if args.dry_run:
        print(format_diff(result))
        return 0

    if args.format == "json":
        output = format_hardening_report_json(result)
    elif args.format == "diff":
        output = format_diff(result)
    else:
        output = format_hardening_text(result)

    if args.output:
        Path(args.output).write_text(
            format_hardened_json(result), encoding="utf-8",
        )
        print(f"Hardened config written to {args.output}")
        print(output)
    else:
        print(output)

    return result.exit_code


def _handle_audit(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit audit' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit audit",
        description="MCP Audit Trail — View and export audit logs",
    )
    parser.add_argument(
        "action",
        choices=["show", "export"],
        help="Action: show (display log) or export (to file)",
    )
    parser.add_argument(
        "--log-file",
        default=".mcpaudit-audit.jsonl",
        help="Path to audit log file (default: .mcpaudit-audit.jsonl)",
    )
    parser.add_argument(
        "--since",
        default=None,
        help="Show entries since date (ISO format, e.g. 2024-01-15)",
    )
    parser.add_argument(
        "--action-filter",
        default=None,
        dest="action_filter",
        help="Filter by action type (scan, fix, guard, harden)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "jsonl", "csv", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .audit_trail import (
        export_csv,
        export_html,
        export_jsonl,
        format_audit_json,
        format_audit_text,
        load_log,
    )

    log = load_log(args.log_file)

    if args.since or args.action_filter:
        from .audit_trail import AuditLog
        filtered_entries = log.query(
            since=args.since,
            action=args.action_filter,
        )
        log = AuditLog(entries=filtered_entries, log_path=log.log_path)

    if args.action == "export" or args.format in ("jsonl", "csv", "html"):
        if args.format == "csv":
            output = export_csv(log)
        elif args.format == "html":
            output = export_html(log)
        elif args.format == "jsonl":
            output = export_jsonl(log)
        elif args.format == "json":
            output = format_audit_json(log)
        else:
            output = export_jsonl(log)
    else:
        if args.format == "json":
            output = format_audit_json(log)
        else:
            output = format_audit_text(log)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Audit log exported to {args.output}")
    else:
        print(output)

    return 0


def _handle_metrics(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit metrics' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit metrics",
        description="MCP Security Metrics — Aggregated security metrics for dashboards",
    )
    parser.add_argument(
        "--period",
        choices=["day", "week", "month"],
        default="day",
        help="Aggregation period (default: day)",
    )
    parser.add_argument(
        "--metrics-file",
        default=".mcpaudit-metrics.json",
        help="Path to metrics data file (default: .mcpaudit-metrics.json)",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    args = parser.parse_args(argv)

    from .security_metrics import (
        MetricCollector,
        format_metrics_html,
        format_metrics_json,
        format_metrics_text,
        load_metrics,
    )

    snapshots = load_metrics(args.metrics_file)

    collector = MetricCollector()
    collector.snapshots = snapshots

    report = collector.aggregate(period=args.period)

    if args.format == "json":
        output = format_metrics_json(report)
    elif args.format == "html":
        output = format_metrics_html(report)
    else:
        output = format_metrics_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Metrics report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_dashboard(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit dashboard' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit dashboard",
        description="MCP Security Dashboard — Generate unified HTML dashboard from scan results",
    )
    parser.add_argument(
        "path",
        help="File or directory to scan",
    )
    parser.add_argument(
        "--output", "-o",
        default="dashboard.html",
        help="Output HTML file path (default: dashboard.html)",
    )
    parser.add_argument(
        "--title",
        default="MCP Security Dashboard",
        help="Dashboard title",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["html", "json", "text"],
        default="html",
        help="Output format (default: html)",
    )
    args = parser.parse_args(argv)

    from .dashboard_gen import (
        DashboardConfig,
        build_dashboard_data,
        format_dashboard_json,
        format_dashboard_text,
        generate_security_dashboard,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    all_rules = get_all_rules()
    scanner = MCPSecurityScanner(rules=all_rules)
    summary = scanner.scan(target)
    cfg = DashboardConfig(title=args.title)

    if args.format == "json":
        data = build_dashboard_data(summary, cfg)
        output = format_dashboard_json(data)
    elif args.format == "text":
        data = build_dashboard_data(summary, cfg)
        output = format_dashboard_text(data)
    else:
        output = generate_security_dashboard(summary, config=cfg)

    Path(args.output).write_text(output, encoding="utf-8")
    print(f"Dashboard written to {args.output}")
    return summary.exit_code


def _handle_profiles(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit profiles' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit profiles",
        description="MCP Scan Profiles — List and inspect scan profiles",
    )
    parser.add_argument(
        "action",
        choices=["list", "show"],
        help="Action: list (all profiles), show (single profile details)",
    )
    parser.add_argument(
        "--name",
        default=None,
        help="Profile name (required for 'show')",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format (default: text)",
    )
    args = parser.parse_args(argv)

    from .scan_profiles import (
        format_profiles_json,
        format_profiles_text,
        get_profile as get_scan_profile,
    )

    if args.action == "list":
        if args.format == "json":
            output = format_profiles_json()
        else:
            output = format_profiles_text()
        print(output)
        return 0

    # show
    if not args.name:
        print("Error: --name is required for 'show'", file=sys.stderr)
        return 1
    try:
        profile = get_scan_profile(args.name)
    except ValueError as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

    if args.format == "json":
        import json as _json
        print(_json.dumps(profile.to_dict(), indent=2))
    else:
        print(f"\n  Profile: {profile.name}")
        print(f"  Description: {profile.description}")
        print(f"  Severity: >= {profile.severity_threshold}")
        print(f"  Format: {profile.output_format}")
        if profile.rules_enabled is not None:
            print(f"  Rules enabled: {len(profile.rules_enabled)}")
        if profile.rules_disabled:
            print(f"  Rules disabled: {len(profile.rules_disabled)}")
        if profile.options:
            for k, v in profile.options.items():
                print(f"  {k}: {v}")
        print()
    return 0


def _handle_remediate(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit remediate' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit remediate",
        description="MCP Remediation Engine — Generate remediation plans for security findings",
    )
    parser.add_argument(
        "path",
        help="File or directory to scan and generate remediation plan",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Preview mode — show plan without applying changes",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    parser.add_argument(
        "--priority",
        default=None,
        help="Comma-separated priority filter (e.g. --priority critical,high)",
    )
    parser.add_argument(
        "--fix-type",
        default=None,
        help="Comma-separated fix type filter (e.g. --fix-type code_change)",
    )
    args = parser.parse_args(argv)

    from .remediation_engine import (
        format_remediation_html,
        format_remediation_json,
        format_remediation_text,
        generate_remediation_plan,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    all_rules = get_all_rules()
    scanner = MCPSecurityScanner(rules=all_rules)
    summary = scanner.scan(target)

    priority_filter = None
    if args.priority:
        priority_filter = [p.strip() for p in args.priority.split(",")]

    fix_type_filter = None
    if args.fix_type:
        fix_type_filter = [t.strip() for t in args.fix_type.split(",")]

    plan = generate_remediation_plan(
        summary,
        dry_run=args.dry_run,
        priority_filter=priority_filter,
        fix_type_filter=fix_type_filter,
    )

    if args.format == "json":
        output = format_remediation_json(plan)
    elif args.format == "html":
        output = format_remediation_html(plan)
    else:
        output = format_remediation_text(plan)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Remediation plan written to {args.output}")
    else:
        print(output)

    return summary.exit_code


def _handle_fleet(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit fleet' subcommand (multi-project scan)."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit fleet",
        description="Multi-Project Fleet Scanner — scan multiple projects",
    )
    sub = parser.add_subparsers(dest="action")

    scan_p = sub.add_parser("scan", help="Scan multiple project directories")
    scan_p.add_argument(
        "paths", nargs="+",
        help="Project directories to scan",
    )
    scan_p.add_argument(
        "--format", "-f",
        choices=["text", "json", "html"],
        default="text",
        help="Output format (default: text)",
    )
    scan_p.add_argument(
        "--output", "-o", default=None,
        help="Output file path (default: stdout)",
    )

    args = parser.parse_args(argv)
    if not args.action:
        parser.print_help()
        return 0

    from .multi_project import (
        format_fleet_json,
        format_fleet_text,
        generate_fleet_html,
        scan_projects,
    )

    report = scan_projects(args.paths)

    if args.format == "json":
        output = format_fleet_json(report)
    elif args.format == "html":
        output = generate_fleet_html(report)
    else:
        output = format_fleet_text(report)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Fleet report written to {args.output}")
    else:
        print(output)

    return 0


def _handle_annotate(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit annotate' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit annotate",
        description="Finding Annotator — mark findings as FP, accepted, etc.",
    )
    parser.add_argument(
        "finding_id", nargs="?", default=None,
        help="Finding ID to annotate (e.g. CMD-001:server.py:42)",
    )
    parser.add_argument(
        "--status", "-s",
        choices=[
            "false_positive", "accepted_risk", "todo",
            "in_progress", "wont_fix",
        ],
        default=None,
        help="Annotation status",
    )
    parser.add_argument(
        "--note", "-n", default="",
        help="Annotation note/reason",
    )
    parser.add_argument(
        "--author", "-a", default="",
        help="Who is annotating",
    )
    parser.add_argument(
        "--file", "-f",
        default=".mcpaudit-annotations.json",
        help="Annotations file (default: .mcpaudit-annotations.json)",
    )
    parser.add_argument(
        "--list", "-l", action="store_true",
        help="List all annotations",
    )
    parser.add_argument(
        "--format",
        choices=["text", "json"],
        default="text",
        help="Output format for --list",
    )
    parser.add_argument(
        "--remove", "-r", action="store_true",
        help="Remove annotation for the given finding ID",
    )
    args = parser.parse_args(argv)

    from .annotator import (
        format_annotations_json,
        format_annotations_text,
        load_and_annotate,
        load_annotations,
        remove_annotation,
        save_annotations,
    )

    # List mode
    if args.list:
        store = load_annotations(args.file)
        if args.format == "json":
            print(format_annotations_json(store))
        else:
            print(format_annotations_text(store))
        return 0

    if not args.finding_id:
        parser.error("finding_id is required (or use --list)")

    # Remove mode
    if args.remove:
        store = load_annotations(args.file)
        if remove_annotation(args.finding_id, store):
            save_annotations(store, args.file)
            print(f"Removed annotation for {args.finding_id}")
        else:
            print(f"No annotation found for {args.finding_id}")
        return 0

    # Annotate mode
    if not args.status:
        parser.error("--status is required when annotating")

    ann, errors = load_and_annotate(
        args.finding_id, args.status,
        note=args.note, author=args.author,
        path=args.file,
    )
    if errors:
        for e in errors:
            print(f"Error: {e}", file=sys.stderr)
        return 1

    print(f"Annotated {ann.finding_id} as {ann.status}")
    return 0


def _handle_rule_sdk(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit rule-sdk' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit rule-sdk",
        description="Rule SDK — list and run custom rules",
    )
    sub = parser.add_subparsers(dest="action")

    sub.add_parser("list", help="List example SDK rules")

    scan_p = sub.add_parser("scan", help="Scan with example SDK rules")
    scan_p.add_argument("path", help="File or directory to scan")
    scan_p.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format",
    )

    args = parser.parse_args(argv)
    if not args.action:
        parser.print_help()
        return 0

    from .rule_sdk import (
        format_findings_json,
        format_findings_text,
        format_registry_text,
        get_example_registry,
    )

    reg = get_example_registry()

    if args.action == "list":
        print(format_registry_text(reg))
        return 0

    # Scan
    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    findings = reg.scan_file(str(target))
    if args.format == "json":
        print(format_findings_json(findings))
    else:
        print(format_findings_text(findings))

    return 1 if findings else 0


def _handle_report(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit report' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit report",
        description="Generate executive security report (HTML)",
    )
    parser.add_argument(
        "path",
        help="Path to scan (file or directory)",
    )
    parser.add_argument(
        "--output", "-o",
        default=None,
        help="Output file path (default: stdout)",
    )
    parser.add_argument(
        "--title", "-t",
        default=None,
        help="Report title",
    )
    parser.add_argument(
        "--author",
        default=None,
        help="Report author",
    )
    parser.add_argument(
        "--format", "-f",
        choices=["html", "json", "text"],
        default="html",
        help="Output format (default: html)",
    )

    args = parser.parse_args(argv)

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    # Scan
    scanner = MCPSecurityScanner()
    summary = scanner.scan(target)
    scan_data = summary.to_dict()

    from .report_generator import (
        ReportOptions,
        build_executive_report,
        format_report_json,
        format_report_text,
        generate_executive_report,
    )

    opts = ReportOptions()
    if args.title:
        opts.title = args.title
    if args.author:
        opts.author = args.author

    if args.format == "json":
        report = build_executive_report(scan_data, opts)
        output = format_report_json(report)
    elif args.format == "text":
        report = build_executive_report(scan_data, opts)
        output = format_report_text(report)
    else:
        output = generate_executive_report(scan_data, opts)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    return summary.exit_code


def _handle_registry(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit registry' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit registry",
        description="MCP Registry Scanner — assess MCP risk",
    )
    sub = parser.add_subparsers(dest="action")

    sub.add_parser("list", help="List known MCPs catalog")

    scan_p = sub.add_parser("scan", help="Scan a registry file")
    scan_p.add_argument(
        "path", nargs="?", default=None,
        help="Registry JSON file (default: builtin catalog)",
    )
    scan_p.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format",
    )

    assess_p = sub.add_parser("assess", help="Assess a specific MCP")
    assess_p.add_argument("name", help="MCP name to assess")
    assess_p.add_argument(
        "--format", "-f",
        choices=["text", "json"],
        default="text",
        help="Output format",
    )

    args = parser.parse_args(argv)
    if not args.action:
        parser.print_help()
        return 0

    from .registry_scanner import (
        assess_mcp_risk,
        format_assessment_text,
        format_mcps_list_text,
        format_registry_json,
        format_registry_text,
        get_known_mcp,
        get_known_mcps,
        scan_registry,
    )

    if args.action == "list":
        mcps = get_known_mcps()
        print(format_mcps_list_text(mcps))
        return 0

    if args.action == "scan":
        if args.path:
            report = scan_registry(path=args.path)
        else:
            report = scan_registry()

        if args.format == "json":
            print(format_registry_json(report))
        else:
            print(format_registry_text(report))

        return 1 if report.high_risk_mcps else 0

    if args.action == "assess":
        mcp = get_known_mcp(args.name)
        if mcp is None:
            print(f"Error: unknown MCP '{args.name}'", file=sys.stderr)
            return 1

        assessment = assess_mcp_risk(mcp)
        if args.format == "json":
            import json as _json
            print(_json.dumps(assessment.to_dict(), indent=2))
        else:
            print(format_assessment_text(assessment))

        return 1 if assessment.risk_level in ("critical", "high") else 0

    return 0


def _handle_exempt(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit exempt' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit exempt",
        description="Exemption Manager — manage finding exemptions",
    )
    sub = parser.add_subparsers(dest="action")

    add_p = sub.add_parser("add", help="Add a new exemption")
    add_p.add_argument("rule", help="Rule ID to exempt (e.g. CMD-001)")
    add_p.add_argument("--reason", "-r", required=True, help="Justification")
    add_p.add_argument("--approved-by", "-a", required=True, help="Approver")
    add_p.add_argument("--expires", default="", help="Expiry date (ISO 8601)")
    add_p.add_argument("--scope", default="", help="File pattern scope")
    add_p.add_argument("--file", "-f", default=None, help="Exemptions file")

    list_p = sub.add_parser("list", help="List all exemptions")
    list_p.add_argument("--format", choices=["text", "json"], default="text")
    list_p.add_argument("--file", "-f", default=None, help="Exemptions file")

    revoke_p = sub.add_parser("revoke", help="Revoke an exemption")
    revoke_p.add_argument("exemption_id", help="Exemption ID to revoke")
    revoke_p.add_argument("--by", required=True, help="Who is revoking")
    revoke_p.add_argument("--file", "-f", default=None, help="Exemptions file")

    audit_p = sub.add_parser("audit", help="Show audit trail")
    audit_p.add_argument("--limit", type=int, default=20, help="Max events")
    audit_p.add_argument("--file", "-f", default=None, help="Exemptions file")

    args = parser.parse_args(argv)
    if not args.action:
        parser.print_help()
        return 0

    from .exemptions import (
        create_exemption,
        format_audit_trail_text,
        format_exemptions_json,
        format_exemptions_text,
        get_audit_trail,
        load_exemptions,
        revoke_exemption,
        save_exemptions,
    )

    store_path = args.file or ".mcpaudit-exemptions.json"
    store = load_exemptions(store_path)

    if args.action == "add":
        ex, errors = create_exemption(
            finding_rule=args.rule,
            reason=args.reason,
            approved_by=args.approved_by,
            expires_at=args.expires,
            scope=args.scope,
            store=store,
        )
        if errors:
            for e in errors:
                print(f"Error: {e}", file=sys.stderr)
            return 1
        save_exemptions(store, store_path)
        print(f"Exemption created: {ex.exemption_id} for {ex.finding_rule}")
        return 0

    if args.action == "list":
        fmt = getattr(args, "format", "text")
        if fmt == "json":
            print(format_exemptions_json(store))
        else:
            print(format_exemptions_text(store))
        return 0

    if args.action == "revoke":
        success, msg = revoke_exemption(
            args.exemption_id, args.by, store,
        )
        if not success:
            print(f"Error: {msg}", file=sys.stderr)
            return 1
        save_exemptions(store, store_path)
        print(msg)
        return 0

    if args.action == "audit":
        events = get_audit_trail(store, limit=args.limit)
        print(format_audit_trail_text(events))
        return 0

    return 0


def _handle_threat_model(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit threat-model' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit threat-model",
        description="MCP Threat Model Generator — STRIDE-based threat modeling",
    )
    parser.add_argument("path", help="File or directory to scan")
    parser.add_argument("--format", "-f", choices=["text", "json", "html"],
                        default="text", help="Output format")
    parser.add_argument("--output", "-o", default=None, help="Output file path")
    args = parser.parse_args(argv)

    from .scanner import MCPSecurityScanner
    from .threat_model import (
        generate_threat_model, format_threat_model_text,
        format_threat_model_json, format_threat_model_html,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    scanner = MCPSecurityScanner()
    summary = scanner.scan_path(target)
    model = generate_threat_model(summary.to_dict())

    if args.format == "json":
        output = format_threat_model_json(model)
    elif args.format == "html":
        output = format_threat_model_html(model)
    else:
        output = format_threat_model_text(model)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Threat model written to {args.output}")
    else:
        print(output)
    return 0


def _handle_compliance_gaps(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit compliance-gaps' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit compliance-gaps",
        description="MCP Compliance Gap Analyzer — Multi-framework gap analysis",
    )
    parser.add_argument("path", help="File or directory to scan")
    parser.add_argument("--frameworks", default=None,
                        help="Comma-separated frameworks (owasp,nist,eu_ai_act,soc2,hipaa,pci_dss)")
    parser.add_argument("--format", "-f", choices=["text", "json", "html"],
                        default="text", help="Output format")
    parser.add_argument("--output", "-o", default=None, help="Output file path")
    args = parser.parse_args(argv)

    from .scanner import MCPSecurityScanner
    from .compliance_gaps import (
        analyze_gaps, format_gaps_text, format_gaps_json, format_gaps_html,
    )

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    scanner = MCPSecurityScanner()
    summary = scanner.scan_path(target)

    fw_list = args.frameworks.split(",") if args.frameworks else None
    analysis = analyze_gaps(summary.to_dict(), fw_list)

    if args.format == "json":
        output = format_gaps_json(analysis)
    elif args.format == "html":
        output = format_gaps_html(analysis)
    else:
        output = format_gaps_text(analysis)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Compliance gap report written to {args.output}")
    else:
        print(output)
    return 0


def _handle_history_analytics(argv: Sequence[str]) -> int:
    """Handle 'mcpaudit history-analytics' subcommand."""
    parser = argparse.ArgumentParser(
        prog="mcpaudit history-analytics",
        description="MCP Scan History Analytics — MTTR, trends, predictions",
    )
    parser.add_argument("--history-dir", default=None,
                        help="Directory with scan history files")
    parser.add_argument("--target-score", type=int, default=None,
                        help="Target weighted score for prediction")
    parser.add_argument("--format", "-f", choices=["text", "json", "html"],
                        default="text", help="Output format")
    parser.add_argument("--output", "-o", default=None, help="Output file path")
    args = parser.parse_args(argv)

    from .history import load_history
    from .history_analytics import (
        analyze_history, predict_target_date,
        format_analytics_text, format_analytics_json, format_analytics_html,
    )

    history_dir = Path(args.history_dir) if args.history_dir else None
    entries = load_history(history_dir)

    if not entries:
        print("No scan history found. Run scans with --save-history first.")
        return 1

    analytics = analyze_history(entries)

    if args.target_score is not None:
        pred = predict_target_date(analytics, args.target_score)
        analytics.predictions.append(pred)

    if args.format == "json":
        output = format_analytics_json(analytics)
    elif args.format == "html":
        output = format_analytics_html(analytics)
    else:
        output = format_analytics_text(analytics)

    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Analytics report written to {args.output}")
    else:
        print(output)
    return 0


# Subcommand dispatch table
_SUBCOMMANDS: dict[str, object] = {
    "guard": _handle_guard,
    "diff": _handle_diff,
    "flows": _handle_flows,
    "plugins": _handle_plugins,
    "live": _handle_live,
    "probes": _handle_probes,
    "scan-all": _handle_scan_all,
    "summary": _handle_summary,
    "generate": _handle_generate,
    "generate-rule": _handle_generate_rule,
    "fingerprint": _handle_fingerprint,
    "deps": _handle_deps,
    "ci": _handle_ci,
    "sbom": _handle_sbom,
    "playbook": _handle_playbook,
    "playbooks": _handle_playbook,
    "regression": _handle_regression,
    "secrets": _handle_secrets,
    "sarif": _handle_sarif,
    "coverage": _handle_coverage,
    "dep-vuln": _handle_dep_vuln,
    "drift": _handle_drift,
    "attack-paths": _handle_attack_paths,
    "supply-chain": _handle_supply_chain,
    "scorecard": _handle_scorecard,
    "ci-template": _handle_ci_template,
    "protocol": _handle_protocol,
    "detect-secrets": _handle_detect_secrets,
    "benchmark": _handle_benchmark,
    "threats": _handle_threats,
    "compliance": _handle_compliance,
    "cache": _handle_cache,
    "risk-score": _handle_risk_score,
    "aggregate": _handle_aggregate,
    "rule-impact": _handle_rule_impact,
    "harden": _handle_harden,
    "audit": _handle_audit,
    "metrics": _handle_metrics,
    "dashboard": _handle_dashboard,
    "profiles": _handle_profiles,
    "remediate": _handle_remediate,
    "fleet": _handle_fleet,
    "annotate": _handle_annotate,
    "rule-sdk": _handle_rule_sdk,
    "report": _handle_report,
    "registry": _handle_registry,
    "exempt": _handle_exempt,
    "threat-model": _handle_threat_model,
    "compliance-gaps": _handle_compliance_gaps,
    "history-analytics": _handle_history_analytics,
}


def main(argv: Sequence[str] | None = None) -> int:
    """Main CLI entry point.

    Args:
        argv: Command line arguments (defaults to sys.argv[1:]).

    Returns:
        Exit code: 0=clean, 1=high findings, 2=critical findings.
    """
    # Check for subcommands first
    raw_argv = list(argv) if argv is not None else sys.argv[1:]
    if raw_argv and raw_argv[0] in _SUBCOMMANDS:
        subcmd = raw_argv[0]
        handler = _SUBCOMMANDS[subcmd]
        return handler(raw_argv[1:])

    parser = build_parser()
    args = parser.parse_args(argv)

    # Handle --init
    if args.init:
        config_path = create_default_config()
        print(f"Configuration file created: {config_path}")
        return 0

    # Handle --history
    history_dir = Path(args.history_dir) if args.history_dir else None
    if args.history:
        from .history import format_history_text, load_history
        entries = load_history(history_dir)
        print(format_history_text(entries))
        return 0

    # Handle --history-dashboard
    if args.history_dashboard:
        from .history import generate_dashboard_html, load_history
        entries = load_history(history_dir)
        html = generate_dashboard_html(entries)
        Path(args.history_dashboard).write_text(html, encoding="utf-8")
        print(f"Dashboard written to {args.history_dashboard}")
        return 0

    # Handle --list-rules
    if args.list_rules:
        from .ts_rules import get_all_ts_rules
        from .go_rules import get_all_go_rules
        py_rules = get_all_rules()
        ts_rules = get_all_ts_rules()
        go_rules = get_all_go_rules()
        print(f"\n{'ID':<16} {'Severity':<10} {'Name'}")
        print(f"{'─' * 16} {'─' * 10} {'─' * 40}")
        print("  Python rules:")
        for rule in sorted(py_rules, key=lambda r: r.rule_id):
            print(f"  {rule.rule_id:<14} {rule.severity.value:<10} {rule.name}")
        print(f"\n  TypeScript/JavaScript rules:")
        for rule in sorted(ts_rules, key=lambda r: r.rule_id):
            print(f"  {rule.rule_id:<14} {rule.severity.value:<10} {rule.name}")
        print(f"\n  Go rules:")
        for rule in sorted(go_rules, key=lambda r: r.rule_id):
            print(f"  {rule.rule_id:<14} {rule.severity.value:<10} {rule.name}")
        total = len(py_rules) + len(ts_rules) + len(go_rules)
        print(f"\n{total} rules available ({len(py_rules)} Python, {len(ts_rules)} TS/JS, {len(go_rules)} Go)")
        return 0

    # Handle --explain RULE_ID
    if args.explain:
        from .ts_rules import get_all_ts_rules
        from .go_rules import get_all_go_rules
        py_rules = get_all_rules()
        ts_rules = get_all_ts_rules()
        go_rules = get_all_go_rules()
        rule_map: dict[str, object] = {r.rule_id: r for r in py_rules}
        rule_map.update({r.rule_id: r for r in ts_rules})
        rule_map.update({r.rule_id: r for r in go_rules})
        rule_id = args.explain.upper()
        if rule_id not in rule_map:
            print(f"Error: unknown rule '{args.explain}'", file=sys.stderr)
            available = ", ".join(sorted(rule_map.keys()))
            print(f"Available rules: {available}", file=sys.stderr)
            return 1
        rule = rule_map[rule_id]
        print(f"\nRule:     {rule.rule_id}")
        print(f"Name:     {rule.name}")
        print(f"Severity: {rule.severity.value}")
        if rule.cwe:
            print(f"CWE:      {rule.cwe}")
        print(f"\n{rule.detailed_description}")
        return 0

    # Handle --list-profiles
    if args.list_profiles:
        from .profiles import format_profiles_text
        print(format_profiles_text())
        return 0

    # Require path if not --init, --list-rules, or --explain
    if args.path is None:
        parser.error("the following arguments are required: path")

    # Load config
    config_path = Path(args.config) if args.config else None
    config = load_config(config_path)

    target = Path(args.path)
    if not target.exists():
        print(f"Error: {target} does not exist", file=sys.stderr)
        return 1

    # Build scanner with filtered rules
    all_rules = get_all_rules()
    filtered_rules = _filter_rules(all_rules, config)

    # Apply compliance profile if specified
    active_profile = None
    if args.profile:
        from .profiles import apply_profile_to_rules, get_profile
        active_profile = get_profile(args.profile)
        filtered_rules = apply_profile_to_rules(active_profile, filtered_rules)

    # CLI --exclude overrides (comma-separated rule IDs)
    if args.exclude:
        exclude_ids = {r.strip().upper() for r in args.exclude.split(",")}
        filtered_rules = [r for r in filtered_rules if r.rule_id not in exclude_ids]

    # Load plugin rules if --plugin-dir is specified
    plugin_rules: list[Rule] | None = None
    if args.plugin_dir:
        from .plugins import discover_plugins
        plugin_dir = Path(args.plugin_dir)
        if not plugin_dir.exists():
            print(f"Error: plugin directory not found: {plugin_dir}", file=sys.stderr)
            return 1
        plugin_rules = discover_plugins(plugin_dir)

    deobfuscate_enabled = not args.no_deobfuscate
    scanner = MCPSecurityScanner(
        rules=filtered_rules,
        deobfuscate_enabled=deobfuscate_enabled,
        plugin_rules=plugin_rules,
    )

    # Handle --watch mode (runs scanner internally, blocks until Ctrl+C)
    if args.watch:
        from .watcher import FileWatcher
        watcher = FileWatcher(
            target,
            scanner,
            interval=args.watch_interval,
        )
        watcher.run()
        return 0

    summary = scanner.scan(target)

    # Handle --fix or --fix-dry-run
    if args.fix or args.fix_dry_run:
        from .fixer import (
            apply_fixes,
            build_rule_map,
            format_fix_applied,
            format_fix_dry_run,
            suggest_fixes,
        )
        rule_map = build_rule_map(filtered_rules)
        fix_results = []
        for scan_result in summary.results:
            if scan_result.findings:
                fr = suggest_fixes(scan_result.file, scan_result.findings, rule_map)
                fix_results.append(fr)

        if args.fix_dry_run:
            output = format_fix_dry_run(fix_results)
            print(output)
            return summary.exit_code

        # --fix: apply and report
        for fr in fix_results:
            if fr.suggestions:
                apply_fixes(fr)
        output = format_fix_applied(fix_results)
        print(output)
        return summary.exit_code

    # Handle --create-baseline
    if args.create_baseline:
        from .baseline import create_baseline
        baseline_json = create_baseline(summary)
        Path(args.create_baseline).write_text(baseline_json, encoding="utf-8")
        count = summary.total_findings
        print(f"Baseline created: {args.create_baseline} ({count} findings)")
        return 0

    # Apply baseline (suppress known findings)
    baseline_suppressed = 0
    if args.baseline:
        from .baseline import apply_baseline, load_baseline
        baseline_path = Path(args.baseline)
        if not baseline_path.exists():
            print(f"Error: baseline file not found: {baseline_path}", file=sys.stderr)
            return 1
        fingerprints = load_baseline(baseline_path)
        baseline_suppressed = apply_baseline(summary, fingerprints)

    # Exclude paths from results
    if config.exclude_paths:
        summary.results = [
            r for r in summary.results
            if not _is_excluded_path(Path(r.file), config.exclude_paths)
        ]

    # Determine severity threshold (CLI overrides config, profile overrides both)
    severity_filter = args.severity or config.severity_threshold
    if active_profile and not args.severity:
        # Profile sets severity threshold unless CLI --severity overrides
        severity_filter = active_profile.min_severity.value.lower()
    if severity_filter and severity_filter != "info":
        severity_order = list(Severity)
        min_idx = [s.value.lower() for s in severity_order].index(severity_filter)
        for r in summary.results:
            r.findings = [
                f for f in r.findings
                if severity_order.index(f.severity) <= min_idx
            ]

    # Compute trust score (always, for --min-score gate and output)
    from .trust_score import compute_summary_trust_score, format_trust_score_text
    trust_result = compute_summary_trust_score(summary)

    # Save to history if requested (before quiet check so -q + --save-history works)
    if args.save_history:
        from .history import save_scan
        summary_dict = summary.to_dict()
        summary_dict["summary"]["trust_score"] = trust_result.to_dict()
        history_path = save_scan(
            summary_dict,
            history_dir=history_dir,
            label=args.history_label,
        )
        if not args.quiet:
            print(f"\nHistory saved: {history_path}")

    # --policy evaluation
    if args.policy:
        from .policy import evaluate_policy, format_policy_json, format_policy_text, load_policy
        policy_path = Path(args.policy)
        if not policy_path.exists():
            print(f"Error: policy file not found: {policy_path}", file=sys.stderr)
            return 1
        policy = load_policy(policy_path)
        policy_result = evaluate_policy(policy, summary, trust_score=trust_result.score)
        if not args.quiet:
            output_format = args.format or config.output_format or "text"
            if output_format == "json":
                print(format_policy_json(policy_result))
            else:
                print(format_policy_text(policy_result))
        if not policy_result.passed:
            return 1

    # --min-score CI/CD gate
    if args.min_score is not None and trust_result.score < args.min_score:
        if not args.quiet:
            print(
                f"\nTrust score {trust_result.score} < minimum {args.min_score}",
                file=sys.stderr,
            )
        return 1

    # Quiet mode: exit code only
    if args.quiet:
        return summary.exit_code

    # Collect all findings for taxonomy/remediation modes
    all_findings = [f for r in summary.results for f in r.findings]

    # Taxonomy mode: group by threat category
    if args.taxonomy:
        from .taxonomy import format_taxonomy_text
        output = format_taxonomy_text(all_findings)
        if args.output:
            Path(args.output).write_text(output, encoding="utf-8")
            print(f"Taxonomy report written to {args.output}")
        else:
            print(output)
        return summary.exit_code

    # Dashboard mode: generate self-contained HTML dashboard
    if args.dashboard is not None:
        from .summary_dashboard import generate_dashboard
        dashboard_html = generate_dashboard(summary, title="mcpaudit Scan Dashboard")
        Path(args.dashboard).write_text(dashboard_html, encoding="utf-8")
        print(f"Dashboard written to {args.dashboard}")
        if not args.format:
            return summary.exit_code

    # Compliance report mode: OWASP Agentic Top 10 HTML report
    if args.compliance_report is not None:
        from .compliance import build_compliance_report, generate_compliance_html
        project_name = target.stem if target.is_file() else target.name
        report = build_compliance_report(summary, project_name=project_name)
        html = generate_compliance_html(report)
        out_path = Path(args.compliance_report)
        out_path.write_text(html, encoding="utf-8")
        print(f"OWASP compliance report written to {out_path}")
        return summary.exit_code

    # Determine output format (CLI overrides config, profile may suggest)
    output_format = args.format or config.output_format
    if not output_format and active_profile:
        output_format = active_profile.output_format
    output_format = output_format or "text"

    # Format output
    if output_format == "json":
        import json as _json
        summary_dict = summary.to_dict()
        summary_dict["summary"]["trust_score"] = trust_result.to_dict()
        output = _json.dumps(summary_dict, indent=2, ensure_ascii=False)
    elif output_format == "html":
        from .reporters.html_reporter import format_html
        output = format_html(summary)
    elif output_format == "sarif":
        from .reporters.sarif_reporter import format_sarif
        output = format_sarif(summary.results)
    else:
        output = format_summary_text(
            summary, show_remediation=args.remediation,
        )
        # Append trust score to text output
        output += "\n" + format_trust_score_text(trust_result) + "\n"

    # Write output
    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)

    # Show fix statistics if requested
    if args.fix_stats:
        from .fixer import (
            build_rule_map,
            compute_fix_stats,
            format_fix_stats,
            suggest_fixes,
        )
        rule_map = build_rule_map(filtered_rules)
        fix_results = []
        for scan_result in summary.results:
            if scan_result.findings:
                fr = suggest_fixes(scan_result.file, scan_result.findings, rule_map)
                fix_results.append(fr)
        stats = compute_fix_stats(fix_results, total_findings=summary.total_findings)
        print(format_fix_stats(stats))

    # Profile fail threshold check
    if active_profile:
        from .profiles import check_fail_threshold
        total_filtered = sum(len(r.findings) for r in summary.results)
        if check_fail_threshold(active_profile, total_filtered):
            return max(summary.exit_code, 1)

    return summary.exit_code


if __name__ == "__main__":
    sys.exit(main())
