"""CLI entry point using Click — defines all commands and options."""

from __future__ import annotations

import logging
import os
import sys
from pathlib import Path
from typing import Optional

import click

from . import __version__

logger = logging.getLogger("oncecheck")


def _setup_logging(verbose: bool = False) -> None:
    """Configure logging for the CLI."""
    level = logging.DEBUG if verbose else logging.WARNING
    logging.basicConfig(
        level=level,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        datefmt="%H:%M:%S",
    )


@click.group(invoke_without_command=True)
@click.version_option(version=__version__, prog_name="Oncecheck")
@click.pass_context
def cli(ctx):
    """Oncecheck — scan iOS, Android, and Web projects for compliance risks."""
    if ctx.invoked_subcommand is None:
        _interactive_flow()


def _interactive_flow() -> None:
    """Full interactive welcome screen + menu-driven scan flow."""
    from .auth.api import check_scan_quota, record_scan
    from .auth.login import ensure_authenticated, login_flow
    from .auth.token_store import load_token
    from .engine.runner import run_scan
    from .scanners.detector import Platform, DetectionResult, detect_platforms
    from .engine.reporter import export_json, export_text, export_sarif
    from .ui.rich_console import (
        console,
        print_welcome,
        print_step_status,
        prompt_main_menu,
        prompt_platform,
        prompt_project_path,
        prompt_post_scan,
        prompt_export_format,
        prompt_export_location,
        print_header,
        print_scan_result,
        print_footer,
    )
    from .ui.interactive import show_interactive
    from .rules.sync import get_policy_sync_status, maybe_auto_sync

    print_welcome()

    while True:
        choice = prompt_main_menu()

        if choice == "scan":
            total_steps = 6
            print_step_status(1, total_steps, "Policy sync", "running")
            maybe_auto_sync()
            status = get_policy_sync_status()
            if not status["healthy"]:
                print_step_status(1, total_steps, "Policy sync", "warn", f"missing {status['missing']}, stale {status['stale']}")
                console.print(
                    f"  [#eab308]Policy sources not fully fresh[/#eab308] "
                    f"(missing: {status['missing']}, stale: {status['stale']}). "
                    "Run `oncecheck rules sync` from CLI for a full refresh.\n"
                )
            else:
                print_step_status(1, total_steps, "Policy sync", "done", "fresh")
            # --- Auth gate ---
            try:
                print_step_status(2, total_steps, "Authentication", "running")
                token_data = ensure_authenticated()
                print_step_status(2, total_steps, "Authentication", "done")
            except SystemExit:
                raise
            except (OSError, ConnectionError, TimeoutError) as exc:
                print_step_status(2, total_steps, "Authentication", "failed", str(exc))
                console.print(f"\n  [bold #ef4444]Auth error:[/bold #ef4444] {exc}\n")
                import readchar
                console.print("  [dim]Press any key to continue...[/dim]")
                readchar.readkey()
                continue

            # --- Quota gate (server-side) ---
            try:
                print_step_status(3, total_steps, "Plan and quota", "running")
                quota = check_scan_quota(token_data["access_token"])
                print_step_status(3, total_steps, "Plan and quota", "done", f"{quota['used']}/{quota['limit']} today")
            except PermissionError:
                print_step_status(3, total_steps, "Plan and quota", "failed", "session expired")
                console.print("\n  [bold #ef4444]Session expired.[/bold #ef4444] Select Login to re-authenticate.\n")
                import readchar
                console.print("  [dim]Press any key to continue...[/dim]")
                readchar.readkey()
                continue
            except ConnectionError as exc:
                print_step_status(3, total_steps, "Plan and quota", "failed", str(exc))
                console.print(f"\n  [bold #ef4444]Server error:[/bold #ef4444] {exc}\n")
                import readchar
                console.print("  [dim]Press any key to continue...[/dim]")
                readchar.readkey()
                continue

            if not quota["allowed"]:
                console.print(f"\n  [bold #eab308]Scan limit reached[/bold #eab308] ({quota['used']}/{quota['limit']} scans today)")
                console.print("  Upgrade to [bold]Team[/bold] for unlimited scans: [#eeef20]https://www.oncecheck.com/dashboard/upgrade[/#eeef20]\n")
                import readchar
                console.print("  [dim]Press any key to continue...[/dim]")
                readchar.readkey()
                continue

            user_plan = quota.get("plan", "starter")

            # --- Scan flow ---
            project_path_str = prompt_project_path()
            project_path = Path(project_path_str).resolve()

            platform_choice = prompt_platform()
            console.clear()

            print_step_status(4, total_steps, "Platform detection", "running")
            if platform_choice == "auto":
                detections = detect_platforms(project_path)
                if not detections:
                    print_step_status(4, total_steps, "Platform detection", "failed", "no platform detected")
                    console.print("  [bold #ef4444]Could not detect project type.[/bold #ef4444]")
                    console.print("  [dim]Try selecting the platform manually.[/dim]\n")
                    import readchar
                    readchar.readkey()
                    continue
                print_step_status(4, total_steps, "Platform detection", "done", f"{len(detections)} target(s)")
            else:
                detections = [
                    DetectionResult(
                        platform=Platform(platform_choice),
                        project_root=project_path,
                        project_type="Manual",
                        confidence=1.0,
                    )
                ]
                print_step_status(4, total_steps, "Platform detection", "done", "manual")

            results = []
            print_step_status(5, total_steps, "Run scanners", "running")
            for detection in detections:
                console.print(f"  [dim]Scanning {detection.platform.value.upper()} ({detection.project_type})...[/dim]")
                result = run_scan(detection, user_plan=user_plan)
                results.append(result)
            print_step_status(5, total_steps, "Run scanners", "done", f"{len(results)} scan(s)")

            # Record the completed scan (increments counter)
            try:
                record_scan(token_data["access_token"])
            except (PermissionError, ConnectionError):
                pass  # Don't block results if recording fails

            print_step_status(6, total_steps, "Render report", "running")
            console.clear()
            print_header()
            for result in results:
                print_scan_result(result)
            print_footer(results)
            print_step_status(6, total_steps, "Render report", "done")

            # Post-scan menu
            if any(r.findings for r in results):
                import readchar
                console.print("  [dim]Press any key to continue...[/dim]")
                readchar.readkey()
                action = prompt_post_scan(is_team=(user_plan == "team"))
                if action == "browse":
                    show_interactive(results)
                elif action == "export":
                    fmt = prompt_export_format()
                    save_dir = prompt_export_location()
                    ext = "txt" if fmt == "text" else fmt
                    filename = f"oncecheck-results.{ext}"
                    output_path = str(Path(save_dir) / filename)
                    try:
                        if fmt == "json":
                            export_json(results, output_path)
                        elif fmt == "text":
                            Path(output_path).write_text(export_text(results))
                        elif fmt == "sarif":
                            export_sarif(results, output_path)
                        console.print(f"\n  [#22c55e]Exported to {output_path}[/#22c55e]\n")
                    except (ValueError, FileNotFoundError, OSError) as exc:
                        console.print(f"\n  [bold #ef4444]Export failed:[/bold #ef4444] {exc}\n")
                    readchar.readkey()
                elif action == "exit":
                    console.print("\n  [dim]Goodbye![/dim]\n")
                    break
            else:
                import readchar
                console.print("  [dim]Press any key to continue...[/dim]")
                readchar.readkey()

        elif choice == "login":
            login_flow()
            console.print()

        elif choice == "status":
            console.clear()
            token = load_token()
            if token:
                plan = token.get("plan", "Unknown")
                email = token.get("email", "Unknown")
                console.print(f"\n  [#22c55e]Authenticated[/#22c55e] \u2014 {email}")
                console.print(f"  Plan: [bold]{plan}[/bold]\n")
            else:
                console.print("\n  [#eab308]Not authenticated.[/#eab308] Select Login to sign in.\n")
            import readchar
            console.print("  [dim]Press any key to continue...[/dim]")
            readchar.readkey()

        elif choice == "exit":
            console.clear()
            console.print("\n  [dim]Goodbye![/dim]\n")
            break


@cli.command()
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option(
    "--platform", "-p",
    type=click.Choice(["ios", "android", "web", "auto"], case_sensitive=False),
    default="auto",
    help="Force a specific platform instead of auto-detecting.",
)
@click.option(
    "--format", "-f", "output_format",
    type=click.Choice(["rich", "json", "text", "sarif"], case_sensitive=False),
    default="rich",
    help="Output format.",
)
@click.option("--output", "-o", type=click.Path(), default=None, help="Write results to file.")
@click.option("--fail-on", type=click.Choice(["FAIL", "WARN"], case_sensitive=False), default=None,
              help="Exit with code 2 if findings of this severity or higher exist.")
@click.option("--interactive", "-i", is_flag=True, help="Launch interactive findings browser.")
@click.option(
    "--policy-sync",
    type=click.Choice(["off", "auto", "force"], case_sensitive=False),
    default="auto",
    show_default=True,
    help="Sync official policy sources before scanning.",
)
@click.option(
    "--policy-max-age-days",
    type=int,
    default=14,
    show_default=True,
    help="Treat policy sources older than this as stale.",
)
@click.option(
    "--require-fresh-policies",
    is_flag=True,
    help="Fail the scan if policy sources are missing or stale.",
)
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging.")
def scan(
    path: str,
    platform: str,
    output_format: str,
    output: Optional[str],
    fail_on: Optional[str],
    interactive: bool,
    policy_sync: str,
    policy_max_age_days: int,
    require_fresh_policies: bool,
    verbose: bool,
):
    """Scan a project directory for compliance issues."""
    from rich.console import Console
    from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn

    from .auth.api import check_scan_quota, record_scan
    from .auth.login import ensure_authenticated
    from .engine.config import ScanConfig
    from .engine.reporter import export_json, export_sarif, export_text
    from .engine.runner import run_scan, ScanResult
    from .scanners.detector import Platform, detect_platforms
    from .ui.rich_console import print_header, print_scan_result, print_footer
    from .ui.rich_console import print_step_status
    from .ui.interactive import show_interactive
    from .rules.sync import get_policy_sync_status, maybe_auto_sync, sync_policies

    _setup_logging(verbose)

    console = Console()
    project_path = Path(path).resolve()
    total_steps = 7

    # --- Live policy sync ---
    print_step_status(1, total_steps, "Policy sync", "running")
    if policy_sync == "force":
        sync_result = sync_policies(force=True)
        step_state = "done" if sync_result["failed"] == 0 else "warn"
        console.print(
            f"[dim]Policy sync: {sync_result['updated']} updated, "
            f"{sync_result['unchanged']} unchanged, {sync_result['failed']} failed[/dim]"
        )
        print_step_status(1, total_steps, "Policy sync", step_state)
    elif policy_sync == "auto":
        sync_result = maybe_auto_sync(stale_after_days=policy_max_age_days)
        if sync_result is not None:
            step_state = "done" if sync_result["failed"] == 0 else "warn"
            console.print(
                f"[dim]Policy sync: {sync_result['updated']} updated, "
                f"{sync_result['unchanged']} unchanged, {sync_result['failed']} failed[/dim]"
            )
            print_step_status(1, total_steps, "Policy sync", step_state)
        else:
            print_step_status(1, total_steps, "Policy sync", "done", "already fresh")
    else:
        print_step_status(1, total_steps, "Policy sync", "done", "disabled")

    policy_status = get_policy_sync_status(stale_after_days=policy_max_age_days)
    if not policy_status["healthy"]:
        message = (
            f"Policy sources not fully fresh "
            f"(missing: {policy_status['missing']}, stale: {policy_status['stale']}). "
            "Run `oncecheck rules sync`."
        )
        if require_fresh_policies:
            print_step_status(1, total_steps, "Policy sync", "failed", "freshness required")
            console.print(f"[bold #ef4444]{message}[/bold #ef4444]")
            sys.exit(2)
        print_step_status(1, total_steps, "Policy sync", "warn", f"missing {policy_status['missing']}, stale {policy_status['stale']}")
        console.print(f"[#eab308]{message}[/#eab308]")

    # --- Load project config ---
    print_step_status(2, total_steps, "Load config", "running")
    config = ScanConfig(project_path)
    print_step_status(2, total_steps, "Load config", "done")
    logger.info("Config loaded: %d ignored rules, %d severity overrides",
                len(config.ignored_rules), len(config.severity_overrides))

    # --- Auth gate ---
    print_step_status(3, total_steps, "Authentication", "running")
    try:
        token_data = ensure_authenticated()
        print_step_status(3, total_steps, "Authentication", "done")
    except SystemExit:
        raise
    except (OSError, ConnectionError, TimeoutError) as exc:
        print_step_status(3, total_steps, "Authentication", "failed", str(exc))
        console.print(f"[bold #ef4444]Auth error:[/bold #ef4444] {exc}")
        sys.exit(1)

    # --- Quota gate (server-side) ---
    print_step_status(4, total_steps, "Plan and quota", "running")
    try:
        quota = check_scan_quota(token_data["access_token"])
        print_step_status(4, total_steps, "Plan and quota", "done", f"{quota['used']}/{quota['limit']} today")
    except PermissionError:
        print_step_status(4, total_steps, "Plan and quota", "failed", "session expired")
        console.print("[bold #ef4444]Session expired.[/bold #ef4444] Run `oncecheck login` to re-authenticate.")
        sys.exit(1)
    except ConnectionError as exc:
        print_step_status(4, total_steps, "Plan and quota", "failed", str(exc))
        console.print(f"[bold #ef4444]Server error:[/bold #ef4444] {exc}")
        sys.exit(1)

    if not quota["allowed"]:
        console.print(f"\n  [bold #eab308]Scan limit reached[/bold #eab308] ({quota['used']}/{quota['limit']} scans today)")
        console.print("  Upgrade to [bold]Team[/bold] for unlimited scans: [#eeef20]https://www.oncecheck.com/dashboard/upgrade[/#eeef20]\n")
        sys.exit(1)

    # --- Plan-gated features ---
    user_plan = quota.get("plan", "starter")
    if user_plan != "team":
        if output_format == "sarif":
            console.print("[bold #eab308]SARIF export requires the Team plan.[/bold #eab308]")
            console.print("Upgrade at: [#eeef20]https://www.oncecheck.com/dashboard/upgrade[/#eeef20]")
            sys.exit(1)
        if output and output_format != "rich":
            console.print("[bold #eab308]File export (--output) requires the Team plan.[/bold #eab308]")
            console.print("Upgrade at: [#eeef20]https://www.oncecheck.com/dashboard/upgrade[/#eeef20]")
            sys.exit(1)

    # --- Detect platforms ---
    print_step_status(5, total_steps, "Platform detection", "running")
    if platform == "auto":
        detections = detect_platforms(project_path)
        if not detections:
            print_step_status(5, total_steps, "Platform detection", "failed", "no platform detected")
            console.print("[bold #ef4444]Could not detect project type.[/bold #ef4444]")
            console.print("Use --platform to specify manually.")
            sys.exit(1)
        print_step_status(5, total_steps, "Platform detection", "done", f"{len(detections)} target(s)")
    else:
        from .scanners.detector import DetectionResult
        detections = [
            DetectionResult(
                platform=Platform(platform.lower()),
                project_root=project_path,
                project_type="Manual",
                confidence=1.0,
            )
        ]
        print_step_status(5, total_steps, "Platform detection", "done", "manual")

    # --- Run scans with progress ---
    print_step_status(6, total_steps, "Run scanners", "running")
    results: list[ScanResult] = []
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        TimeElapsedColumn(),
        console=console,
        transient=True,
    ) as progress:
        for detection in detections:
            task_desc = f"Scanning {detection.platform.value.upper()} ({detection.project_type})"
            progress.add_task(task_desc, total=None)
            result = run_scan(detection, config=config, user_plan=user_plan)
            results.append(result)
    print_step_status(6, total_steps, "Run scanners", "done", f"{len(results)} scan(s)")

    # --- Record the completed scan ---
    try:
        record_scan(token_data["access_token"])
    except (PermissionError, ConnectionError):
        pass  # Don't block results if recording fails

    # --- Resolve fail_on: CLI flag > config file ---
    effective_fail_on = fail_on or config.fail_on

    # --- Output ---
    print_step_status(7, total_steps, "Render report", "running")
    if output_format == "rich":
        print_header()
        for result in results:
            print_scan_result(result)
        print_footer(results)
        # Show suppression summary
        total_suppressed = sum(r.suppressed_count for r in results)
        if total_suppressed:
            console.print(f"  [dim]{total_suppressed} finding(s) suppressed via .oncecheckignore[/dim]")
        # Show plan gating summary
        total_gated = sum(r.gated_count for r in results)
        if total_gated:
            console.print(f"  [dim]{total_gated} additional rule(s) available with Team plan[/dim]")
            console.print(f"  [dim]Upgrade at: https://www.oncecheck.com/dashboard/upgrade[/dim]\n")
    elif output_format == "json":
        text = export_json(results, output)
        if not output:
            console.print(text)
    elif output_format == "text":
        text = export_text(results)
        if output:
            Path(output).write_text(text)
        else:
            console.print(text)
    elif output_format == "sarif":
        text = export_sarif(results, output)
        if not output:
            console.print(text)

    if output:
        console.print(f"  [#22c55e]Results written to {output}[/#22c55e]")
    print_step_status(7, total_steps, "Render report", "done")

    # --- Interactive mode ---
    if interactive:
        show_interactive(results)

    # --- Exit code ---
    _exit_code(results, effective_fail_on)


def _exit_code(results, fail_on: Optional[str]) -> None:
    """Set process exit code based on findings."""
    total_fail = sum(r.fail_count for r in results)
    total_warn = sum(r.warn_count for r in results)

    if fail_on == "FAIL" and total_fail > 0:
        sys.exit(2)
    elif fail_on == "WARN" and (total_fail > 0 or total_warn > 0):
        sys.exit(2)
    elif total_fail > 0:
        sys.exit(2)
    elif total_warn > 0:
        sys.exit(1)
    else:
        sys.exit(0)


@cli.command()
def login():
    """Authenticate with Oncecheck."""
    from .auth.login import login_flow
    login_flow()


@cli.command()
def logout():
    """Clear stored authentication credentials."""
    from .auth.login import logout
    logout()


@cli.command()
def status():
    """Show authentication and subscription status."""
    from rich.console import Console

    from .auth.token_store import load_token

    console = Console()
    token = load_token()
    if token:
        plan = token.get("plan", "Unknown")
        email = token.get("email", "Unknown")
        console.print(f"  [#22c55e]Authenticated[/#22c55e] \u2014 {email}")
        console.print(f"  Plan: [bold]{plan}[/bold]")
    else:
        console.print("  [#eab308]Not authenticated.[/#eab308] Run `oncecheck login` to sign in.")


@cli.group("rules")
def rules_group():
    """Manage rule catalogs and official policy source sync."""


@rules_group.command("sync")
@click.option("--force", is_flag=True, help="Force re-download all policy sources.")
@click.option("--timeout", type=int, default=20, show_default=True, help="HTTP timeout in seconds.")
def rules_sync(force: bool, timeout: int):
    """Fetch official policy pages used for rule maintenance."""
    from rich.console import Console

    from .rules.sync import sync_policies

    console = Console()
    result = sync_policies(force=force, timeout_seconds=timeout)
    console.print(
        f"  [#22c55e]Policy sync complete[/#22c55e] "
        f"(updated: {result['updated']}, unchanged: {result['unchanged']}, failed: {result['failed']})"
    )
    for item in result["results"]:
        if item["status"] == "failed":
            console.print(f"  [#ef4444]✗[/#ef4444] {item['name']}: {item['error']}")
        elif item["status"] == "updated":
            console.print(f"  [#22c55e]✓[/#22c55e] {item['name']} (updated)")
        else:
            console.print(f"  [dim]• {item['name']} (unchanged)[/dim]")


@rules_group.command("status")
@click.option(
    "--stale-after-days",
    type=int,
    default=14,
    show_default=True,
    help="Treat policy sources older than this as stale.",
)
def rules_status(stale_after_days: int):
    """Show freshness status of synced official policy sources."""
    from datetime import datetime, timezone

    from rich.console import Console

    from .rules.sync import get_policy_sync_status

    console = Console()
    status = get_policy_sync_status(stale_after_days=stale_after_days)
    color = "#22c55e" if status["healthy"] else "#eab308"
    console.print(
        f"  [{color}]Policy sync status[/{color}] "
        f"(missing: {status['missing']}, stale: {status['stale']}, total: {status['total']})"
    )
    for item in status["sources"]:
        synced = item["synced_at"]
        if synced:
            synced_text = datetime.fromtimestamp(synced, tz=timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
        else:
            synced_text = "never"
        marker = "stale" if item["stale"] else ("missing" if item["missing"] else "fresh")
        console.print(f"  - [{item['platform']}] {item['name']}: {marker} (last sync: {synced_text})")


def _platforms_for_filter(platform: str) -> list[str]:
    if platform == "all":
        return ["ios", "android", "web", "common"]
    return [platform]


def _platform_from_rule_id(rule_id: str) -> Optional[str]:
    if rule_id.startswith("IOS-"):
        return "ios"
    if rule_id.startswith("AND-"):
        return "android"
    if rule_id.startswith("WEB-"):
        return "web"
    if rule_id.startswith("CROSS-") or rule_id.startswith("SUPPLY-"):
        return "common"
    return None


@rules_group.command("list")
@click.option(
    "--platform",
    type=click.Choice(["all", "ios", "android", "web", "common"], case_sensitive=False),
    default="all",
    show_default=True,
    help="Filter by platform.",
)
@click.option(
    "--severity",
    type=click.Choice(["ALL", "FAIL", "WARN", "INFO"], case_sensitive=False),
    default="ALL",
    show_default=True,
    help="Filter by severity.",
)
@click.option(
    "--tier",
    type=click.Choice(["all", "starter", "team"], case_sensitive=False),
    default="all",
    show_default=True,
    help="Filter by plan tier.",
)
def rules_list(platform: str, severity: str, tier: str):
    """List available rules with filters."""
    from rich.console import Console
    from rich.table import Table

    from .rules.loader import load_rules

    console = Console()
    table = Table(title="Oncecheck Rule Catalog")
    table.add_column("ID", style="bold")
    table.add_column("Platform")
    table.add_column("Severity")
    table.add_column("Tier")
    table.add_column("Summary")

    severity = severity.upper()
    total = 0
    for p in _platforms_for_filter(platform.lower()):
        for rule in load_rules(p):
            if severity != "ALL" and rule.get("severity") != severity:
                continue
            rule_tier = str(rule.get("tier", ""))
            if tier != "all" and rule_tier != tier:
                continue
            total += 1
            table.add_row(
                str(rule.get("id", "")),
                p,
                str(rule.get("severity", "")),
                rule_tier or "-",
                str(rule.get("summary", "")),
            )

    if total == 0:
        console.print("  [#eab308]No rules matched filters.[/#eab308]")
        return

    console.print(table)
    console.print(f"  [dim]{total} rule(s) shown[/dim]")


@rules_group.command("show")
@click.argument("rule_id")
def rules_show(rule_id: str):
    """Show full details for a specific rule ID."""
    from rich.console import Console
    from rich.panel import Panel

    from .rules.loader import get_rule_by_id

    console = Console()
    rid = rule_id.strip().upper()
    platform = _platform_from_rule_id(rid)
    if not platform:
        console.print(f"  [#ef4444]Unknown rule ID prefix:[/#ef4444] {rid}")
        sys.exit(1)

    rule = get_rule_by_id(platform, rid)
    if not rule:
        console.print(f"  [#ef4444]Rule not found:[/#ef4444] {rid}")
        sys.exit(1)

    detection = rule.get("detection", {})
    if detection:
        import json
        detection_text = json.dumps(detection, indent=2, sort_keys=True)
    else:
        detection_text = "{}"

    body = (
        f"[bold]Platform:[/bold] {platform}\n"
        f"[bold]Severity:[/bold] {rule.get('severity', '')}\n"
        f"[bold]Tier:[/bold] {rule.get('tier', '-')}\n\n"
        f"[bold]Summary:[/bold]\n{rule.get('summary', '')}\n\n"
        f"[bold]Fix:[/bold]\n{rule.get('fix', '')}\n\n"
        f"[bold]Reference:[/bold]\n{rule.get('reference', '')}\n\n"
        f"[bold]Detection:[/bold]\n{detection_text}"
    )
    console.print(Panel.fit(body, title=f"Rule {rid}", border_style="cyan"))


@rules_group.command("doctor")
@click.option(
    "--stale-after-days",
    type=int,
    default=14,
    show_default=True,
    help="Treat policy sources older than this as stale.",
)
def rules_doctor(stale_after_days: int):
    """Run rule and policy health checks (useful for CI)."""
    from rich.console import Console

    from .rules.loader import validate_rules
    from .rules.sync import get_policy_sync_status

    console = Console()
    failures = 0

    for platform in ("ios", "android", "web", "common"):
        errors = validate_rules(platform)
        if errors:
            failures += len(errors)
            console.print(f"  [#ef4444]{platform}: {len(errors)} validation error(s)[/#ef4444]")
            for err in errors:
                console.print(f"    - {err}")
        else:
            console.print(f"  [#22c55e]{platform}: OK[/#22c55e]")

    status = get_policy_sync_status(stale_after_days=stale_after_days)
    if status["healthy"]:
        console.print("  [#22c55e]policy sync: fresh[/#22c55e]")
    else:
        failures += status["missing"] + status["stale"]
        console.print(
            "  [#ef4444]policy sync: not healthy[/#ef4444] "
            f"(missing: {status['missing']}, stale: {status['stale']})"
        )
        console.print("    Run `oncecheck rules sync --force`")

    if failures > 0:
        sys.exit(2)


@cli.command("completions")
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"]))
def completions(shell: str):
    """Generate shell completion script.

    \b
    Install completions:
      oncecheck completions bash >> ~/.bashrc
      oncecheck completions zsh >> ~/.zshrc
      oncecheck completions fish > ~/.config/fish/completions/oncecheck.fish
    """
    import subprocess

    env_var = "_ONCECHECK_COMPLETE"
    shell_map = {"bash": "bash_source", "zsh": "zsh_source", "fish": "fish_source"}

    result = subprocess.run(
        ["oncecheck"],
        env={**os.environ, env_var: shell_map[shell]},
        capture_output=True,
        text=True,
    )
    click.echo(result.stdout)


@cli.command("init")
@click.argument("path", default=".", type=click.Path(exists=True))
def init_config(path: str):
    """Generate a .oncecheckrc config and .oncecheckignore in the project."""
    from rich.console import Console

    console = Console()
    project_path = Path(path).resolve()
    rc_path = project_path / ".oncecheckrc"

    if rc_path.exists():
        console.print(f"  [#eab308]Config already exists:[/#eab308] {rc_path}")
    else:
        rc_path.write_text(
            "# Oncecheck configuration\n"
            "# https://oncecheck.com\n"
            "\n"
            "# Rules to suppress (same as .oncecheckignore)\n"
            "disabled_rules: []\n"
            "  # - IOS-SEC-001\n"
            "  # - WEB-OWASP-003\n"
            "\n"
            "# Override severity for specific rules\n"
            "severity_overrides: {}\n"
            "  # IOS-SEC-001: WARN\n"
            "  # WEB-A11Y-001: INFO\n"
            "\n"
            "# Exit code threshold (FAIL or WARN)\n"
            "# fail_on: FAIL\n"
        )
        console.print(f"  [#22c55e]Created config:[/#22c55e] {rc_path}")

    ignore_path = project_path / ".oncecheckignore"
    if ignore_path.exists():
        console.print(f"  [#eab308]Ignore file already exists:[/#eab308] {ignore_path}")
    else:
        ignore_path.write_text(
            "# Oncecheck — suppressed rules (one ID per line)\n"
            "# Lines starting with # are comments\n"
            "#\n"
            "# Example:\n"
            "# IOS-SEC-001\n"
            "# WEB-OWASP-003\n"
        )
        console.print(f"  [#22c55e]Created ignore file:[/#22c55e] {ignore_path}")
