#!/usr/bin/env python3
# coding: utf-8
"""
Tailscale status plugin for NetAlertX v25.10.1
Writes native plugin objects to last_result.TSCALE.log so NetAlertX ingests them as a scan.
"""

import os
import sys
import json
import subprocess
import hashlib
import time
from datetime import datetime, timezone

# Extend path like the template
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])

# imports from template environment (exists in NetAlertX image)
from plugin_helper import Plugin_Objects
from helper import timeNowTZ, get_setting_value
from logger import mylog, Logger
from const import logPath

# Plugin identity
pluginName = "TSCALE"
LOG_PATH = os.path.join(logPath, "plugins")
RESULT_FILE = os.path.join(LOG_PATH, f"last_result.{pluginName}.log")

# Init logger level from settings (fallbacks safe)
try:
    Logger(get_setting_value('LOG_LEVEL'))
except Exception:
    Logger("INFO")

# Utilities
def safe_run(cmd_list, timeout):
    try:
        proc = subprocess.run(cmd_list, capture_output=True, text=True, timeout=timeout, check=False)
        return proc.returncode, proc.stdout, proc.stderr
    except subprocess.TimeoutExpired as e:
        mylog('error', [f'[{pluginName}] Command timeout: {" ".join(cmd_list)}'])
        return 124, "", f"timeout: {e}"
    except Exception as e:
        mylog('error', [f'[{pluginName}] Command failed: {e}'])
        return 1, "", str(e)

def sha1_hex(s: str) -> str:
    return hashlib.sha1(s.encode('utf-8')).hexdigest()

def make_synthetic_mac(seed: str) -> str:
    # Create semi-MAC style string from hash to serve as stable primaryId
    h = sha1_hex(seed)
    # take first 12 hex digits => 6 bytes
    mac = ":".join(h[i:i+2] for i in range(0, 12, 2))
    return f"TS-{mac}"

def parse_size_to_bytes(s: str) -> int:
    # Accepts "62,06 MB", "243.05 MB", "123 KB", "1.2 GB"
    if not s:
        return 0
    s = s.strip()
    s = s.replace(",", ".")
    parts = s.split()
    try:
        value = float(parts[0])
    except Exception:
        return 0
    unit = parts[1].upper() if len(parts) > 1 else ""
    if unit.startswith("KB"):
        return int(value * 1024)
    if unit.startswith("MB"):
        return int(value * 1024 * 1024)
    if unit.startswith("GB"):
        return int(value * 1024 * 1024 * 1024)
    return int(value)

def iso_to_local_str(iso_str: str) -> str:
    if not iso_str:
        return ""
    try:
        dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
        return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S %z")
    except Exception:
        return iso_str

# Main parsing logic
def parse_tailscale_json(raw_json: str):
    try:
        data = json.loads(raw_json)
    except Exception as e:
        mylog('error', [f'[{pluginName}] JSON parse error: {e}'])
        return None
    devices = []

    # Parent node info (local gateway)
    self_info = data.get("Self", {})
    interface_ips = data.get("TailscaleIPs", []) or self_info.get("TailscaleIPs", [])
    primary_ip = interface_ips[0] if interface_ips else ""
    parent_id = f"TS-PARENT-{sha1_hex(self_info.get('ID','srv'))[:10]}"

    parent = {
        "device_id": parent_id,
        "mac_address": parent_id,
        "ip_address": primary_ip,
        "hostname": "Nœud réseau",
        # vendor contains link info per your request
        "vendor": "Lien fibre - FREE",
        "device_type": "Gateway",
        "last_seen": "online" if self_info.get("Online", False) else "offline",
        "extra": "Appareil du réseau principal;Noeud racine",
        "rx_bytes": int(self_info.get("RxBytes", 0) or 0),
        "tx_bytes": int(self_info.get("TxBytes", 0) or 0)
    }
    devices.append(parent)

    # Peers
    peers = data.get("Peer", {}) or {}
    for key, p in peers.items():
        # stable id: use Peer ID or PublicKey or HostName
        seed = p.get("ID") or p.get("PublicKey") or p.get("HostName") or key
        mac = make_synthetic_mac(seed)
        ips = p.get("TailscaleIPs") or []
        ip = ips[0] if ips else ""
        hostname = p.get("HostName") or p.get("DNSName") or "unknown"
        name_with_suffix = f"{hostname} - Tailscale"
        last_seen = iso_to_local_str(p.get("LastSeen")) or ""
        online = p.get("Online", False)
        vendor = p.get("OS", "") or ""
        device = {
            "device_id": seed,
            "mac_address": mac,
            "ip_address": ip,
            "hostname": name_with_suffix,
            "vendor": vendor,
            "device_type": "Tailscale device",
            "last_seen": last_seen if last_seen else ("online" if online else "offline"),
            "extra": json.dumps({
                "DNSName": p.get("DNSName"),
                "PublicKey": p.get("PublicKey"),
                "Relay": p.get("Relay"),
                "TaildropTarget": p.get("TaildropTarget")
            }),
            "rx_bytes": int(p.get("RxBytes", 0) or 0),
            "tx_bytes": int(p.get("TxBytes", 0) or 0),
            "parent_fk": parent_id
        }
        devices.append(device)

    return devices

def parse_tailscale_text(text: str):
    # Minimal but robust fallback: look for lines with IP and hostname
    devices = []
    lines = text.splitlines()
    # create a parent placeholder from first lines if possible
    primary_ip = ""
    for L in lines:
        if "tailscale0" in L or "100." in L:
            parts = L.split()
            for part in parts:
                if part.count(".") >= 2 and part.startswith("100."):
                    primary_ip = part.split("/")[0]
                    break
            if primary_ip:
                break
    parent_id = f"TS-PARENT-{sha1_hex(primary_ip or 'srv')[:10]}"
    parent = {
        "device_id": parent_id,
        "mac_address": parent_id,
        "ip_address": primary_ip,
        "hostname": "Nœud réseau",
        "vendor": "Lien fibre - FREE",
        "device_type": "Gateway",
        "last_seen": "online",
        "extra": "Appareil du réseau principal;Noeud racine",
        "rx_bytes": 0,
        "tx_bytes": 0
    }
    devices.append(parent)

    # attempts to capture peers like "hostname (100.x.x.x)"
    for L in lines:
        if "(" in L and ")" in L:
            try:
                name = L.split("(")[0].strip()
                ip = L.split("(")[1].split(")")[0].strip()
                seed = f"{name}-{ip}"
                mac = make_synthetic_mac(seed)
                device = {
                    "device_id": seed,
                    "mac_address": mac,
                    "ip_address": ip,
                    "hostname": f"{name} - Tailscale",
                    "vendor": "",
                    "device_type": "Tailscale device",
                    "last_seen": "",
                    "extra": "",
                    "rx_bytes": 0,
                    "tx_bytes": 0,
                    "parent_fk": parent_id
                }
                devices.append(device)
            except Exception:
                continue
    return devices

def main():
    mylog('verbose', [f'[{pluginName}] Starting Tailscale plugin'])
    # Get settings
    tailscale_bin = get_setting_value('TAILSCALE_BIN') or "/usr/bin/tailscale"
    iface = get_setting_value('INTERFACE') or "tailscale0"
    timeout = int(get_setting_value('TIMEOUT') or 8)
    dry_run = bool(get_setting_value('DRY_RUN') or False)

    # Prepare Plugin_Objects writer
    plugin_objects = Plugin_Objects(RESULT_FILE)

    # Try json command first
    rc, out, err = safe_run([tailscale_bin, "status", "--json"], timeout)
    devices = None
    if rc == 0 and out:
        devices = parse_tailscale_json(out)
    else:
        mylog('info', [f'[{pluginName}] JSON mode failed (rc={rc}), stderr={err}; falling back to text parser'])
        rc2, out2, err2 = safe_run([tailscale_bin, "status"], timeout)
        if rc2 == 0 and out2:
            devices = parse_tailscale_text(out2)
        else:
            mylog('error', [f'[{pluginName}] tailscale status failed both JSON and text: rc1={rc}, rc2={rc2}, err1={err}, err2={err2}'])
            devices = []

    # If dry-run, just print payload to logs and exit
    if dry_run:
        mylog('info', [f'[{pluginName}] Dry run enabled. Payload:'])
        mylog('info', [json.dumps(devices, indent=2, ensure_ascii=False)])
        return 0

    # Build and write plugin objects (parent first)
    if devices:
        # add parent as first object
        # Ensure parent is first item (we already constructed it that way)
        for d in devices:
            primary = d.get('mac_address') or d.get('device_id') or ''
            secondary = d.get('ip_address') or ''
            watched1 = d.get('hostname') or ''
            watched2 = d.get('vendor') or ''
            watched3 = d.get('device_type') or ''
            watched4 = d.get('last_seen') or ''
            extra = d.get('extra') or ''
            foreign = d.get('mac_address') or d.get('device_id') or ''
            # For children, set watched1 to "{hostname} - Tailscale" was done earlier
            try:
                plugin_objects.add_object(
                    primaryId=primary,
                    secondaryId=secondary,
                    watched1=watched1,
                    watched2=watched2,
                    watched3=watched3,
                    watched4=watched4,
                    extra=extra,
                    foreignKey=foreign
                )
            except Exception as e:
                mylog('error', [f'[{pluginName}] Failed adding object {primary}: {e}'])

        # write result file for NetAlertX ingestion
        plugin_objects.write_result_file()
        mylog('info', [f'[{pluginName}] Wrote {len(devices)} objects to {RESULT_FILE}'])
    else:
        mylog('info', [f'[{pluginName}] No devices discovered. Writing empty result file.'])
        plugin_objects.write_result_file()

    return 0

if __name__ == "__main__":
    main()
