#!/usr/bin/env python3
"""Por qué un connection ID sobrevive a un cambio de dirección y una conexión TCP no.

No es QUIC: es un eco de juguete sobre UDP, sin cifrado y sin validar el camino, para mostrar
una sola idea. El servidor busca la sesión por el ID que viene en el mensaje, y no por la
dirección y el puerto de origen. Al lado corre un eco TCP, que la busca por los cuatro campos.

  cid_echo.py serve <puerto>
  cid_echo.py udp <servidor> <puerto>     el cliente cambia de red a la mitad
  cid_echo.py tcp <servidor> <puerto>     lo mismo, con una conexión TCP
  cid_echo.py restore                     devuelve al cliente a su red original
"""
import socket
import subprocess
import sys
import threading
import time

ROUNDS = 12
SWITCH_AT = 6
GAP = 0.1
WAIT = 1.0
DEVICE = "eth1"
# Dos redes del router mb: es lo que le pasa a un celular al dejar el Wi-Fi.
FIRST = ("10.213.1.10", "10.213.1.1")
SECOND = ("10.213.3.20", "10.213.3.1")


def serve_udp(port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(("0.0.0.0", port))
    sessions = {}
    while True:
        data, source = sock.recvfrom(2048)
        cid, _, text = data.partition(b" ")
        session = sessions.setdefault(cid, {"count": 0, "source": source})
        session["count"] += 1
        if session["source"] != source:
            print(f"sesión {cid.decode()}: cambió de {session['source']} a {source}, sigue en "
                  f"el mensaje {session['count']}", flush=True)
            session["source"] = source
        sock.sendto(b"%s %d %s" % (cid, session["count"], text), source)


def serve_tcp(port):
    listener = socket.create_server(("0.0.0.0", port))
    while True:
        conn, _ = listener.accept()
        threading.Thread(target=echo_tcp, args=(conn,), daemon=True).start()


def echo_tcp(conn):
    with conn:
        while True:
            data = conn.recv(2048)
            if not data:
                return
            conn.sendall(data)


def move(old, new):
    """Pone la dirección nueva, con su ruta por defecto, y quita la vieja."""
    subprocess.run(["ip", "addr", "add", f"{new[0]}/24", "dev", DEVICE], check=True, capture_output=True)
    subprocess.run(["ip", "addr", "del", f"{old[0]}/24", "dev", DEVICE], check=True, capture_output=True)
    subprocess.run(["ip", "route", "replace", "default", "via", new[1]], check=True, capture_output=True)


def client_udp(server, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((FIRST[0], 0))
    sock.settimeout(WAIT)
    answered = 0
    for number in range(ROUNDS):
        if number == SWITCH_AT:
            move(FIRST, SECOND)
            sock.close()
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.bind((SECOND[0], 0))
            sock.settimeout(WAIT)
            print(f"-- el cliente cambia a {SECOND[0]}, con otro puerto y otro socket", flush=True)
        sock.sendto(b"c0ffee mensaje-%d" % number, (server, port))
        try:
            reply = sock.recv(2048)
            answered += 1
            print(f"{number}: {reply.decode()}", flush=True)
        except socket.timeout:
            print(f"{number}: sin respuesta", flush=True)
        time.sleep(GAP)
    print(f"respondidos: {answered} de {ROUNDS}", flush=True)
    return answered


def client_tcp(server, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind((FIRST[0], 0))
    sock.connect((server, port))
    sock.settimeout(WAIT)
    answered = 0
    for number in range(ROUNDS):
        if number == SWITCH_AT:
            move(FIRST, SECOND)
            print(f"-- el cliente cambia a {SECOND[0]}; el socket sigue atado a {FIRST[0]}", flush=True)
        try:
            sock.sendall(b"mensaje-%d" % number)
            reply = sock.recv(2048)
            answered += 1
            print(f"{number}: {reply.decode()}", flush=True)
        except OSError as error:
            print(f"{number}: sin respuesta ({error})", flush=True)
        time.sleep(GAP)
    sock.close()
    print(f"respondidos: {answered} de {ROUNDS}", flush=True)
    return answered


def main():
    if len(sys.argv) == 3 and sys.argv[1] == "serve":
        port = int(sys.argv[2])
        threading.Thread(target=serve_tcp, args=(port,), daemon=True).start()
        serve_udp(port)
    elif len(sys.argv) == 4 and sys.argv[1] in ("udp", "tcp"):
        client = client_udp if sys.argv[1] == "udp" else client_tcp
        client(sys.argv[2], int(sys.argv[3]))
    elif len(sys.argv) == 2 and sys.argv[1] == "restore":
        move(SECOND, FIRST)
    else:
        print(__doc__)
        return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())
