#!/usr/bin/env python3
# migratool.py - Outil interactif de migration domotique (rebind des controleurs)
#
# A LANCER SUR L'ANCIEN SERVEUR (IP master courante = 192.168.2.1).
# PREREQUIS : le manager domotique legacy doit etre ARRETE (il occupe le port
# UDP 19876). Sinon l'outil ne pourra pas bind le socket / recevoir les reponses.
#
# Purement live : aucun acces base, aucun log fichier. Socket UDP + affichage ecran.
#
# Principe du protocole (verifie sur le firmware) :
#  - Les commandes sont des chaines texte envoyees en UDP vers <device>:19876.
#  - Le device repond en UNICAST vers l'IP SOURCE de la commande (donc vers cet
#    outil, sur le port 19876) -> le thread recepteur affiche tout ce qui arrive.
#  - UNBIND / STATUS / TEMP / NAME / VERSION ne sont obeis que si la source == master
#    courant du device. Lance depuis .2.1 (= master actuel), l'outil peut donc les
#    faire sur les devices encore bindes a .2.1.
#  - BIND fige l'IP SOURCE comme nouveau master : envoye par cet outil, il bind le
#    device vers .2.1 (utile UNIQUEMENT pour un rollback). Le bind normal vers domo
#    se fait DEPUIS domo (.2.5).

import socket
import threading
import sys
from datetime import datetime

PORT = 19876
RECV_BUF = 1024

# Inventaire connu (modifiable) : nom -> IP
DEVICES = [
    ("Salon",          "192.168.2.101"),
    ("CuisineEntree",  "192.168.2.102"),
    ("CuisineCouloir", "192.168.2.103"),
    ("Chambre",        "192.168.2.104"),
    ("WWC",            "192.168.2.105"),
    ("Bureau",         "192.168.2.106"),
    ("Chambre2",       "192.168.2.107"),
    ("SDB",            "192.168.2.108"),
    ("Jardin (ESP)",   "192.168.2.111"),
]

# Commandes de base proposees au menu (envoi tel quel)
BASE_COMMANDS = ["MASTER", "DISCOVER", "STATUS", "TEMP", "VERSION", "UNBIND", "BIND"]

running = True


def ts():
    return datetime.now().strftime("%H:%M:%S")


def receiver(sock):
    """Thread : affiche tout ce qui arrive sur le socket."""
    while running:
        try:
            data, addr = sock.recvfrom(RECV_BUF)
        except OSError:
            return
        try:
            txt = data.decode("ascii", errors="replace")
        except Exception:
            txt = repr(data)
        print("\n  <<< [{}] {}:{}  ->  {}".format(ts(), addr[0], addr[1], txt))
        print("cmd> ", end="", flush=True)


def send(sock, ip, msg):
    try:
        sock.sendto(msg.encode("ascii"), (ip, PORT))
        print("  >>> [{}] envoye a {} : {}".format(ts(), ip, msg))
    except Exception as e:
        print("  !!! erreur d'envoi a {} : {}".format(ip, e))


def choose_device():
    """Retourne une IP (choix dans la liste ou saisie manuelle), ou None."""
    print("\n--- Devices ---")
    for i, (name, ip) in enumerate(DEVICES, 1):
        print("  {:>2}. {:<16} {}".format(i, name, ip))
    print("   m. saisir une IP manuellement")
    print("   a. annuler")
    sel = input("device> ").strip().lower()
    if sel == "a" or sel == "":
        return None
    if sel == "m":
        ip = input("IP> ").strip()
        return ip if ip else None
    if sel.isdigit() and 1 <= int(sel) <= len(DEVICES):
        return DEVICES[int(sel) - 1][1]
    print("  choix invalide")
    return None


def choose_command(sock, ip):
    while True:
        print("\n--- Cible : {} ---".format(ip))
        for i, c in enumerate(BASE_COMMANDS, 1):
            print("  {:>2}. {}".format(i, c))
        print("   l. commande libre")
        print("   c. changer de device")
        print("   q. quitter")
        sel = input("cmd> ").strip()
        low = sel.lower()
        if low == "q":
            return "QUIT"
        if low == "c":
            return "CHANGE"
        if low == "l":
            free = input("commande libre> ").strip()
            if free:
                send(sock, ip, free)
            continue
        if sel.isdigit() and 1 <= int(sel) <= len(BASE_COMMANDS):
            cmd = BASE_COMMANDS[int(sel) - 1]
            if cmd == "BIND":
                print("  /!\\ BIND depuis cet outil bind le device vers l'IP SOURCE"
                      " de l'outil (.2.1).")
                print("      A n'utiliser que pour un ROLLBACK. Le bind vers domo se"
                      " fait DEPUIS domo (.2.5).")
                if input("      confirmer BIND vers .2.1 ? (oui/non) ").strip().lower() != "oui":
                    print("  annule.")
                    continue
            send(sock, ip, cmd)
            continue
        print("  choix invalide")


def main():
    global running
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        sock.bind(("", PORT))
    except OSError as e:
        print("ERREUR : impossible de bind le port UDP {} ({}).".format(PORT, e))
        print("Le manager legacy est probablement encore actif -> l'arreter d'abord.")
        sys.exit(1)

    print("=" * 60)
    print(" migratool - rebind domotique (live, sans base ni log)")
    print(" Ecoute sur UDP {} - reponses des devices affichees ci-dessous.".format(PORT))
    print(" Rappel : le manager legacy doit etre arrete.")
    print("=" * 60)

    rx = threading.Thread(target=receiver, args=(sock,), daemon=True)
    rx.start()

    try:
        while True:
            ip = choose_device()
            if ip is None:
                if input("\nquitter ? (oui/non) ").strip().lower() == "oui":
                    break
                continue
            res = choose_command(sock, ip)
            if res == "QUIT":
                break
            # res == "CHANGE" -> reboucle sur choose_device
    except (KeyboardInterrupt, EOFError):
        print("\ninterruption.")
    finally:
        running = False
        sock.close()
        print("Termine.")


if __name__ == "__main__":
    main()
