#!/usr/bin/env python3
"""Akzeptiert die SSH-Host-Keys aller aktiven Geraete (Clients) in einem
Rutsch -- macht technisch dasselbe wie "Verbindung testen" im Browser-
Terminal (Verbindungsaufbau -> Host-Key abfragen -> "yes" -> trennen),
nur automatisiert fuer alle Geraete statt eines einzelnen.

Nutzt bewusst dieselbe known_hosts-Datei/dasselbe Format wie die App
(SSH_KNOWN_HOSTS_PATH in app.py) -- Geraete, deren Key hier akzeptiert
wird, muessen danach in der Weboberflaeche NICHT mehr einzeln bestaetigt
werden (SSH-Verbindungstest und Wartungs-Aktionen teilen sich dieselbe
Datei).

Aufruf auf dem Server: sudo /srv/tesm/venv/bin/python3 accept_all_hostkeys.py
"""
import socket
import sqlite3
from concurrent.futures import ThreadPoolExecutor, as_completed

import paramiko

DB_PATH = "/srv/tesm/sqlite.db"
KNOWN_HOSTS_PATH = "/srv/tesm/known_hosts"
DEFAULT_SSH_PORT = 22
CONNECT_TIMEOUT = 4
MAX_WORKERS = 32

conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
devices = conn.execute(
    "SELECT name, ip, ssh_port FROM devices WHERE deleted_at IS NULL ORDER BY name"
).fetchall()
conn.close()


def fetch_key(dev):
    """Nur der Handshake (Verbindungsaufbau + Schluesseltausch), KEIN Login
    -- exakt der Teil, der im Browser-Terminal vor der Passwortabfrage
    passiert. Parallel je Gerät, sonst dauert eine Liste mit vielen
    offline Geraeten (je bis zu CONNECT_TIMEOUT Sekunden) unnoetig lange."""
    host = dev["ip"]
    port = dev["ssh_port"] or DEFAULT_SSH_PORT
    try:
        sock = socket.create_connection((host, port), timeout=CONNECT_TIMEOUT)
        transport = paramiko.Transport(sock)
        transport.start_client(timeout=CONNECT_TIMEOUT)
        server_key = transport.get_remote_server_key()
        transport.close()
        return dev, server_key, None
    except (OSError, paramiko.SSHException, EOFError) as e:
        return dev, None, str(e)


hostkeys = paramiko.HostKeys()
try:
    hostkeys.load(KNOWN_HOSTS_PATH)
except (OSError, paramiko.SSHException):
    pass

accepted, already_known, changed, unreachable = [], [], [], []

with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
    futures = [pool.submit(fetch_key, dev) for dev in devices]
    for future in as_completed(futures):
        dev, server_key, err = future.result()
        host = dev["ip"]
        port = dev["ssh_port"] or DEFAULT_SSH_PORT
        lookup_host = host if port == DEFAULT_SSH_PORT else f"[{host}]:{port}"

        if err is not None:
            unreachable.append((dev["name"], host, err))
            continue

        existing = hostkeys.lookup(lookup_host)
        if existing and existing.get(server_key.get_name()):
            if existing[server_key.get_name()] == server_key:
                already_known.append((dev["name"], host))
                continue
            else:
                # Nicht blind ueberschreiben -- das waere genau das MITM-
                # Szenario, vor dem die App im Terminal auch schuetzt (harte
                # Ablehnung statt automatischer Uebernahme eines GEAENDERTEN
                # Keys).
                changed.append((dev["name"], host))
                continue

        hostkeys.add(lookup_host, server_key.get_name(), server_key)
        accepted.append((dev["name"], host))

hostkeys.save(KNOWN_HOSTS_PATH)

print(f"Neu akzeptiert:     {len(accepted)}")
for name, host in accepted:
    print(f"  + {name} ({host})")

print(f"Bereits bekannt:    {len(already_known)}")

if changed:
    print(f"ACHTUNG -- Host-Key geaendert, NICHT automatisch uebernommen ({len(changed)}):")
    for name, host in changed:
        print(f"  ! {name} ({host}) -- manuell in der App pruefen (moeglicher MITM oder Geraetetausch)")

print(f"Nicht erreichbar:   {len(unreachable)}")
for name, host, err in unreachable:
    print(f"  - {name} ({host}): {err}")
