"""
albert_serial.py — utilitarios de comunicacao serial com o Albert.

Usado por 2_Primeira_Instalacao_Albert.bat e 3_Atualizacao_Albert.bat.

Funcionalidades:
- detect_port(): detecta a primeira porta COM com chip ESP32 valido (via DTR/RTS reset + handshake esptool).
  Aqui simplificado: usa pyserial.tools.list_ports e devolve a primeira com VID conhecido (Silicon Labs CP210x, FTDI, CH340).
- send_command_and_wait(port, baud, cmd, prefix, timeout): envia comando + \n, espera linha com prefix, devolve linha.
- whoami(port): envia "whoami", parseia resposta WHOAMI: numSerie=... mod=... sigla=... placa=... sw=... hw=...
- install_from_sd(port): envia "install_from_sd", espera INSTALL_FROM_SD: OK/ERR.

Uso direto via CLI:
    python albert_serial.py detect-port
    python albert_serial.py whoami COM4
    python albert_serial.py install-from-sd COM4
"""
import sys
import time
import argparse
from typing import Optional, Tuple

try:
    import serial
    from serial.tools import list_ports
except ImportError:
    print("[ERRO] pyserial nao instalado. Rode: pip install pyserial")
    sys.exit(2)


BAUD_DEFAULT = 115200
ALBERT_VID_PATTERNS = ["10C4", "1A86", "0403", "303A"]  # SiLabs CP210x, CH340, FTDI, Espressif USB


def detect_port() -> Optional[str]:
    """Procura uma porta COM com chip USB-to-UART de fabricante conhecido."""
    candidates = list_ports.comports()
    for p in candidates:
        hwid = (p.hwid or "").upper()
        if any(("VID:PID=" + vid) in hwid or ("VID_" + vid) in hwid for vid in ALBERT_VID_PATTERNS):
            return p.device
    # Fallback: se nada bateu por VID, retorna a 1a porta nao-virtual
    for p in candidates:
        if "VIRTUAL" not in (p.description or "").upper():
            return p.device
    return None


def open_port(port: str, baud: int = BAUD_DEFAULT, timeout: float = 1.0) -> serial.Serial:
    """Abre porta serial sem reset (DTR/RTS estaveis), bauds padrao."""
    s = serial.Serial()
    s.port = port
    s.baudrate = baud
    s.timeout = timeout
    s.dtr = False
    s.rts = False
    s.open()
    return s


def send_command_and_wait(
    port: str,
    cmd: str,
    expect_prefixes: Tuple[str, ...],
    timeout_s: float = 30.0,
    baud: int = BAUD_DEFAULT,
    echo: bool = True,
) -> Optional[str]:
    """Envia 'cmd\\n' e bloqueia ate ler uma linha que comece com algum dos prefixes,
    ou ate timeout. Devolve a linha encontrada (rstrip), ou None se timeout."""
    s = open_port(port, baud=baud, timeout=0.5)
    try:
        # Drena buffer pre-existente
        s.reset_input_buffer()
        s.write((cmd + "\n").encode("ascii"))
        s.flush()

        deadline = time.time() + timeout_s
        line_buf = b""
        while time.time() < deadline:
            chunk = s.read(256)
            if not chunk:
                continue
            line_buf += chunk
            while b"\n" in line_buf:
                idx = line_buf.index(b"\n")
                line = line_buf[:idx].decode("utf-8", errors="replace").rstrip("\r")
                line_buf = line_buf[idx + 1:]
                if echo:
                    print(f"[serial] {line}")
                for prefix in expect_prefixes:
                    if line.startswith(prefix):
                        return line
        return None
    finally:
        s.close()


def whoami(port: str) -> dict:
    """Pergunta identidade do device. Retorna dict com keys (numSerie, mod, sigla, placa, sw, hw)
    ou dict vazio se nao conseguiu parsear / timeout."""
    line = send_command_and_wait(port, "whoami", ("WHOAMI:",), timeout_s=10.0, echo=False)
    if not line:
        return {}
    out = {}
    # Format: "WHOAMI: numSerie=... mod=... sigla=... placa=... sw=... hw=..."
    body = line.split(":", 1)[1].strip()
    for token in body.split():
        if "=" in token:
            k, v = token.split("=", 1)
            out[k] = v
    return out


def install_from_sd(port: str) -> Tuple[bool, str]:
    """Dispara install_from_sd. Devolve (sucesso, linha_final).
    Linhas no protocolo:
      INSTALL_FROM_SD: BEGIN
      INSTALL_FROM_SD: OK numSerie=... sigla=... placa=...
      INSTALL_FROM_SD: ERR <reason>
    """
    line = send_command_and_wait(
        port, "install_from_sd", ("INSTALL_FROM_SD: OK", "INSTALL_FROM_SD: ERR"),
        timeout_s=30.0, echo=True
    )
    if not line:
        return False, "timeout"
    if line.startswith("INSTALL_FROM_SD: OK"):
        return True, line
    return False, line


def hard_reset(port: str) -> None:
    """Pulso DTR/RTS pra reset hardware do ESP32 (mesmo padrao do esptool)."""
    s = open_port(port, baud=BAUD_DEFAULT, timeout=0.1)
    try:
        # Esptool reset sequence: DTR low + RTS high -> reset; depois libera DTR
        s.dtr = False
        s.rts = True
        time.sleep(0.1)
        s.rts = False
        s.dtr = False
        time.sleep(0.05)
    finally:
        s.close()


def main():
    ap = argparse.ArgumentParser(description="Albert serial utilities")
    sub = ap.add_subparsers(dest="cmd", required=True)
    sub.add_parser("detect-port", help="Detecta porta COM do Albert")
    p_who = sub.add_parser("whoami", help="Identidade do device")
    p_who.add_argument("port")
    p_ins = sub.add_parser("install-from-sd", help="Dispara install_from_sd no device")
    p_ins.add_argument("port")
    p_rst = sub.add_parser("reset", help="Hard reset via DTR/RTS")
    p_rst.add_argument("port")
    args = ap.parse_args()

    if args.cmd == "detect-port":
        p = detect_port()
        if p:
            print(p)
            sys.exit(0)
        print("[ERRO] Nenhuma porta encontrada")
        sys.exit(1)

    if args.cmd == "whoami":
        info = whoami(args.port)
        if not info:
            print("[ERRO] whoami nao respondeu")
            sys.exit(1)
        for k, v in info.items():
            print(f"{k}={v}")
        sys.exit(0)

    if args.cmd == "install-from-sd":
        ok, line = install_from_sd(args.port)
        print(line)
        sys.exit(0 if ok else 1)

    if args.cmd == "reset":
        hard_reset(args.port)
        print("reset OK")
        sys.exit(0)


if __name__ == "__main__":
    main()
