"""sshj.state — live ssh connection state (outgoing + incoming).

B2 step 3b: moved verbatim from sshj_cli.py (single-file era) — no
behavior change. Owns CONNS_REFRESH (only its consumer, Conns,
lives here). Uses discover.ssh_target_from_line for the ps scan.
"""

import re
import subprocess
import threading
import time

from .discover import ssh_target_from_line


CONNS_REFRESH = 15         # seconds between live-connection rescans


class Conns:
    """Outgoing ssh sessions per host (ps) + incoming sessions to this box
    (ss / /proc/net/tcp, who). Rescanned in a background thread."""

    def __init__(self, hosts_fn):
        self.hosts_fn = hosts_fn
        self.lock = threading.Lock()
        self.out = {}          # alias -> session count
        self.incoming = []     # (user, peer_ip, count)
        self.done = False
        self.t = None

    def first_pass(self):
        self._scan()

    def start(self):
        self.t = threading.Thread(daemon=True, target=self._loop)
        self.t.start()

    def stop(self):
        self.done = True

    def _loop(self):
        while not self.done:
            time.sleep(CONNS_REFRESH)
            try:
                self._scan()
            except Exception:
                pass

    def _scan(self):
        out = {}
        try:
            r = subprocess.run(["ps", "-eo", "args="],
                               capture_output=True, text=True, timeout=3)
            matches = {}
            for h in self.hosts_fn():
                for k in (h["alias"], h.get("hostname") or ""):
                    if k:
                        matches.setdefault(k, h["alias"])
                u, hn = h.get("user"), h.get("hostname")
                if u and hn:
                    matches.setdefault(f"{u}@{hn}", h["alias"])
            for ln in r.stdout.splitlines():
                cand = ssh_target_from_line(ln)
                if not cand or cand[3] == "true":
                    continue
                user, host, port, _ = cand
                alias = matches.get(host) or (
                    matches.get(f"{user}@{host}") if user else None)
                if alias:
                    out[alias] = out.get(alias, 0) + 1
        except Exception:
            pass
        inc = {}
        try:
            r = subprocess.run(["ss", "-tn", "state", "established"],
                               capture_output=True, text=True, timeout=3)
            for ln in r.stdout.splitlines()[1:]:
                p = ln.split()
                if len(p) < 5:
                    continue
                local, peer = p[3], p[4]
                lport = local.rsplit(":", 1)[-1]
                if lport in ("22", "2222"):
                    ip = peer.rsplit(":", 1)[0]
                    inc[ip] = inc.get(ip, 0) + 1
        except Exception:
            inc = self._scan_proc_tcp()
        users = {}
        try:
            r = subprocess.run(["who"], capture_output=True, text=True,
                               timeout=3)
            for ln in r.stdout.splitlines():
                m = re.search(r"\(([\d.]+|[0-9a-f:]+)\)\s*$", ln)
                if m and ln.split():
                    users[m.group(1)] = ln.split()[0]
        except Exception:
            pass
        with self.lock:
            self.out = out
            self.incoming = [(users.get(ip, ""), ip, n)
                             for ip, n in sorted(inc.items())]

    def _scan_proc_tcp(self):
        inc = {}
        for f in ("/proc/net/tcp", "/proc/net/tcp6"):
            try:
                with open(f) as fh:
                    lines = fh.read().splitlines()[1:]
            except OSError:
                continue
            for ln in lines:
                p = ln.split()
                if len(p) < 4 or p[3] != "01":  # 01 = ESTABLISHED
                    continue
                lport = int(p[1].rsplit(":", 1)[-1], 16)
                if lport in (22, 2222):
                    ip = p[2].rsplit(":", 1)[0]
                    if "0" not in ip[:8] or ":" in ip:
                        ip = p[2]
                    inc[ip] = inc.get(ip, 0) + 1
        return inc

    def count(self, alias):
        with self.lock:
            return self.out.get(alias, 0)

    def counts(self):
        """The whole per-alias outgoing-session map in ONE locked snapshot.
        render() uses it per frame (one lock instead of one per row —
        render is on the hot path and must stay fast)."""
        with self.lock:
            return dict(self.out)

    def incoming_list(self):
        with self.lock:
            return list(self.incoming)


