"""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 /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(
        "--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 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.
    """
    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
        py_rules = get_all_rules()
        ts_rules = get_all_ts_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}")
        total = len(py_rules) + len(ts_rules)
        print(f"\n{total} rules available ({len(py_rules)} Python, {len(ts_rules)} TS/JS)")
        return 0

    # Handle --explain RULE_ID
    if args.explain:
        from .ts_rules import get_all_ts_rules
        py_rules = get_all_rules()
        ts_rules = get_all_ts_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_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

    # 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)

    # 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]

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

    # 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)
    severity_filter = args.severity or config.severity_threshold
    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}")

    # --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

    # 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)
    output_format = args.format or config.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)

    return summary.exit_code


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