#!/usr/bin/env python3
"""
sysinfo - usuals commands all in one
"""

import argparse
import getpass
import socket
import os
import sys
import subprocess
import platform
import json
import hashlib
import shutil
import re
import unicodedata
import urllib.request
import urllib.error
import curses
from datetime import datetime

VERSION = "1.0.0"
SYSINFO_SOURCE_URL = "https://devexploris.com/sysinfo/sysinfo.py"
SYSINFO_CHECKSUM_URL = "https://devexploris.com/sysinfo/sysinfo.py.sha256"


def get_uptime():
    try:
        with open("/proc/uptime") as f:
            seconds = float(f.readline().split()[0])
        h, rem = divmod(int(seconds), 3600)
        m, _ = divmod(rem, 60)
        return f"{h}h {m}m"
    except Exception:
        return "N/A"


def get_ipv4():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "N/A"


def get_groups():
    try:
        return subprocess.check_output(["groups"], text=True).strip()
    except Exception:
        return "N/A"


def fetch_json(url, timeout=4):
    req = urllib.request.Request(url, headers={"User-Agent": "sysinfo/1.0"})
    with urllib.request.urlopen(req, timeout=timeout) as response:
        if response.status != 200:
            raise urllib.error.HTTPError(url, response.status, "Bad status", None, None)
        return json.loads(response.read().decode())


def get_local_ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(1)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "N/A"


def get_location():
    # --- Tentative 1 : ipinfo.io ---
    try:
        data = fetch_json("https://ipinfo.io/json")
        ip = data.get("ip", "N/A")
        city = data.get("city", "N/A")
        country = data.get("country", "N/A")
        return ip, f"{city} - {country}", "ipinfo.io"
    except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, Exception):
        pass

    # --- Fallback : ip-api.com ---
    try:
        data = fetch_json("http://ip-api.com/json")
        if data.get("status") == "success":
            ip = data.get("query", "N/A")
            city = data.get("city", "N/A")
            country = data.get("countryCode", "N/A")
            return ip, f"{city} - {country}", "ip-api.com"
    except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, Exception):
        pass

    return "N/A", "N/A", None


def get_vpn_status():
    try:
        data = fetch_json("http://ip-api.com/json/?fields=status,proxy,hosting,org,isp,as")
        if data.get("status") == "success":
            return data.get("proxy"), data.get("hosting"), data.get("org") or data.get("isp", "N/A")
    except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, Exception):
        pass
    return None, None, None


def color(text, code, enabled):
    return f"\033[{code}m{text}\033[0m" if enabled else text

def char_widths(text):
    chars = list(text)
    widths = []
    for ch in chars:
        if unicodedata.category(ch) in ("Mn", "Me", "Cf"):
            widths.append(0)
        else:
            widths.append(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1)
    return chars, widths


def align_icon(label):
    chars, widths = char_widths(label)
    icon = 0
    while icon < len(chars) and widths[icon] == 0:
        icon += 1
    if icon >= len(chars) or widths[icon] != 1:
        return label

    cut = icon + 1
    while cut < len(chars) and widths[cut] == 0:
        cut += 1

    is_icon = cut > icon + 1 or unicodedata.category(chars[icon]) == "So"
    if not is_icon or chars[cut:cut + 1] != [" "]:
        return label
    return "".join(chars[:cut]) + " " + "".join(chars[cut:])


def display_width(text):
    return sum(char_widths(text)[1])


def pad_display(text, width):
    return text + " " * max(width - display_width(text), 0)


def truncate_display(text, width):
    if display_width(text) <= width:
        return text
    if width <= 1:
        return "…"
    chars, widths = char_widths(text)
    out, used = [], 0
    for ch, w in zip(chars, widths):
        if used + w > width - 1:
            break
        out.append(ch)
        used += w
    return "".join(out) + "…"


INFO_SEPARATOR = " - "

MARGIN = "  "


def print_info(label, value, code, enabled):
    print(f"{MARGIN}{color(label, code, enabled)}{INFO_SEPARATOR}{value}")


def print_help_screen():
    tty = sys.stdout.isatty()

    def h(text, code="1;36"):
        return color(text, code, tty)

    banner_title = " S Y S I N F O "
    border = "═" * (len(banner_title) + 2)
    print(h(f"╔{border}╗", "1;35"))
    print(h(f"║ {banner_title} ║", "1;35"))
    print(h(f"╚{border}╝", "1;35"))
    print(h("sysinfo") + " — système, réseau, SSH et Docker en un coup d'œil\n")

    print(h("USAGE"))
    print("  sysinfo [options]\n")

    sections = [
        ("Infos système", [
            ("-a, --all", "Afficher toutes les infos disponibles"),
            ("-4, --ipv4", "Adresse IPv4 (interface réseau locale)"),
            ("-g, --groups", "Groupes de l'utilisateur"),
            ("-u, --uptime", "Uptime de la machine"),
        ]),
        ("Réseau / IP", [
            ("-l, --location", "IP publique + ville - pays (ipinfo.io, fallback ip-api.com)"),
            ("    --local-ip", "IP locale (LAN)"),
            ("    --vpn", "Détection VPN / proxy / hébergeur"),
            ("    --ports", "Ports en écoute (ss/netstat -tulpn)"),
            ("    --ssh", "Lister les hosts SSH enregistrés (~/.ssh/config)"),
            ("    --docker", "Containers actifs + networks Docker"),
        ]),
        ("Affichage", [
            ("-c, --color", "Activer les couleurs dans la sortie"),
            ("-t, --table", "Afficher les infos sous forme de tableau"),
            ("-i, --interactive", "Mode interactif avec onglets navigables"),
            ("    --version", "Afficher la version installée"),
            ("    --update", "Mettre à jour vers la dernière version"),
            ("-h, --help", "Afficher cette aide"),
        ]),
    ]

    for title, rows in sections:
        print(h(f"{title.upper()}", "1;33"))
        for flag, desc in rows:
            print(f"  {h(flag, '1;32'):<28} {desc}")
        print()

    print(h("EXEMPLES"))
    examples = [
        ("sysinfo -a -c", "tout afficher, en couleur"),
        ("sysinfo -l --vpn", "localisation + détection VPN"),
        ("sysinfo --ports", "ports en écoute (ss/netstat)"),
        ("sysinfo --local-ip -4", "IP locale et IPv4 réseau"),
        ("sysinfo -a -t", "tout afficher sous forme de tableau"),
        ("sysinfo -i", "mode interactif avec onglets"),
        ("sysinfo --ssh", "lister les hosts SSH connus"),
        ("sysinfo --docker", "containers actifs + networks Docker"),
        ("sysinfo --update", "mettre à jour vers la dernière version"),
    ]
    for cmd, desc in examples:
        print(f"  {h(cmd, '1;37')}")
        print(f"      → {desc}")
    print()


class CustomHelpAction(argparse.Action):
    def __init__(self, option_strings, dest, **kwargs):
        super().__init__(option_strings, dest, nargs=0, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        print_help_screen()
        parser.exit()


def collect_system_info():
    return [
        ("👤 Utilisateur", getpass.getuser()),
        ("🖥️ Machine", socket.gethostname()),
        ("📁 Dossier", os.getcwd()),
        ("🐚 Shell", os.environ.get("SHELL", "N/A")),
        ("💻 OS", f"{platform.system()} {platform.release()}"),
        ("🔑 Groupes", get_groups()),
        ("⏱️ Uptime", get_uptime()),
        ("🕐 Date/heure", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
    ]


def parse_ss_output(output):
    ports = []
    lines = output.strip().splitlines()
    if len(lines) < 2:
        return ports
    for line in lines[1:]:
        parts = line.split()
        if len(parts) < 5:
            continue
        proto = parts[0]
        local_addr = parts[4]
        process = "N/A"
        m = re.search(r'\(\("([^"]+)",pid=(\d+)', line)
        if m:
            process = f"{m.group(1)} (pid {m.group(2)})"
        ports.append({"proto": proto, "local": local_addr, "process": process})
    return ports


def parse_netstat_output(output):
    ports = []
    for line in output.strip().splitlines():
        if not (line.startswith("tcp") or line.startswith("udp")):
            continue
        parts = line.split()
        if len(parts) < 4:
            continue
        proto = parts[0]
        local_addr = parts[3]
        last = parts[-1]
        if "/" in last:
            pid, _, prog = last.partition("/")
            process = f"{prog} (pid {pid})" if pid.isdigit() else last
        else:
            process = "N/A" 
        ports.append({"proto": proto, "local": local_addr, "process": process})
    return ports


def get_listening_ports():
    try:
        proc = subprocess.run(["ss", "-tulpn"], capture_output=True, text=True, timeout=5)
        if proc.returncode == 0:
            return parse_ss_output(proc.stdout), "ss"
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass
    except Exception:
        pass

    try:
        proc = subprocess.run(["netstat", "-tulpn"], capture_output=True, text=True, timeout=5)
        if proc.returncode == 0:
            return parse_netstat_output(proc.stdout), "netstat"
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass
    except Exception:
        pass

    return None, None


def collect_network_info():
    ip, loc, source = get_location()
    rows = [
        ("🌐 IPv4 (réseau)", get_ipv4()),
        ("🏠 IP locale (LAN)", get_local_ip()),
        ("🌍 IP publique", ip if source else "indisponible"),
        ("📍 Localisation", loc if source else "indisponible"),
        ("🔗 Source", source or "aucune (services hors ligne)"),
    ]

    ports, port_source = get_listening_ports()
    rows.append(("──────────", "──────────"))
    if ports is None:
        rows.append(("⚠️ Ports", "ss/netstat indisponibles"))
    elif not ports:
        rows.append(("ℹ️ Ports", "Aucun port en écoute détecté"))
    else:
        for p in ports:
            rows.append((f"🔌 {p['proto'].upper()} {p['local']}", p["process"]))

    return rows


def collect_vpn_info():
    is_proxy, is_hosting, org = get_vpn_status()
    if is_proxy is None:
        return [("🕵️ Statut", "indisponible (service hors ligne ou limite atteinte)")]
    if is_proxy:
        verdict = "⚠️ VPN/Proxy détecté"
    elif is_hosting:
        verdict = "⚠️ Datacenter/hébergeur (suspect)"
    else:
        verdict = "✅ Connexion normale"
    return [
        ("🕵️ Statut", verdict),
        ("🏢 Org/ISP", org),
    ]


def interactive_mode():
    TABS = [
        ("Système", collect_system_info),
        ("Réseau", collect_network_info),
        ("VPN", collect_vpn_info),
        ("SSH", collect_ssh_info),
        ("Docker", collect_docker_info),
    ]

    cache = {}

    def load_tab(idx, force=False):
        if force or idx not in cache:
            cache[idx] = TABS[idx][1]()
        return cache[idx]

    def run(stdscr):
        curses.curs_set(0)
        curses.start_color()
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_CYAN, -1)
        curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN)
        curses.init_pair(3, curses.COLOR_YELLOW, -1)
        curses.init_pair(4, curses.COLOR_GREEN, -1)

        current = 0
        loading = True
        stdscr.nodelay(False)

        while True:
            stdscr.clear()
            height, width = stdscr.getmaxyx()

            x = 2
            stdscr.addstr(1, x, " sysinfo ", curses.color_pair(1) | curses.A_BOLD)
            x += 10
            for i, (name, _) in enumerate(TABS):
                label = f" {name} "
                if i == current:
                    stdscr.addstr(1, x, label, curses.color_pair(2) | curses.A_BOLD)
                else:
                    stdscr.addstr(1, x, label, curses.color_pair(1))
                x += len(label) + 1

            stdscr.hline(2, 1, curses.ACS_HLINE, max(width - 2, 0))

            if loading:
                stdscr.addstr(4, 2, "⏳ Chargement...", curses.color_pair(3))
                stdscr.refresh()
                load_tab(current)
                loading = False
                continue

            rows = load_tab(current)
            for row_idx, (label, value) in enumerate(rows):
                y = 4 + row_idx
                if y >= height - 2:
                    break

                cell = pad_display(truncate_display(align_icon(label), 22), 22)
                stdscr.addstr(y, 2, cell, curses.color_pair(4) | curses.A_BOLD)
                stdscr.addstr(y, 26, str(value)[:max(width - 28, 0)])

            footer = " ←/→ ou Tab : changer d'onglet   r : rafraîchir   q : quitter "
            stdscr.addstr(height - 1, 0, footer[:width - 1], curses.A_DIM)

            stdscr.refresh()

            key = stdscr.getch()
            if key in (curses.KEY_RIGHT, ord("\t")):
                current = (current + 1) % len(TABS)
                loading = True
            elif key == curses.KEY_LEFT:
                current = (current - 1) % len(TABS)
                loading = True
            elif key in (ord("r"), ord("R")):
                loading = True
                cache.pop(current, None)
            elif key in (ord("q"), ord("Q"), 27):
                break

    curses.wrapper(run)


def parse_ssh_config(path):
    hosts = []
    if not os.path.isfile(path):
        return hosts

    try:
        with open(path, encoding="utf-8", errors="ignore") as f:
            lines = f.readlines()
    except Exception:
        return hosts

    current_group = []
    for line in lines:
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        parts = line.split(None, 1)
        if len(parts) < 2:
            continue
        key, value = parts[0].lower(), parts[1].strip().strip('"')

        if key == "host":
            current_group = []
            for alias in value.split():
                if "*" in alias or "?" in alias:
                    continue
                entry = {"alias": alias, "hostname": None, "user": None, "port": None}
                hosts.append(entry)
                current_group.append(entry)
        elif current_group:
            if key == "hostname":
                for e in current_group:
                    e["hostname"] = value
            elif key == "user":
                for e in current_group:
                    e["user"] = value
            elif key == "port":
                for e in current_group:
                    e["port"] = value

    return hosts


def collect_ssh_info():
    path = os.path.expanduser("~/.ssh/config")
    if not os.path.isfile(path):
        return [("⚠️ Fichier", "~/.ssh/config introuvable")]

    hosts = parse_ssh_config(path)
    if not hosts:
        return [("ℹ️ Info", "Aucun host défini dans ~/.ssh/config (hors wildcards)")]

    rows = []
    for h in hosts:
        target = h["hostname"] or h["alias"]
        user_part = f"{h['user']}@" if h["user"] else ""
        port_part = f":{h['port']}" if h.get("port") and h["port"] != "22" else ""
        rows.append((f"🖧 {h['alias']}", f"{user_part}{target}{port_part}"))
    return rows


def run_docker_json(args_list):
    try:
        proc = subprocess.run(
            ["docker"] + args_list,
            capture_output=True, text=True, timeout=5
        )
    except FileNotFoundError:
        return None, "Docker CLI non installé (commande 'docker' introuvable)"
    except subprocess.TimeoutExpired:
        return None, "Timeout : le daemon Docker ne répond pas"

    if proc.returncode != 0:
        err = proc.stderr.strip()
        low = err.lower()
        if "permission denied" in low:
            return None, "Permission refusée (ajoutez votre utilisateur au groupe 'docker', ou utilisez sudo)"
        if "cannot connect" in low or "daemon" in low:
            return None, "Impossible de contacter le daemon Docker (est-il lancé ?)"
        return None, err or "Erreur inconnue lors de l'appel à docker"

    items = []
    for line in proc.stdout.strip().splitlines():
        if not line:
            continue
        try:
            items.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    return items, None


def get_docker_containers():
    data, err = run_docker_json(["ps", "--format", "{{json .}}"])
    if data is None:
        return None, err
    containers = [
        {
            "name": d.get("Names", "N/A"),
            "image": d.get("Image", "N/A"),
            "ports": d.get("Ports") or "—",
            "status": d.get("Status", ""),
        }
        for d in data
    ]
    return containers, None


def get_docker_networks():
    data, err = run_docker_json(["network", "ls", "--format", "{{json .}}"])
    if data is None:
        return None, err

    networks = []
    for d in data:
        name = d.get("Name")
        containers_in_net = []
        try:
            proc = subprocess.run(
                ["docker", "network", "inspect", name],
                capture_output=True, text=True, timeout=5
            )
            if proc.returncode == 0:
                inspect_data = json.loads(proc.stdout)
                if inspect_data:
                    cdict = inspect_data[0].get("Containers", {})
                    containers_in_net = [c.get("Name") for c in cdict.values()]
        except Exception:
            pass
        networks.append({
            "name": name,
            "driver": d.get("Driver", "N/A"),
            "containers": containers_in_net,
        })
    return networks, None


def collect_docker_info():
    rows = []
    containers, err = get_docker_containers()
    if err:
        rows.append(("⚠️ Containers", err))
    elif not containers:
        rows.append(("ℹ️ Containers", "Aucun container actif"))
    else:
        for cnt in containers:
            rows.append((f"📦 {cnt['name']}", f"{cnt['image']} | ports: {cnt['ports']}"))

    networks, err2 = get_docker_networks()
    rows.append(("──────────", "──────────"))
    if err2:
        rows.append(("⚠️ Networks", err2))
    elif not networks:
        rows.append(("ℹ️ Networks", "Aucun network trouvé"))
    else:
        for net in networks:
            containers_str = ", ".join(net["containers"]) if net["containers"] else "—"
            rows.append((f"🌐 {net['name']} ({net['driver']})", containers_str))
    return rows


def get_running_script_path():
    argv0 = sys.argv[0]
    has_explicit_path = os.path.sep in argv0 or (os.altsep and os.altsep in argv0)

    if has_explicit_path:
        return os.path.realpath(argv0)

    resolved = shutil.which(argv0)
    if resolved:
        return os.path.realpath(resolved)

    return os.path.realpath(argv0)


def self_update():
    print("🔄 Vérification de mise à jour...")

    try:
        with urllib.request.urlopen(SYSINFO_SOURCE_URL, timeout=6) as r:
            remote_bytes = r.read()
    except Exception as e:
        print(f"⚠️ Impossible de contacter le serveur de mise à jour : {e}")
        return

    try:
        with urllib.request.urlopen(SYSINFO_CHECKSUM_URL, timeout=6) as r:
            checksum_line = r.read().decode().strip()
        expected_sum = checksum_line.split()[0]
    except Exception as e:
        print(f"⚠️ Impossible de récupérer le checksum : {e}")
        return

    actual_sum = hashlib.sha256(remote_bytes).hexdigest()
    if actual_sum != expected_sum:
        print("✗ Checksum invalide — mise à jour annulée par sécurité.")
        print(f"  Attendu : {expected_sum}")
        print(f"  Obtenu  : {actual_sum}")
        return

    current_path = get_running_script_path()
    print(f"  Emplacement détecté : {current_path}")

    try:
        with open(current_path, "rb") as f:
            local_bytes = f.read()
        local_sum = hashlib.sha256(local_bytes).hexdigest()
    except Exception:
        local_sum = None

    if local_sum == actual_sum:
        print(f"✓ Déjà à jour (version {VERSION}).")
        return

    tmp_path = current_path + ".tmp"
    try:
        with open(tmp_path, "wb") as f:
            f.write(remote_bytes)
        os.chmod(tmp_path, 0o755)
        os.replace(tmp_path, current_path)
    except PermissionError:
        print(f"⚠️ Permission refusée pour écrire dans {current_path}.")
        print("   Essayez avec sudo, ou vérifiez les droits sur ce fichier.")
        try:
            os.remove(tmp_path)
        except OSError:
            pass
        return
    except Exception as e:
        print(f"⚠️ Erreur pendant la mise à jour : {e}")
        return

    print(f"✓ Mis à jour avec succès : {current_path}")
    print("  Relancez sysinfo --version pour confirmer.")



def build_rows(args):
    items = []

    def pair(label, value, code):
        items.append(("pair", align_icon(label), value, code))

    def section(label, code):
        items.append(("section", align_icon(label), None, code))

    def sub(label, value, code):
        items.append(("sub", align_icon(label), value, code))

    def note(text):
        items.append(("note", align_icon(text), None, None))

    pair("👤 Utilisateur", getpass.getuser(), "1;36")
    pair("🖥️ Machine", socket.gethostname(), "1;36")
    pair("📁 Dossier", os.getcwd(), "1;36")

    if args.all or args.ipv4:
        pair("🌐 IPv4", get_ipv4(), "1;32")
    if args.all or args.groups:
        pair("🔑 Groupes", get_groups(), "1;33")
    if args.all or args.uptime:
        pair("⏱️ Uptime", get_uptime(), "1;35")
    if args.all:
        pair("🐚 Shell", os.environ.get("SHELL", "N/A"), "1;34")
        pair("💻 OS", f"{platform.system()} {platform.release()}", "1;34")
        pair("🕐 Date/heure", datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "1;37")

    if args.all or args.location:
        ip, loc, source = get_location()
        if source:
            pair("🌍 IP publique", ip, "1;31")
            pair("📍 Localisation", loc, "1;31")
        else:
            pair("🌍 IP publique", "indisponible (services hors ligne ou limite atteinte)", "1;31")

    if args.all or args.local_ip:
        pair("🏠 IP locale", get_local_ip(), "1;36")

    if args.all or args.vpn:
        is_proxy, is_hosting, org = get_vpn_status()
        if is_proxy is None:
            pair("🕵️ VPN/Proxy", "indisponible (service hors ligne ou limite atteinte)", "1;33")
        else:
            if is_proxy:
                verdict = "⚠️ VPN/Proxy détecté"
            elif is_hosting:
                verdict = "⚠️ Datacenter/hébergeur (pas forcément un VPN, mais suspect)"
            else:
                verdict = "✅ Connexion normale (pas de VPN/proxy détecté)"
            pair("🕵️ VPN/Proxy", verdict, "1;33")
            pair("🏢 Org/ISP", org, "1;33")

    if args.all or args.ports:
        section("🔌 Ports", "1;32")
        ports, _port_source = get_listening_ports()
        if ports is None:
            note("⚠️ ss/netstat indisponibles ou erreur d'exécution")
        elif not ports:
            note("ℹ️ Aucun port en écoute détecté")
        else:
            for p in ports:
                sub(f"🔌 {p['proto'].upper()} {p['local']}", p["process"], "1;32")

    if args.all or args.ssh:
        section("🖧 Hosts SSH (~/.ssh/config)", "1;35")
        for label, value in collect_ssh_info():
            sub(label, value, "1;35")

    if args.all or args.docker:
        section("📦 Containers Docker actifs", "1;34")
        containers, err = get_docker_containers()
        if err:
            note(f"⚠️ {err}")
        elif not containers:
            note("ℹ️ Aucun container actif")
        else:
            for cnt in containers:
                sub(f"📦 {cnt['name']}", f"{cnt['image']} | ports: {cnt['ports']}", "1;34")

        section("🌐 Networks Docker", "1;34")
        networks, err2 = get_docker_networks()
        if err2:
            note(f"⚠️ {err2}")
        elif not networks:
            note("ℹ️ Aucun network trouvé")
        else:
            for net in networks:
                containers_str = ", ".join(net["containers"]) if net["containers"] else "—"
                sub(f"🌐 {net['name']} ({net['driver']})", containers_str, "1;34")

    return items


SUB_GUTTER = 3

SUB_MARGIN = MARGIN + "  "

TITLE_CHROME = 4


def print_section_title(label, code, enabled):
    columns = shutil.get_terminal_size((100, 24)).columns
    text = truncate_display(label, max(columns - len(MARGIN) - TITLE_CHROME, 1))
    rule = "─" * (display_width(text) + 2)
    print(f"{MARGIN}{color(f'┌{rule}┐', code, enabled)}")
    print(f"{MARGIN}{color(f'│ {text} │', code, enabled)}")
    print(f"{MARGIN}{color(f'└{rule}┘', code, enabled)}")


def render_plain(items, enabled):
    idx = 0
    printed = False
    while idx < len(items):
        kind, label, value, code = items[idx]

        if kind == "sub":
            run = []
            while idx < len(items) and items[idx][0] == "sub":
                run.append(items[idx])
                idx += 1
            width = max(display_width(r[1]) for r in run)
            for _kind, lbl, val, cd in run:
                cell = color(pad_display(lbl, width), cd, enabled)
                print(f"{SUB_MARGIN}{cell}{' ' * SUB_GUTTER}{val}")
            printed = True
            continue

        if kind == "section":
            if printed:
                print()
            print_section_title(label, code, enabled)
        elif kind == "note":
            print(f"{SUB_MARGIN}{label}")
        else:
            print_info(label, value, code, enabled)
        printed = True
        idx += 1


TABLE_CHROME = 7
TABLE_MIN_LABEL, TABLE_MIN_VALUE = 10, 10
TABLE_LABEL_SHARE = 0.45


def table_widths(nat_label, nat_value, columns):
    avail = max(columns - TABLE_CHROME, TABLE_MIN_LABEL + TABLE_MIN_VALUE)
    if nat_label + nat_value <= avail:
        return nat_label, nat_value

    cap_label = max(TABLE_MIN_LABEL, int(avail * TABLE_LABEL_SHARE))
    w_label = min(nat_label, max(cap_label, avail - nat_value))
    w_value = min(nat_value, max(TABLE_MIN_VALUE, avail - w_label))
    return min(nat_label, avail - w_value), w_value


def render_table(items, enabled):
    head_label, head_value = "Info", "Valeur"
    rows = []
    for kind, label, value, code in items:
        if kind in ("section", "note"):
            rows.append((label, "", code, kind == "section"))
        else:
            rows.append((label, str(value), code, False))

    nat_label = max([display_width(head_label)] + [display_width(r[0]) for r in rows])
    nat_value = max([display_width(head_value)] + [display_width(r[1]) for r in rows])
    columns = shutil.get_terminal_size((100, 24)).columns
    w_label, w_value = table_widths(nat_label, nat_value, columns)

    def rule(left, mid, right):
        return f"{left}{'─' * (w_label + 2)}{mid}{'─' * (w_value + 2)}{right}"

    def emit(label, value, code):
        cell = pad_display(truncate_display(label, w_label), w_label)
        cell = color(cell, code, enabled) if code else cell
        val = pad_display(truncate_display(value, w_value), w_value)
        print(f"│ {cell} │ {val} │")

    print(rule("┌", "┬", "┐"))
    emit(head_label, head_value, "1;37")
    print(rule("├", "┼", "┤"))
    for label, value, code, is_section in rows:
        if is_section:
            print(rule("├", "┼", "┤"))
        emit(label, value, code)
        if is_section:
            print(rule("├", "┼", "┤"))
    print(rule("└", "┴", "┘"))


def main():
    parser = argparse.ArgumentParser(
        description="sysinfo - système, réseau, SSH et Docker en un coup d'œil",
        add_help=False,
    )
    parser.add_argument("-h", "--help", action=CustomHelpAction, help="Afficher cette aide")
    parser.add_argument("-4", "--ipv4", action="store_true", help="Afficher l'adresse IPv4")
    parser.add_argument("-a", "--all", action="store_true", help="Afficher toutes les infos")
    parser.add_argument("-c", "--color", action="store_true", help="Activer les couleurs")
    parser.add_argument("-t", "--table", action="store_true",
                        help="Afficher les infos sous forme de tableau")
    parser.add_argument("-g", "--groups", action="store_true", help="Afficher les groupes")
    parser.add_argument("-u", "--uptime", action="store_true", help="Afficher l'uptime")
    parser.add_argument("-l", "--location", action="store_true", help="Afficher IP publique + ville/pays")
    parser.add_argument("--local-ip", action="store_true", help="Afficher l'IP locale (LAN)")
    parser.add_argument("--vpn", action="store_true", help="Détecter VPN/proxy/hébergeur")
    parser.add_argument("--ports", action="store_true", help="Lister les ports en écoute (ss/netstat -tulpn)")
    parser.add_argument("--ssh", action="store_true", help="Lister les hosts SSH enregistrés (~/.ssh/config)")
    parser.add_argument("--docker", action="store_true", help="Lister containers actifs + networks Docker")
    parser.add_argument("-i", "--interactive", action="store_true", help="Mode interactif avec onglets")
    parser.add_argument("--version", action="store_true", help="Afficher la version installée")
    parser.add_argument("--update", action="store_true", help="Mettre à jour vers la dernière version")
    args = parser.parse_args()

    if args.version:
        print(f"sysinfo version {VERSION}")
        return

    if args.update:
        self_update()
        return

    if args.interactive:
        try:
            interactive_mode()
        except curses.error:
            print("⚠️ Le mode interactif nécessite un vrai terminal (TTY).")
            print("   Lancez-le directement dans un terminal, pas via un pipe ou un script non-interactif.")
        return

    c = args.color
    items = build_rows(args)
    if args.table:
        render_table(items, c)
    else:
        render_plain(items, c)


if __name__ == "__main__":
    main()
