"""SSH forwarding to remote hub_manager; remote lsusb/status helpers for calibrate."""

import json
import os
import shlex
import subprocess
import sys

from hubmgr.paths import base_dir

REMOTE_SSH_ENV_KEYS = frozenset(
    {
        "HUB_MANAGER_REMOTE_PYTHON",
        "HUB_MANAGER_REMOTE_SCRIPT",
        "HUB_MANAGER_SSH_BIN",
        "HUB_MANAGER_SSH_OPTS",
        # Comma-separated user@host; panel calibrate adds these without repeating --ssh on CLI.
        "HUB_MANAGER_CALIBRATE_REMOTES",
    }
)


def apply_remote_ssh_env_file():
    """
    Load remote_ssh.env (or .hub_manager_remote) from the hub_manager install directory.
    Uses os.environ.setdefault so real environment variables still win.
    """
    b = base_dir()
    for fname in ("remote_ssh.env", ".hub_manager_remote"):
        path = os.path.join(b, fname)
        if not os.path.isfile(path):
            continue
        try:
            with open(path, encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith("#"):
                        continue
                    if "=" not in line:
                        continue
                    key, _, val = line.partition("=")
                    key, val = key.strip(), val.strip()
                    if len(val) >= 2 and val[0] == val[-1] and val[0] in "'\"":
                        val = val[1:-1]
                    if key in REMOTE_SSH_ENV_KEYS:
                        os.environ.setdefault(key, val)
        except OSError:
            continue
        break


def ssh_forward(remote_host, remote_args):
    """
    Run hub_manager on a remote machine (e.g. Raspberry Pi with USB hubs attached).
    Does not import brainstem locally — only needs OpenSSH client.
    """
    apply_remote_ssh_env_file()
    py = os.environ.get("HUB_MANAGER_REMOTE_PYTHON", "python3")
    script = os.environ.get("HUB_MANAGER_REMOTE_SCRIPT", "/usr/local/bin/hub_manager.py")
    ssh_bin = os.environ.get("HUB_MANAGER_SSH_BIN", "ssh")
    extra = shlex.split(os.environ.get("HUB_MANAGER_SSH_OPTS", ""))
    if (
        len(remote_args) >= 2
        and remote_args[0].lower() == "panel"
        and remote_args[1].lower() == "calibrate"
        and not any(x in ("-t", "-tt") for x in extra)
    ):
        extra = ["-t", *extra]
    cmd = [ssh_bin, *extra, remote_host, py, script, *remote_args]
    print(f"hub_manager: ssh {remote_host} → {py} {script} {' '.join(remote_args)}", file=sys.stderr, flush=True)
    proc = subprocess.run(cmd, stdin=sys.stdin)
    return proc.returncode if proc.returncode is not None else 1


def ssh_forward_capture(remote_host, remote_args, timeout=90):
    """Run hub_manager on remote; return (exit_code, stdout, stderr). No TTY."""
    apply_remote_ssh_env_file()
    py = os.environ.get("HUB_MANAGER_REMOTE_PYTHON", "python3")
    script = os.environ.get("HUB_MANAGER_REMOTE_SCRIPT", "/usr/local/bin/hub_manager.py")
    ssh_bin = os.environ.get("HUB_MANAGER_SSH_BIN", "ssh")
    extra = shlex.split(os.environ.get("HUB_MANAGER_SSH_OPTS", ""))
    cmd = [ssh_bin, *extra, remote_host, py, script, *remote_args]
    try:
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
            stdin=subprocess.DEVNULL,
        )
    except subprocess.TimeoutExpired:
        return 124, "", "ssh/hub_manager timed out"
    return (
        proc.returncode if proc.returncode is not None else 1,
        proc.stdout or "",
        proc.stderr or "",
    )


def parse_panel_calibrate_argv(args):
    """
    panel calibrate [merge] [N] [--ssh user@host] ...
    Returns (merge, limit, calibrate_ssh_hosts).
    """
    merge = False
    limit = None
    hosts = []
    i = 0
    while i < len(args):
        a = args[i]
        low = a.lower()
        if low == "merge":
            merge = True
            i += 1
            continue
        if low == "--ssh":
            if i + 1 >= len(args):
                print("panel calibrate: --ssh requires user@host", file=sys.stderr, flush=True)
                sys.exit(2)
            hosts.append(args[i + 1].strip())
            i += 2
            continue
        if a.isdigit():
            limit = int(a)
            i += 1
            continue
        print(f"panel calibrate: unknown argument {a!r}", file=sys.stderr, flush=True)
        sys.exit(2)
    return merge, limit, hosts


def parse_discover_stdout_for_calibrate_ports(stdout):
    """
    Parse `discover` table lines: '1      | 0x........   | 8' → (1,0)…(1,7).
    """
    pairs = []
    for line in stdout.splitlines():
        raw = line.rstrip()
        if "|" not in raw:
            continue
        if set(raw.strip()) <= {"-", " "}:
            continue
        parts = [p.strip() for p in raw.split("|")]
        if len(parts) < 3:
            continue
        hub_s, _, n_s = parts[0], parts[1], parts[2]
        if not hub_s.isdigit():
            continue
        try:
            hub = int(hub_s)
            nports = int(n_s)
        except ValueError:
            continue
        if hub < 1 or nports < 1:
            continue
        for p in range(nports):
            pairs.append((hub, p))
    return pairs


def maybe_hint_remote_ssh_python(exit_code, out, err):
    """Exit 126/127 usually means bad HUB_MANAGER_REMOTE_PYTHON path on the *remote* host."""
    if exit_code not in (126, 127):
        return
    blob = f"{out or ''} {err or ''}".lower()
    if "no such file" not in blob and "not found" not in blob:
        return
    py = os.environ.get("HUB_MANAGER_REMOTE_PYTHON", "python3")
    script = os.environ.get("HUB_MANAGER_REMOTE_SCRIPT", "/usr/local/bin/hub_manager.py")
    print(
        "  SSH ran this on the Pi (paths must exist *on the Pi*, not on Fedora):\n"
        f"    interpreter: {py}\n"
        f"    script:      {script}\n"
        "  Fix: SSH to the Pi, activate the venv with brainstem, run:\n"
        "    which python3\n"
        "    realpath /path/to/hub_manager.py\n"
        "  Put those absolute paths in remote_ssh.env next to hub_manager.py on Fedora, or export:\n"
        "    HUB_MANAGER_REMOTE_PYTHON=…   HUB_MANAGER_REMOTE_SCRIPT=…",
        file=sys.stderr,
        flush=True,
    )


def fetch_calibrate_ports_json(ssh_host):
    """Ask remote hub_manager for [[hub,port], ...]; fall back to parsing `discover` if remote script is older."""
    code, out, err = ssh_forward_capture(ssh_host, ["calibrate-ports-json"], timeout=90)
    out = out or ""
    err = err or ""
    json_pairs = None
    if code == 0 and out.strip():
        try:
            data = json.loads(out.strip())
        except json.JSONDecodeError:
            json_pairs = None
        else:
            if isinstance(data, list):
                json_pairs = []
                for item in data:
                    if isinstance(item, (list, tuple)) and len(item) == 2:
                        try:
                            json_pairs.append((int(item[0]), int(item[1])))
                        except (TypeError, ValueError):
                            continue
            else:
                json_pairs = None

    if json_pairs is not None:
        return json_pairs

    if "Unknown command" in (out + err):
        print(
            f"Remote {ssh_host!r} has no calibrate-ports-json; using discover fallback "
            "(scp this hub_manager.py to the Pi to skip the extra SSH round trip).",
            file=sys.stderr,
            flush=True,
        )

    code2, out2, err2 = ssh_forward_capture(ssh_host, ["discover"], timeout=120)
    out2 = out2 or ""
    if code2 != 0:
        print(
            f"discover on {ssh_host!r} failed (exit {code2}): {(err2 or out2).strip()[:400]}",
            file=sys.stderr,
            flush=True,
        )
        maybe_hint_remote_ssh_python(code2, out2, err2)
        if code != 0 or (out + err).strip():
            print(
                f"calibrate-ports-json on {ssh_host!r} (exit {code}): {(err or out).strip()[:400]}",
                file=sys.stderr,
                flush=True,
            )
            maybe_hint_remote_ssh_python(code, out, err)
        return []

    pairs = parse_discover_stdout_for_calibrate_ports(out2)
    if not pairs:
        print(
            f"Could not parse hub/port list from discover on {ssh_host!r}. "
            "Ensure the Pi connects to its hubs (udev 24ff) and discover prints the Hub|Serial|Ports table.",
            file=sys.stderr,
            flush=True,
        )
        return []

    print(
        f"Using discover output for {ssh_host!r} ({len(pairs)} hub.port step(s)).",
        file=sys.stderr,
        flush=True,
    )
    return pairs


def remote_hub_port_power(ssh_host, hub_1, port_0, enable):
    sub = "on" if enable else "off"
    code, out, err = ssh_forward_capture(ssh_host, [sub, f"{hub_1}.{port_0}"])
    blob = "\n".join(x.strip() for x in (out or "", err or "") if x and x.strip()).strip()
    return code, blob


def remote_port_power_feedback(ssh_host, hub_1, port_0):
    code, out, err = ssh_forward_capture(ssh_host, ["status", f"{hub_1}.{port_0}"])
    if code != 0:
        return f"remote status failed ({code}): {(err or out).strip()[:120]}"
    pwr, cur = parse_status_line_for_hub_port(out, hub_1, port_0)
    return f"remote hub reports {pwr}, {cur} mA"


def remote_lsusb_lines(ssh_host):
    """Full lsusb output lines on the SSH host (hub_manager lsusb-lines-json or plain `ssh … lsusb`)."""
    code, out, err = ssh_forward_capture(ssh_host, ["lsusb-lines-json"], timeout=45)
    if code == 0 and out.strip():
        try:
            data = json.loads(out.strip())
            if isinstance(data, list) and all(isinstance(x, str) for x in data):
                return data
        except json.JSONDecodeError:
            pass
    apply_remote_ssh_env_file()
    ssh_bin = os.environ.get("HUB_MANAGER_SSH_BIN", "ssh")
    extra = shlex.split(os.environ.get("HUB_MANAGER_SSH_OPTS", ""))
    try:
        proc = subprocess.run(
            [ssh_bin, *extra, ssh_host, "lsusb"],
            capture_output=True,
            text=True,
            timeout=45,
        )
    except (OSError, subprocess.TimeoutExpired):
        return []
    if proc.returncode != 0 or not proc.stdout:
        return []
    return proc.stdout.splitlines()


def parse_status_line_for_hub_port(stdout, hub_1, port_0):
    """Parse `status hub.port` table output for one port; return (power, current_str) or (?, ?)."""
    for line in stdout.splitlines():
        ln = line.strip()
        if not ln or ln.startswith("-") or "Identity" in ln or "Hub" not in ln:
            continue
        parts = [p.strip() for p in line.split("|")]
        if len(parts) < 4:
            continue
        hub_part = parts[0].replace("Hub", "", 1).strip()
        try:
            h = int(hub_part)
            p = int(parts[1])
        except ValueError:
            continue
        if h == hub_1 and p == port_0:
            return parts[2], parts[3]
    return "?", "?"
