#!/usr/bin/env python3
"""Construye la entrega confiable paso a paso, con un solo paquete en vuelo.

    python3 rdt_sim.py                     lista los escenarios
    python3 rdt_sim.py <escenario>         corre uno y muestra el diagrama de tiempo
    python3 rdt_sim.py todos               corre todos

Es un simulador: no hay red ni sockets. El canal sigue un guion que dice qué le pasa a cada
transmisión, en orden. Así, un error ocurre siempre en el mismo lugar y se puede razonar.
Cada versión del protocolo agrega un solo mecanismo a la anterior:

    v1    canal perfecto: se envía y ya
    v2    errores de bits: checksum, ACK y NAK
    v2.1  ACK o NAK dañados: un bit de secuencia en los datos
    v2.2  lo mismo sin NAK: el ACK dice qué número confirma
    v3    pérdidas: un temporizador
"""
import heapq
import sys
from dataclasses import dataclass, field

ONE_WAY_MS = 10
TIMEOUT_MS = 30
LATE_EXTRA_MS = 25  # lo que se atrasa un paquete con el resultado "tarde"


@dataclass
class Packet:
    kind: str  # DATA, ACK o NAK
    seq: int | None = None
    payload: str | None = None
    corrupt: bool = False

    def label(self):
        text = self.kind
        if self.seq is not None:
            text += f" {self.seq}"
        if self.payload is not None:
            text += f' "{self.payload}"'
        return text


@dataclass
class Result:
    version: str
    trace: list = field(default_factory=list)
    delivered: list = field(default_factory=list)
    finished: bool = False
    transmissions: int = 0


class Simulation:
    """El emisor y el receptor de una versión, unidos por un canal con guion."""

    def __init__(self, version, messages, script=()):
        self.version = version
        self.messages = list(messages)
        self.script = list(script)
        self.result = Result(version)
        self.events = []
        self.order = 0
        self.now = 0
        self.index = 0  # próximo mensaje por confirmar
        self.send_seq = 0
        self.expected_seq = 0
        self.timer_generation = 0

    @property
    def uses_seq(self):
        return self.version in ("v2.1", "v2.2", "v3")

    # El canal --------------------------------------------------------------------------
    def schedule(self, delay, action, *args):
        heapq.heappush(self.events, (self.now + delay, self.order, action, args))
        self.order += 1

    def log(self, side, text):
        self.result.trace.append((self.now, side, text))

    def transmit(self, side, packet, is_retransmission=False):
        outcome = self.script.pop(0) if self.script else "ok"
        self.result.transmissions += 1
        note = {"ok": "", "corrupt": "  [el canal daña bits]", "lost": "  [el canal lo pierde]",
                "late": "  [llega tarde]"}[outcome]
        again = " (otra vez)" if is_retransmission else ""
        self.log(side, f"envía {packet.label()}{again}{note}")
        if outcome == "lost":
            return
        delay = ONE_WAY_MS + (LATE_EXTRA_MS if outcome == "late" else 0)
        if outcome == "corrupt":
            packet = Packet(packet.kind, packet.seq, packet.payload, corrupt=True)
        handler = self.at_receiver if side == "emisor" else self.at_sender
        self.schedule(delay, handler, packet)

    # El emisor -------------------------------------------------------------------------
    def data_packet(self):
        seq = self.send_seq if self.uses_seq else None
        return Packet("DATA", seq, self.messages[self.index])

    def send_current(self, is_retransmission=False):
        self.transmit("emisor", self.data_packet(), is_retransmission)
        if self.version == "v3":
            self.timer_generation += 1
            self.schedule(TIMEOUT_MS, self.on_timeout, self.timer_generation)

    def advance(self):
        self.timer_generation += 1  # apaga el temporizador
        self.index += 1
        self.send_seq ^= 1
        if self.index < len(self.messages):
            self.send_current()
        else:
            self.result.finished = True
            self.log("emisor", "terminó: todo confirmado")

    def on_timeout(self, generation):
        if generation != self.timer_generation or self.result.finished:
            return
        self.log("emisor", "vence el temporizador")
        self.send_current(is_retransmission=True)

    def at_sender(self, packet):
        if self.result.finished:
            self.log("emisor", f"recibe {packet.label()}: ya terminó, lo ignora")
            return
        if packet.corrupt:
            if self.version == "v3":
                self.log("emisor", "recibe una respuesta dañada: la ignora y espera al temporizador")
            else:
                self.log("emisor", "recibe una respuesta dañada: no sabe qué era, repite")
                self.send_current(is_retransmission=True)
            return
        if packet.kind == "NAK":
            self.log("emisor", f"recibe {packet.label()}: repite")
            self.send_current(is_retransmission=True)
        elif self.version in ("v2.2", "v3") and packet.seq != self.send_seq:
            if self.version == "v3":
                self.log("emisor", f"recibe {packet.label()}: es de un paquete viejo, la ignora")
            else:
                self.log("emisor", f"recibe {packet.label()}: esperaba ACK {self.send_seq}, repite")
                self.send_current(is_retransmission=True)
        else:
            self.log("emisor", f"recibe {packet.label()}")
            self.advance()

    # El receptor -----------------------------------------------------------------------
    def deliver(self, packet):
        text = "?!?" if packet.corrupt else packet.payload
        self.result.delivered.append(text)
        return text

    def reply(self, kind, seq=None):
        self.transmit("receptor", Packet(kind, seq))

    def at_receiver(self, packet):
        version = self.version
        if version == "v1":
            # Sin checksum, el receptor no se entera de que los bits llegaron dañados.
            self.log("receptor", f'recibe y entrega "{self.deliver(packet)}"')
            self.schedule(1, self.next_without_reply)
            return
        if packet.corrupt:
            if version in ("v2", "v2.1"):
                self.log("receptor", "recibe datos dañados")
                self.reply("NAK")
            else:
                self.log("receptor", "recibe datos dañados: confirma el último bueno")
                self.reply("ACK", self.expected_seq ^ 1)
            return
        if version == "v2":
            self.log("receptor", f'recibe y entrega "{self.deliver(packet)}"')
            self.reply("ACK")
            return
        if packet.seq == self.expected_seq:
            self.log("receptor", f'recibe {packet.seq} y entrega "{self.deliver(packet)}"')
            self.expected_seq ^= 1
            self.reply("ACK", None if version == "v2.1" else packet.seq)
        else:
            self.log("receptor", f"recibe {packet.seq}: duplicado, no lo entrega")
            self.reply("ACK", None if version == "v2.1" else packet.seq)

    def next_without_reply(self):
        self.index += 1
        if self.index < len(self.messages):
            self.send_current()
        else:
            self.result.finished = True
            self.log("emisor", "terminó")

    # La simulación ---------------------------------------------------------------------
    def run(self):
        self.send_current()
        while self.events:
            self.now, _, action, args = heapq.heappop(self.events)
            action(*args)
        if not self.result.finished:
            self.log("emisor", "se quedó esperando para siempre: nada en camino, ningún temporizador")
        return self.result


# nombre: (versión, guion del canal, qué muestra)
MESSAGES = ["a", "b", "c"]
SCENARIOS = {
    "v1-perfecto": ("v1", [], "v1 sobre un canal perfecto: funciona"),
    "v1-errores": ("v1", ["ok", "corrupt"], "v1 con un error de bits: entrega basura sin enterarse"),
    "v2-datos": ("v2", ["ok", "ok", "corrupt"], "v2 con un dato dañado: el NAK lo arregla"),
    "v2-ack": ("v2", ["ok", "corrupt"], "v2 con un ACK dañado: el emisor repite y se duplica"),
    "v21-ack": ("v2.1", ["ok", "corrupt"], "v2.1 con un ACK dañado: el bit de secuencia lo arregla"),
    "v22-datos": ("v2.2", ["ok", "ok", "corrupt"], "v2.2 con un dato dañado: un ACK repetido hace de NAK"),
    "v22-perdida": ("v2.2", ["ok", "ok", "lost"], "v2.2 con una pérdida: se queda esperando para siempre"),
    "v3-dato": ("v3", ["ok", "ok", "lost"], "v3 con un dato perdido: el temporizador lo repite"),
    "v3-ack": ("v3", ["ok", "lost"], "v3 con un ACK perdido: repite, y el receptor reconoce el duplicado"),
    "v3-tarde": ("v3", ["ok", "late"], "v3 con un ACK tarde: retransmisión innecesaria, pero correcta"),
    "v3-sin-seq": ("v3-sin-seq", ["ok", "lost"], "temporizador sin números de secuencia: se duplica"),
}


class NoSeqSimulation(Simulation):
    """El temporizador de v3 sin el bit de secuencia, para ver por qué hace falta el bit."""

    @property
    def uses_seq(self):
        return False

    def __init__(self, messages, script=()):
        super().__init__("v3", messages, script)

    def at_receiver(self, packet):
        self.log("receptor", f'recibe y entrega "{self.deliver(packet)}"')
        self.reply("ACK")

    def at_sender(self, packet):
        if self.result.finished:
            self.log("emisor", f"recibe {packet.label()}: ya terminó, lo ignora")
            return
        self.log("emisor", f"recibe {packet.label()}")
        self.advance()


def run_scenario(name):
    version, script, _ = SCENARIOS[name]
    if version == "v3-sin-seq":
        return NoSeqSimulation(MESSAGES, script).run()
    return Simulation(version, MESSAGES, script).run()


def print_result(name):
    result = run_scenario(name)
    print(f"--- {name}: {SCENARIOS[name][2]}")
    for when, side, text in result.trace:
        print(f"{when:5d} ms  {side:<9}{text}")
    verdict = "correcto" if result.delivered == MESSAGES and result.finished else "INCORRECTO"
    print(f"entregado: {' '.join(result.delivered)}   ({verdict}, "
          f"{result.transmissions} transmisiones)")


def main():
    if len(sys.argv) == 2 and sys.argv[1] == "todos":
        for name in SCENARIOS:
            print_result(name)
            print()
    elif len(sys.argv) == 2 and sys.argv[1] in SCENARIOS:
        print_result(sys.argv[1])
    else:
        for name, (_, _, description) in SCENARIOS.items():
            print(f"{name:<13}{description}")


if __name__ == "__main__":
    main()
