#!/usr/bin/env python3
"""Pasarela USB entre unha micro:bit e un campo «Monitor de datos» de Peneira.

A micro:bit non ten WiFi: non pode facer o POST HTTP por si soa. Este script le
as liñas JSON que a placa escribe no porto serie USB e reenvíaas a Peneira co
token privado do monitor.

Uso:

    pip install pyserial
    export PENEIRA_MONITOR_URL="http://peneira.cesga.es/api/web-monitor/<seriesId>/readings"
    export PENEIRA_MONITOR_TOKEN="<token privado>"
    python3 peneira_gateway.py

Tamén se pode pasar todo por argumentos:

    python3 peneira_gateway.py --url ... --token ... --port /dev/cu.usbmodem1102

O enderezo e o token obtéñense en Peneira ao crear a observación, co botón
«Obter URL e token». Son privados: non os publiques nin os gardes nun
repositorio.
"""

import argparse
import glob
import json
import os
import sys
import time
import urllib.error
import urllib.request

# Peneira acepta como máximo unha medición por segundo e por monitor.
MIN_INTERVAL_S = 1.1
BAUD_RATE = 115200

# Patróns do porto serie da micro:bit segundo o sistema operativo.
PORT_PATTERNS = (
    "/dev/cu.usbmodem*",   # macOS
    "/dev/ttyACM*",        # Linux
)


def find_port():
    """Devolve o primeiro porto serie que semella unha micro:bit."""
    try:
        from serial.tools import list_ports
    except ImportError:
        list_ports = None

    if list_ports is not None:
        for port in list_ports.comports():
            haystack = f"{port.description} {port.manufacturer or ''}".lower()
            # A micro:bit V1/V2 identifícase como interface DAPLink de ARM/NXP.
            if "microbit" in haystack or "micro:bit" in haystack or "daplink" in haystack:
                return port.device

    for pattern in PORT_PATTERNS:
        matches = sorted(glob.glob(pattern))
        if matches:
            return matches[0]
    return None


def send_reading(url, token, payload, timeout=10):
    """Fai o POST a Peneira. Devolve (ok, mensaxe)."""
    body = json.dumps(payload).encode("utf-8")
    request = urllib.request.Request(
        url,
        data=body,
        method="POST",
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}",
        },
    )
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            answer = json.loads(response.read().decode("utf-8") or "{}")
            missing = answer.get("missing") or []
            note = f" (rutas sen valor: {', '.join(missing)})" if missing else ""
            return True, f"gardada ás {answer.get('observedAt', '?')}{note}"
    except urllib.error.HTTPError as error:
        detail = error.read().decode("utf-8", "replace")[:200]
        if error.code == 429:
            retry_after = error.headers.get("Retry-After", "1")
            return False, f"demasiado rápido, agarda {retry_after} s ({detail})"
        if error.code == 401:
            return False, f"token non válido: revisa PENEIRA_MONITOR_TOKEN ({detail})"
        if error.code == 404:
            return False, f"monitor non atopado: revisa a URL ({detail})"
        if error.code == 410:
            return False, f"o monitor xa non acepta medicións ({detail})"
        return False, f"HTTP {error.code}: {detail}"
    except urllib.error.URLError as error:
        return False, f"non se puido conectar con Peneira: {error.reason}"


def main():
    parser = argparse.ArgumentParser(
        description="Reenvía a Peneira as medicións que a micro:bit escribe no porto serie."
    )
    parser.add_argument("--url", default=os.environ.get("PENEIRA_MONITOR_URL", ""),
                        help="enderezo POST do monitor (ou variable PENEIRA_MONITOR_URL)")
    parser.add_argument("--token", default=os.environ.get("PENEIRA_MONITOR_TOKEN", ""),
                        help="token privado do monitor (ou variable PENEIRA_MONITOR_TOKEN)")
    parser.add_argument("--port", default=os.environ.get("PENEIRA_MICROBIT_PORT", ""),
                        help="porto serie da micro:bit; detéctase automaticamente se non se indica")
    parser.add_argument("--dry-run", action="store_true",
                        help="amosa as medicións sen enviarllas a Peneira")
    args = parser.parse_args()

    if not args.dry_run and (not args.url or not args.token):
        parser.error("indica --url e --token (ou as variables PENEIRA_MONITOR_URL e "
                     "PENEIRA_MONITOR_TOKEN). Obtéñense en Peneira con «Obter URL e token».")

    try:
        import serial
    except ImportError:
        sys.exit("Falta pyserial. Instálao con:  pip install pyserial")

    port = args.port or find_port()
    if not port:
        sys.exit("Non se atopou ningunha micro:bit. Conéctaa por USB e volve tentalo "
                 "(ou indica o porto con --port).")

    try:
        connection = serial.Serial(port, BAUD_RATE, timeout=1)
    except serial.SerialException as error:
        sys.exit(f"Non se puido abrir {port}: {error}\n"
                 "Pecha calquera outro programa que estea usando a placa "
                 "(o editor de MakeCode, un monitor serie…).")

    print(f"Escoitando a micro:bit en {port} a {BAUD_RATE} baudios.")
    print("Preme o botón A da placa para enviar unha medición. Ctrl+C para saír.\n")

    last_sent = 0.0
    with connection:
        while True:
            try:
                raw = connection.readline()
            except KeyboardInterrupt:
                break
            if not raw:
                continue

            line = raw.decode("utf-8", "replace").strip()
            if not line.startswith("{"):
                # A placa tamén pode escribir mensaxes que non son medicións.
                if line:
                    print(f"  · {line}")
                continue

            try:
                payload = json.loads(line)
            except json.JSONDecodeError:
                print(f"  ! liña ignorada, non é JSON válido: {line[:120]}")
                continue

            print(f"→ {json.dumps(payload, ensure_ascii=False)}")
            if args.dry_run:
                continue

            wait = MIN_INTERVAL_S - (time.monotonic() - last_sent)
            if wait > 0:
                time.sleep(wait)

            ok, message = send_reading(args.url, args.token, payload)
            last_sent = time.monotonic()
            print(f"  {'✓' if ok else '✗'} {message}")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nSaíndo.")
