"""Command-line interface (stdlib argparse only, to keep the base dep surface minimal).

Subcommands: scan, sanitize, web. The profile flag presets behavior over one engine.
Exit codes: 0 (no finding meets the fail threshold), 2 (one or more do), 1 (tool error).
"""
from __future__ import annotations

import argparse
import sys
import threading
from typing import Optional

from . import __version__


class _Heartbeat:
    """A stderr progress indicator so a long model pass never appears hung (V-9)."""

    def __init__(self, message: str):
        self.message = message
        self._stop = threading.Event()
        self._thread: Optional[threading.Thread] = None

    def __enter__(self) -> "_Heartbeat":
        def run() -> None:
            seconds = 0
            while not self._stop.wait(2.0):
                seconds += 2
                print(f"\r{self.message}... ({seconds}s)", end="", file=sys.stderr, flush=True)
        self._thread = threading.Thread(target=run, daemon=True)
        self._thread.start()
        return self

    def __exit__(self, *exc) -> None:
        self._stop.set()
        if self._thread:
            self._thread.join(timeout=0.2)
        print("", file=sys.stderr)

# Per-profile default fail-on thresholds (R-8).
PROFILE_DEFAULT_FAIL_ON = {
    "external-llm": "high",
    "repo": "high",
    "email": "medium",
    None: "high",
}
PROFILES = ["external-llm", "repo", "email"]


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="privsweep",
        description=(
            "Zero-config, no-egress pre-flight scanner and sanitizer for privileged "
            "content, PII, and confidential metadata. Not legal advice; detection is "
            "best-effort and does not certify a file clean."
        ),
    )
    parser.add_argument("--version", action="version", version=f"privsweep {__version__}")
    sub = parser.add_subparsers(dest="command", metavar="{scan,sanitize,web}")

    # --- scan ---
    scan = sub.add_parser("scan", help="Scan files/folders and report findings.")
    scan.add_argument("paths", nargs="+", help="Files or directories to scan.")
    scan.add_argument("--profile", choices=PROFILES, default=None,
                      help="Destination profile preset (external-llm, repo, email).")
    fmt = scan.add_mutually_exclusive_group()
    fmt.add_argument("--json", action="store_true", help="Emit JSON.")
    fmt.add_argument("--sarif", action="store_true", help="Emit SARIF 2.1.0 (for CI).")
    scan.add_argument("--fail-on", choices=["critical", "high", "medium", "low", "any"],
                      default=None, help="Severity at or above which the gate fails.")
    scan.add_argument("--privilege-endpoint", default=None,
                      help="OpenAI-compatible endpoint for the optional privilege "
                           "classifier (R-12). Off unless set.")
    scan.add_argument("--privilege-key", default=None,
                      help="Bearer key for the privilege endpoint, if required.")
    scan.add_argument("--no-ner", action="store_true",
                      help="Disable the optional NER tier even if installed.")
    scan.add_argument("--no-model", action="store_true",
                      help="Disable the local model pass (names in prose, unmarked "
                           "privilege). On by default (V-1).")
    scan.add_argument("--model-endpoint", default=None,
                      help="Use this OpenAI-compatible endpoint instead of the bundled "
                           "local model (must be 127.0.0.1 for the no-egress guarantee).")
    scan.add_argument("--max-pages", type=int, default=None,
                      help="Override the model-pass page cap (default 150). Larger files "
                           "skip the model pass unless raised.")
    scan.set_defaults(func=_cmd_scan)

    # --- sanitize ---
    san = sub.add_parser("sanitize", help="Write a sanitized safe copy of each file.")
    san.add_argument("paths", nargs="+", help="Files or directories to sanitize.")
    san.add_argument("--out", required=True, metavar="DIR",
                     help="Output directory for sanitized copies.")
    san.add_argument("--profile", choices=PROFILES, default=None,
                     help="Destination profile preset.")
    san.add_argument("--map-dir", default=None,
                     help="Private directory for the real-to-dummy mapping sidecar "
                          "(default ~/.privsweep/maps).")
    san.add_argument("--corpus-consistent", action="store_true",
                     help="Keep one mapping and date offset across all files (R-15).")
    san.add_argument("--no-ner", action="store_true",
                     help="Disable the optional NER tier even if installed.")
    san.set_defaults(func=_cmd_sanitize)

    # --- web ---
    web = sub.add_parser("web", help="Launch the local web UI (binds 127.0.0.1).")
    web.add_argument("--host", default="127.0.0.1", help="Bind host (default 127.0.0.1).")
    web.add_argument("--port", type=int, default=8765, help="Bind port (default 8765).")
    web.add_argument("--no-browser", action="store_true",
                     help="Do not open a browser window.")
    web.set_defaults(func=_cmd_web)

    return parser


def _resolve_fail_on(args) -> str:
    if getattr(args, "fail_on", None):
        return args.fail_on
    return PROFILE_DEFAULT_FAIL_ON.get(getattr(args, "profile", None), "high")


def _cmd_scan(args) -> int:
    from .engine import scan_path
    from .report import exit_code, render

    report = scan_path(args.paths, profile=args.profile)

    # Optional tiers attach here; both degrade silently when unavailable (R-11, R-12).
    if not getattr(args, "no_ner", False):
        try:
            from .ner import augment_with_ner  # noqa: WPS433
            augment_with_ner(args.paths, report)
        except ImportError:
            pass
    # Local bundled model (V-1): on by default; catches names in bare prose and judges
    # unmarked privilege. Degrades to a note if the model is absent; never crashes scan.
    if not getattr(args, "no_model", False):
        if getattr(args, "max_pages", None):
            import os as _os
            _os.environ["PRIVSWEEP_MAX_PAGES"] = str(args.max_pages)
        try:
            from .model import augment_with_model  # noqa: WPS433
            with _Heartbeat("  scanning with the local model"):
                augment_with_model(args.paths, report,
                                   endpoint=getattr(args, "model_endpoint", None))
        except Exception as exc:  # noqa: BLE001 - the model must never crash the scan
            report.note(f"model tier error ({type(exc).__name__}); "
                        f"deterministic results stand.")
    import os
    endpoint = (getattr(args, "privilege_endpoint", None)
                or os.environ.get("PRIVSWEEP_PRIVILEGE_ENDPOINT"))
    key = getattr(args, "privilege_key", None) or os.environ.get("PRIVSWEEP_PRIVILEGE_KEY")
    if endpoint:
        try:
            from .privilege import augment_with_privilege  # noqa: WPS433
            augment_with_privilege(args.paths, report, endpoint, key)
        except ImportError:
            pass

    fail_on = _resolve_fail_on(args)
    fmt = "json" if args.json else "sarif" if args.sarif else "human"
    out = render(report, fmt=fmt, fail_on=fail_on)
    print(out)
    if args.profile == "external-llm" and report.findings and fmt == "human":
        print("\n  external-llm profile: run `privsweep sanitize` to produce a safe copy.")
    return exit_code(report, fail_on)


def _cmd_sanitize(args) -> int:
    from .sanitize import sanitize_path

    result = sanitize_path(
        args.paths,
        out_dir=args.out,
        profile=args.profile,
        map_dir=args.map_dir,
        corpus_consistent=getattr(args, "corpus_consistent", False),
    )
    print(f"Sanitized {len(result.outputs)} file(s) into {args.out}")
    for o in result.outputs:
        verified = result.verified.get(o, True)
        print(f"  wrote {o}  [{'verified clean' if verified else 'NOT verified clean'}]")
    for mp in result.map_paths:
        print(f"  mapping sidecar: {mp}")
    for note in result.notes:
        print(f"  note: {note}")
    return 0


def _cmd_web(args) -> int:
    from .web import serve

    return serve(host=args.host, port=args.port, open_browser=not args.no_browser)


def main(argv: Optional[list[str]] = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    if not getattr(args, "command", None):
        parser.print_help()
        return 0
    try:
        return args.func(args)
    except KeyboardInterrupt:
        return 1
    except Exception as exc:  # noqa: BLE001 - map any tool error to exit 1
        print(f"privsweep: error: {type(exc).__name__}: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
