# src/remoteRF_host/hostrf_cli.py

from __future__ import annotations

import sys
import shutil
import re
from pathlib import Path
from typing import Optional, Tuple, Dict, List

from .config.cert_fetcher import fetch_and_save_ca_cert


# -----------------------------
# Repo-local host config locations
# -----------------------------

def _repo_root() -> Path:
    # <repo>/src/remoteRF_host/hostrf_cli.py -> parents[2] == <repo>
    return Path(__file__).resolve().parents[2]

def _cfg_dir() -> Path:
    # <repo>/config/remoteRF_host/
    # return _repo_root() / "src" / "remoteRF_host" / "config"
    return _repo_root() / ".config"

def _ca_path() -> Path:
    return _cfg_dir() / "ca.crt"

def _net_env_path() -> Path:
    return _cfg_dir() / "net.env"

def _host_env_path() -> Path:
    return _cfg_dir() / "host.env"

def _devices_env_path() -> Path:
    return _cfg_dir() / "devices.env"


# -----------------------------
# Help (exact commands only)
# -----------------------------

def print_help() -> None:
    print(
        "RemoteRF Host CLI Help\n"
        "\n"
        "Usage:\n"
        "  hostrf -h | --help                   Show this help\n"
        "\n"
        "  hostrf -s | --server                 Run hostrf server\n"
        "\n"
        "  hostrf -c | --config [options]\n"
        "\n"
        "Network option:\n"
        "  -a, --addr <host:port>               Set target server\n"
        "  -a --show                            Show current target\n"
        "  -a -w [-y]                           Wipe ONLY address stuff (net.env + ca.crt)\n"
        "\n"
        "Host option:\n"
        "  -h, --host <str>                     Set host name (HOST_ID)\n"
        "  -h --show                            Show host name\n"
        "  -h --wipe [-y]                       Wipe ONLY host-id (host.env)\n"
        "\n"
        "Device option:\n"
        "  --device --add --pluto <gid:name:serial>  Add device (fails if gid OR serial already used)\n"
        "  --device --remove <gid>                   Remove device\n"
        "  --device --show                           Show all devices\n"
        "  --device --wipe [-y]                      Wipe ONLY device stuff (devices.env)\n"
        "\n"
        "Wipe all:\n"
        "  -w, --wipe [-y]                       Wipe ALL config\n"
        "\n"
        "Examples:\n"
        "  hostrf -c -a 1.2.3.4:61007\n"
        "  hostrf -c -a --show\n"
        "  hostrf -c -a -w -y\n"
        "\n"
        "  hostrf -c -h lab-host-01\n"
        "  hostrf -c -h --show\n"
        "  hostrf -c -h --wipe -y\n"
        "\n"
        "  hostrf -d --add --pluto 0:pluto_aaa:123123\n"
        "  hostrf -d --remove 0\n"
        "  hostrf -d --show\n"
        "  hostrf -d --wipe -y\n"
        "\n"
        "  hostrf -c -w -y\n"
        "\n"
    )


# -----------------------------
# Tiny env utils
# -----------------------------

def _parse_hostport(s: str) -> Tuple[str, int]:
    s = (s or "").strip()
    if "://" in s:
        s = s.split("://", 1)[1]
    if ":" not in s:
        raise ValueError("Expected format host:port")
    host, port_str = s.rsplit(":", 1)
    host = host.strip()
    port = int(port_str.strip())
    if not host:
        raise ValueError("Host is empty")
    if port <= 0 or port > 65535:
        raise ValueError("Port out of range")
    return host, port

def _write_env_kv(path: Path, kv: Dict[str, str]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    lines: List[str] = []
    for k, v in kv.items():
        v = str(v)
        if any(c.isspace() for c in v) or any(c in v for c in ['"', "'"]):
            v = v.replace('"', '\\"')
            lines.append(f'{k}="{v}"')
        else:
            lines.append(f"{k}={v}")
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")

def _read_env_kv(path: Path) -> Dict[str, str]:
    out: Dict[str, str] = {}
    if not path.exists():
        return out
    for raw in path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        out[k.strip()] = v.strip().strip('"').strip("'")
    return out


# -----------------------------
# Device storage (future-friendly)
#   devices.env keys:
#     DEVICE_<gid>_TYPE=pluto
#     DEVICE_<gid>_NAME=pluto_aaa
#     DEVICE_<gid>_IDENT_KIND=iio_serial
#     DEVICE_<gid>_IDENT=<serial>
# -----------------------------

_DEVICE_KEY_RE = re.compile(r"^DEVICE_(\d+)_(.+)$", re.IGNORECASE)

def _parse_pluto_add_spec(tok: str) -> Tuple[int, str, str]:
    # <gid:name:iio_serial>
    parts = [p.strip() for p in (tok or "").split(":")]
    if len(parts) != 3:
        raise ValueError("Expected <gid:name:iio_serial>")
    gid_s, name, iio_serial = parts
    gid = int(gid_s)
    if gid < 0:
        raise ValueError("gid must be >= 0")
    if not name:
        raise ValueError("name is empty")
    if not iio_serial:
        raise ValueError("iio_serial is empty")
    return gid, name, iio_serial

def _read_device_records() -> Dict[int, Dict[str, str]]:
    kv = _read_env_kv(_devices_env_path())
    recs: Dict[int, Dict[str, str]] = {}
    for k, v in kv.items():
        m = _DEVICE_KEY_RE.match(k.strip())
        if not m:
            continue
        gid = int(m.group(1))
        field = m.group(2).upper().strip()
        recs.setdefault(gid, {})[field] = str(v).strip()
    return recs

def _write_device_records(recs: Dict[int, Dict[str, str]]) -> None:
    out: Dict[str, str] = {}
    for gid in sorted(recs.keys()):
        fields = recs[gid]
        order = ["TYPE", "NAME", "IDENT_KIND", "IDENT"]
        for f in order:
            if f in fields and fields[f] != "":
                out[f"DEVICE_{gid}_{f}"] = fields[f]
        for f in sorted(fields.keys()):
            if f in order:
                continue
            if fields[f] != "":
                out[f"DEVICE_{gid}_{f}"] = fields[f]
    _write_env_kv(_devices_env_path(), out)


# -----------------------------
# Actions (ONLY the ones listed)
# -----------------------------
def _configure_addr(addr: str) -> int:
    try:
        host, grpc_port = _parse_hostport(addr)
    except Exception as e:
        print(f"ERROR: addr must be in 'host:port' form: {e}", file=sys.stderr)
        return 2

    cert_port = int(grpc_port) + 1

    # Ensure <repo>/config/remoteRF_host exists
    cfg_dir = _cfg_dir()
    cfg_dir.mkdir(parents=True, exist_ok=True)

    # With your new cert_fetcher.py, the cert + .env live in cfg_dir
    ok = fetch_and_save_ca_cert(
        host,
        cert_port,
        env_dir=cfg_dir,                 # writes ca.crt + updates cfg_dir/.env
        env_filename="net.env",
        cert_filename="ca.crt",
        grpc_addr=f"{host}:{grpc_port}", # also upserts REMOTERF_ADDR
        timeout_sec=3.0,
        overwrite=True,
    )
    if not ok:
        print(f"Failed to fetch CA cert from {host}:{cert_port}.", file=sys.stderr)
        return 1
    
    print("Configured network:")
    print(f"  {_net_env_path()}")
    return 0


def _show_addr() -> int:
    p = _net_env_path()
    if not p.exists():
        print(f"No network config found (missing {p}).")
        return 0

    kv = _read_env_kv(p)
    if not kv:
        print("Network config is empty.")
        return 0

    print(f"Network config: {p}")
    if "REMOTERF_ADDR" in kv:
        print(f"  REMOTERF_ADDR={kv['REMOTERF_ADDR']}")
    if "REMOTERF_CA_CERT" in kv:
        print(f"  REMOTERF_CA_CERT={kv['REMOTERF_CA_CERT']}")
    return 0

def _configure_host(host_id: str) -> int:
    host_id = (host_id or "").strip()
    if not host_id:
        print("ERROR: host name is empty", file=sys.stderr)
        return 2

    _cfg_dir().mkdir(parents=True, exist_ok=True)
    _write_env_kv(_host_env_path(), {"HOST_ID": host_id})

    print("Configured host:")
    print(f"  {_host_env_path()}")
    return 0

def _show_host() -> int:
    p = _host_env_path()
    if not p.exists():
        print(f"No host config found (missing {p}).")
        return 0

    kv = _read_env_kv(p)
    if not kv or "HOST_ID" not in kv:
        print("Host config is empty.")
        return 0

    print(f"Host config: {p}")
    print(f"  HOST_ID={kv.get('HOST_ID','')}")
    return 0

def _device_add_pluto(spec: str) -> int:
    gid, name, iio_serial = _parse_pluto_add_spec(spec)

    _cfg_dir().mkdir(parents=True, exist_ok=True)

    recs = _read_device_records()

    # "appends IF id is available"
    if gid in recs:
        print(f"ERROR: global_id {gid} already exists", file=sys.stderr)
        return 2

    # Prevent duplicate iio_serial across existing devices that use iio_serial
    for other_gid, r in recs.items():
        if r.get("IDENT_KIND", "") == "iio_serial" and r.get("IDENT", "") == iio_serial:
            print(f"ERROR: iio_serial already used by global_id={other_gid}", file=sys.stderr)
            return 2

    recs[gid] = {
        "TYPE": "pluto",
        "NAME": name,
        "IDENT_KIND": "iio_serial",
        "IDENT": iio_serial,
    }
    _write_device_records(recs)

    print(f"Added device: {gid} type=pluto name={name} iio_serial={iio_serial}")
    return 0

def _device_remove(global_id_str: str) -> int:
    try:
        gid = int((global_id_str or "").strip())
    except Exception:
        print("ERROR: --remove expects <gid:int>", file=sys.stderr)
        return 2

    recs = _read_device_records()
    if gid not in recs:
        print(f"WARNING: global_id {gid} not present; nothing to remove.")
        return 0

    recs.pop(gid, None)
    _write_device_records(recs)
    print(f"Removed device: {gid}")
    return 0

def _device_show() -> int:
    p = _devices_env_path()
    if not p.exists():
        print(f"No devices configured (missing {p}).")
        return 0

    recs = _read_device_records()
    print(f"Devices: {p}")
    if not recs:
        print("  (none)")
        return 0

    for gid in sorted(recs.keys()):
        r = recs[gid]
        dtype = r.get("TYPE", "")
        name = r.get("NAME", "")
        ik = r.get("IDENT_KIND", "")
        ident = r.get("IDENT", "")
        print(f"  {gid}: type={dtype} name={name} {ik}={ident}")
    return 0

def _wipe_all(*, yes: bool) -> int:
    root = _cfg_dir()
    if not root.exists():
        print(f"No config found at: {root}")
        return 0
    if not yes:
        try:
            if input(f"This will delete ALL hostrf config at:\n  {root}\nType 'wipe' to confirm: ").strip().lower() != "wipe":
                print("Wipe aborted.")
                return 1
        except KeyboardInterrupt:
            print("\nCancelled.")
            return 1
    shutil.rmtree(root)
    print(f"Wiped ALL config: {root}")
    return 0

def _wipe_addr_only(*, yes: bool) -> int:
    if not yes:
        try:
            if input("This will delete net.env + ca.crt. Type 'wipe' to confirm: ").strip().lower() != "wipe":
                print("Wipe aborted.")
                return 1
        except KeyboardInterrupt:
            print("\nCancelled.")
            return 1

    if _net_env_path().exists():
        _net_env_path().unlink()
    if _ca_path().exists():
        _ca_path().unlink()

    print("Wiped address config (net.env + ca.crt).")
    return 0

def _wipe_host_only(*, yes: bool) -> int:
    if not yes:
        try:
            if input("This will delete host.env. Type 'wipe' to confirm: ").strip().lower() != "wipe":
                print("Wipe aborted.")
                return 1
        except KeyboardInterrupt:
            print("\nCancelled.")
            return 1

    if _host_env_path().exists():
        _host_env_path().unlink()
    print("Wiped host config (host.env).")
    return 0

def _wipe_devices_only(*, yes: bool) -> int:
    if not yes:
        try:
            if input("This will delete devices.env. Type 'wipe' to confirm: ").strip().lower() != "wipe":
                print("Wipe aborted.")
                return 1
        except KeyboardInterrupt:
            print("\nCancelled.")
            return 1

    if _devices_env_path().exists():
        _devices_env_path().unlink()
    print("Wiped device config (devices.env).")
    return 0


# -----------------------------
# CLI parser (ONLY commands in help)
# -----------------------------

def main() -> int:
    argv = list(sys.argv[1:])
    
    if len(argv) > 0 and argv[0] in ("--device", "-device", "-d"):
        argv = ["-c", "--device"] + argv[1:]

    # help
    if len(argv) == 0 or argv[0] in ("--help", "-help", "-h"):
        print_help()
        return 0
    
    if argv[0] in ("-s", "--server"):
        from .server.tunnel_agent import main as tunnel_main
        tunnel_main()          # blocks until Ctrl+C
        return 0

    # config
    if argv[0] not in ("--config", "-config", "-c"):
        print(f"ERROR: unknown command: {argv[0]!r}", file=sys.stderr)
        return 2

    yes = False

    # addr scope/actions
    addr_selected = False
    addr_value: Optional[str] = None
    addr_show = False
    addr_wipe = False

    # host scope/actions
    host_selected = False
    host_value: Optional[str] = None
    host_show = False
    host_wipe = False

    # device scope/actions
    device_selected = False
    device_show = False
    device_wipe = False
    device_add_type: Optional[str] = None
    device_add_spec: Optional[str] = None
    device_remove_gid: Optional[str] = None

    # wipe-all (only when no scope is selected at wipe time)
    wipe_all = False

    i = 1
    while i < len(argv):
        tok = argv[i]

        if tok in ("-y", "--yes", "-yes"):
            yes = True
            i += 1
            continue

        # addr scope
        if tok in ("-a", "--addr", "-addr"):
            addr_selected = True
            # optional value
            if i + 1 < len(argv) and not argv[i + 1].startswith("-"):
                addr_value = argv[i + 1].strip()
                i += 2
            else:
                i += 1
            continue

        # host scope (NOTE: -h means host ONLY inside -c mode)
        if tok in ("--host", "-host", "-h"):
            host_selected = True
            # optional value
            if i + 1 < len(argv) and not argv[i + 1].startswith("-"):
                host_value = argv[i + 1].strip()
                i += 2
            else:
                i += 1
            continue

        # device scope
        if tok in ("--device", "-device", "-d"):
            device_selected = True
            i += 1
            continue

        # show (scope-dependent)
        if tok in ("--show", "-show", "-s"):
            if device_selected:
                device_show = True
                i += 1
                continue
            if host_selected:
                host_show = True
                i += 1
                continue
            if addr_selected:
                addr_show = True
                i += 1
                continue
            print("ERROR: --show must be used with -a/--addr, -h/--host, or --device", file=sys.stderr)
            return 2

        # wipe (scope-dependent OR wipe-all)
        if tok in ("-w", "--wipe", "-wipe"):
            if device_selected:
                device_wipe = True
            elif host_selected:
                host_wipe = True
            elif addr_selected:
                addr_wipe = True
            else:
                wipe_all = True
            i += 1
            continue

        # device add/remove
        if device_selected and tok in ("--add", "-add", "-a"):
            if i + 1 >= len(argv):
                print("ERROR: missing device type after --device --add (expected --pluto)", file=sys.stderr)
                return 2
            dt = argv[i + 1].strip()
            if dt != "--pluto":
                print("ERROR: only '--device --add --pluto <gid:name:iio_serial>' is supported", file=sys.stderr)
                return 2
            if i + 2 >= len(argv):
                print("ERROR: missing spec after --device --add --pluto", file=sys.stderr)
                return 2
            device_add_type = "pluto"
            device_add_spec = argv[i + 2].strip()
            i += 3
            continue

        if device_selected and tok in ("--remove", "-remove", "-r"):
            if i + 1 >= len(argv):
                print("ERROR: missing value after --device --remove <gid>", file=sys.stderr)
                return 2
            device_remove_gid = argv[i + 1].strip()
            i += 2
            continue

        print(f"ERROR: unknown option: {tok!r}", file=sys.stderr)
        return 2

    # ---- conflict checks (keep it strict) ----
    if (addr_wipe or host_wipe or device_wipe or wipe_all) and (addr_show or host_show or device_show):
        print("ERROR: cannot combine --show with --wipe", file=sys.stderr)
        return 2

    if addr_wipe and addr_value is not None:
        print("ERROR: cannot use '--addr <host:port>' with wipe. Use '-a -w' to wipe address config.", file=sys.stderr)
        return 2

    if host_wipe and host_value is not None:
        print("ERROR: cannot use '--host <name>' with wipe. Use '-h --wipe' to wipe host config.", file=sys.stderr)
        return 2

    if device_wipe and (device_add_spec is not None or device_remove_gid is not None):
        print("ERROR: cannot combine device add/remove with device wipe. Use '--device --wipe' only.", file=sys.stderr)
        return 2

    # ---- wipe execution ----
    if wipe_all or addr_wipe or host_wipe or device_wipe:
        if wipe_all:
            return int(_wipe_all(yes=yes))
        if addr_wipe:
            return int(_wipe_addr_only(yes=yes))
        if host_wipe:
            return int(_wipe_host_only(yes=yes))
        if device_wipe:
            return int(_wipe_devices_only(yes=yes))
        return 2

    # ---- non-wipe actions ----
    did_any = False

    if addr_show:
        rc = _show_addr()
        if rc != 0:
            return rc
        did_any = True

    if host_show:
        rc = _show_host()
        if rc != 0:
            return rc
        did_any = True

    if device_show:
        rc = _device_show()
        if rc != 0:
            return rc
        did_any = True

    if addr_value is not None:
        rc = _configure_addr(addr_value)
        if rc != 0:
            return rc
        did_any = True

    if host_value is not None:
        rc = _configure_host(host_value)
        if rc != 0:
            return rc
        did_any = True

    if device_add_type == "pluto" and device_add_spec is not None:
        rc = _device_add_pluto(device_add_spec)
        if rc != 0:
            return rc
        did_any = True

    if device_remove_gid is not None:
        rc = _device_remove(device_remove_gid)
        if rc != 0:
            return rc
        did_any = True

    if not did_any:
        print_help()
        return 2

    return 0


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