"""sshj.tailnet — tailscale peers + MagicDNS name targeting (B12/B16, leaf).

B2 step 2: moved verbatim from sshj_cli.py (single-file era) — no behavior
change. Owns TAILNET_MARK (the muted dot-matrix mark the UI renders for
tailnet rows) because it is the marker this module stamps.
"""
import json
import os
import shutil
import subprocess

# B12: tailnet device marker, shown in the NAME column's reserved last cell
# (ui.row_segments always appends it there, so truncation can never eat it).
# The full braille dot block — the single-cell terminal rendering of
# Tailscale's dot-matrix logo (a 3x3 grid of 9 dots; braille cells are 2x4,
# so the full block is the closest 1-wide approximation).
TAILNET_MARK = "⣿"


TS_TIMEOUT = 2             # seconds: tailscale status --json, bounded
TERMUX_SSH_USER = "u0_a360"  # Termux sshd default user (android peers)
TERMUX_SSH_PORT = "2222"     # Termux sshd default port


def tailnet_peers():
    """(self_ip, [peer, ...]) from `tailscale status --json`, or ("", []).

    Each peer: {"name", "ip" (first v4), "os", "online", "dnsname"}.
    Degrades to ("", []) when tailscale is missing, the call fails, or the
    JSON is unparseable — the tailnet is a source, never a crash (C3)."""
    if not shutil.which("tailscale"):
        return "", []
    try:
        r = subprocess.run(["tailscale", "status", "--json"],
                           capture_output=True, text=True,
                           timeout=TS_TIMEOUT)
        d = json.loads(r.stdout)
    except Exception:
        return "", []
    self_ip = ""
    for ip in d.get("TailscaleIPs", []):
        if not ip.startswith("fd") and ":" not in ip:
            self_ip = ip
            break
    peers = []
    for p in d.get("Peer", {}).values():
        ips = [ip for ip in p.get("TailscaleIPs", []) if ":" not in ip]
        if not ips or ips[0] == self_ip:
            continue  # no v4, or (defensively) ourselves
        peers.append({"name": p.get("HostName", ips[0]), "ip": ips[0],
                      "os": (p.get("OS") or "").lower(),
                      "online": bool(p.get("Online")),
                      "dnsname": (p.get("DNSName") or "").strip()})
    return self_ip, peers


# ------------------------ MagicDNS (B16) --------------------------
MAGICDNS_NAMESERVER = "100.100.100.100"   # tailscale's embedded DNS


def _magicdns_short(fqdn):
    """Peer DNSName FQDN -> the short MagicDNS name that actually resolves.

    MagicDNS serves each node by its FIRST label (e.g. `cw-phone`), so the
    short name is the first label of the FQDN; the tailnet domain
    (`<tailnet>.ts.net`) is dropped. None/'' -> ''."""
    if not fqdn:
        return ""
    name = fqdn.strip().rstrip(".")
    return name.split(".")[0]


def magicdns_on(self_dnsname, resolv_conf=None):
    """True when the local box can RESOLVE MagicDNS names, so tailnet rows
    can target the human-readable name instead of the IP.

    Detection (both must hold for a confident True):
      1. self has a `...ts.net` DNSName (we're on a MagicDNS tailnet), AND
      2. resolv.conf lists 100.100.100.100, OR `getent hosts <self-short>`
         resolves (covers systemd-resolved/WSL where the nameserver line
         lives elsewhere).
    Falsy self DNSName or a foreign domain => False (never a crash)."""
    if not self_dnsname or not self_dnsname.strip().rstrip(".").endswith(
            ".ts.net"):
        return False
    conf = resolv_conf or os.environ.get("SSHJ_RESOLV_CONF") \
        or "/etc/resolv.conf"
    try:
        with open(conf) as f:
            if MAGICDNS_NAMESERVER in f.read():
                return True
    except OSError:
        pass
    # getent fallback (the real resolver is authoritative for what ssh sees)
    try:
        r = subprocess.run(["getent", "hosts", _magicdns_short(self_dnsname)],
                           capture_output=True, text=True, timeout=2)
        return r.returncode == 0 and bool(r.stdout.strip())
    except Exception:
        return False


def ssh_host(h, with_user=False):
    """The ssh ADDRESS for a host: its MagicDNS short name when we can
    resolve it (B16), else the raw hostname/IP. `with_user` adds user@
    (connect uses it; probes stay bare so ssh tries both keys). Everything
    that builds an ssh address goes through here so the IP-fallback stays
    exact B12 behavior."""
    host = h.get("magicdns") or h.get("hostname") or h.get("alias", "")
    if with_user and h.get("user"):
        return f"{h['user']}@{host}"
    return host


def self_dnsname():
    """Our own MagicDNS FQDN from `tailscale status --json` (Self.DNSName),
    or "". One bounded call; degrades to "" like the rest of B12/B16."""
    if not shutil.which("tailscale"):
        return ""
    try:
        r = subprocess.run(["tailscale", "status", "--json"],
                           capture_output=True, text=True,
                           timeout=TS_TIMEOUT)
        d = json.loads(r.stdout)
        return (d.get("Self") or {}).get("DNSName") or ""
    except Exception:
        return ""


def merge_tailnet(hosts, self_ip, peers, magicdns_on=False):
    """Fold tailnet peers into the pool (B12/B16) — dedupe, never duplicate.

    1. A peer whose IP is already a known row gets the `tailnet` flag
       (the UI draws the mark in the name cell's reserved last cell —
       the data stays clean; NO new row, anti-redundancy).
    2. A peer matching NO row becomes a new `Name·ts` row: android peers
       take the Termux sshd convention (user:port); everyone else the
       bare-IP floor (ssh's own defaults). Heuristic rows are the honest
       fallback — the checker will tell the truth once it probes them.
    3. (B16) When magicdns_on and the peer carries a DNSName, the row gets
       a `magicdns` short name — the ssh TARGETS use it (ssh_host), the
       displayed target + dedupe keep the IP. No DNSName / off => exact B12.
    Returns (hosts, newly_discovered). The TAILNET_MARK is a muted
    dot-matrix glyph in the NAME column's reserved last cell (ui renders
    it from the `tailnet` flag, so truncation can never eat it)."""
    by_ip = {}
    for h in hosts:
        hn = h.get("hostname") or ""
        if hn:
            by_ip[hn] = h
    out = list(hosts)
    new = []
    for p in peers:
        ip = p["ip"]
        existing = by_ip.get(ip)
        if existing is not None:
            existing["tailnet"] = True
            # the FLAG is the single source of truth; the dot-matrix mark
            # itself is drawn ONLY by ui (row's reserved last cell + footer
            # band) from this flag. Never baked into disp: that produced a
            # DOUBLE indicator (one in the name, one in the reserved cell)
            # and the name-embedded copy could be truncated away.
            continue
        if p["os"] == "android":
            user, port = TERMUX_SSH_USER, TERMUX_SSH_PORT
        else:
            user, port = "", "22"
        tgt = ip + (f":{port}" if port not in ("", "22") else "")
        if user:
            tgt = f"{user}@{tgt}"
        h = {"alias": p["name"], "disp": p["name"] + "·ts",
             "user": user, "hostname": ip, "port": port,
             "timeout": "0", "target": tgt, "src": "ts",
             "src_path": "tailnet", "tailnet": True}
        if magicdns_on and p.get("dnsname"):
            short = _magicdns_short(p["dnsname"])
            if short:
                h["magicdns"] = short
        out.append(h)
        new.append(h)
        by_ip[ip] = h
    return out, new


