"""
ICEBERG Hardware Agent - Servicio HTTP para el programador IR de pulseras.

Endpoints publicos (sin auth):
  GET  /status        - Estado del agente y conexion USB

Endpoints protegidos (requieren Bearer token):
  GET  /info          - Version, uptime, estadisticas y ultimo error
  POST /program       - Programar una pulsera (bloquea hasta OK o timeout)
  GET  /deactivations - Cuantas pulseras fueron desactivadas (y reset del contador)
  GET  /program_count - Total de pulseras programadas desde que arranco el agente

Keep-alive:
  Envia R=0,G=0,B=0 cada ~500ms para mantener el IR activo.
  Cuando una pulsera en fase roja se acerca al lector, recibe este paquete
  y se apaga (responde OK). El agente cuenta esas desactivaciones.

Seguridad:
  El token se genera en agent_token.txt al primer arranque y se reutiliza.
  Los clientes deben enviar: Authorization: Bearer <token>
"""
import sys
import os

# Con Python embebido, el directorio del script puede no estar en sys.path.
# Garantizarlo aqui antes de cualquier import local (programmer, config).
_agent_dir = os.path.dirname(os.path.abspath(__file__))
if _agent_dir not in sys.path:
    sys.path.insert(0, _agent_dir)

from flask import Flask, request, jsonify
from programmer import (
    connect, disconnect, is_connected,
    program_wristband, build_packet, send_packet, read_response,
)
from config import PORT, HOST, AGENT_ID, HEARTBEAT_INTERVAL
import threading
import logging
import secrets
import time

# ---------------------------------------------------------------------------
# Logging a archivo
# ---------------------------------------------------------------------------

_LOG_DIR  = os.path.join(os.path.dirname(__file__), "logs")
_LOG_FILE = os.path.join(_LOG_DIR, "agent.log")
os.makedirs(_LOG_DIR, exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(_LOG_FILE, encoding="utf-8"),
        logging.StreamHandler(),   # tambien a consola
    ],
)
log = logging.getLogger("agent")

# ---------------------------------------------------------------------------
# Token de autenticacion
# ---------------------------------------------------------------------------

_TOKEN_FILE  = os.path.join(os.path.dirname(__file__), "agent_token.txt")
_AGENT_TOKEN = ""


def _load_token_from_file() -> str:
    """Carga el token desde agent_token.txt. Genera uno local si no existe."""
    global _AGENT_TOKEN
    if os.path.exists(_TOKEN_FILE):
        try:
            tok = open(_TOKEN_FILE, "r").read().strip()
            if tok:
                _AGENT_TOKEN = tok
                log.info("Token cargado desde archivo (fallback local)")
                return tok
        except Exception:
            pass
    # Último recurso: generar token local
    tok = secrets.token_hex(32)
    try:
        with open(_TOKEN_FILE, "w") as f:
            f.write(tok)
        log.warning("Token generado localmente (servidor no disponible)")
    except Exception as e:
        log.error(f"No se pudo guardar el token: {e}")
    _AGENT_TOKEN = tok
    return tok


def _register_with_server() -> str:
    """
    Intenta registrar el agente con el servidor y obtener el token oficial.
    POST /api/agents/register → {"token": "..."}

    Si el servidor no responde, usa el token guardado en archivo como fallback.
    """
    import socket
    import httpx as _httpx
    from config import SERVER_HOST, SERVER_PORT, AGENT_ID, AGENT_NAME

    try:
        host = socket.gethostbyname(socket.gethostname())
    except Exception:
        host = "127.0.0.1"

    try:
        r = _httpx.post(
            f"http://{SERVER_HOST}:{SERVER_PORT}/api/agents/register",
            json={"id": AGENT_ID, "name": AGENT_NAME, "host": host, "port": PORT},
            timeout=5,
        )
        if r.status_code in (200, 201):
            token = r.json().get("token", "")
            if token:
                global _AGENT_TOKEN
                _AGENT_TOKEN = token
                try:
                    with open(_TOKEN_FILE, "w") as f:
                        f.write(token)
                except Exception:
                    pass
                log.info(f"Agente registrado en servidor (HTTP {r.status_code}). Token actualizado.")
                return token
        log.warning(f"Registro rechazado por servidor: HTTP {r.status_code}")
    except Exception as e:
        log.warning(f"Servidor no disponible para registro: {e}")

    # Fallback: usar token del archivo
    return _load_token_from_file()


def _check_auth():
    """
    Verifica el Bearer token en el header Authorization.
    Retorna (response, code) si hay error, None si OK.
    """
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        return jsonify({"error": "Authorization required"}), 401
    if auth[7:] != _AGENT_TOKEN:
        return jsonify({"error": "Invalid token"}), 403
    return None


# ---------------------------------------------------------------------------
# Estado global
# ---------------------------------------------------------------------------

app = Flask(__name__)

device      = None
device_lock = threading.Lock()
start_time  = time.time()

# Estado de conexion USB — actualizado por keep-alive y program,
# leido por /status SIN lock para evitar bloqueo.
_usb_connected = False

_AGENT_VERSION = "1.3.0"

# Contador de desactivaciones (pulseras que respondieron OK al paquete 0,0,0)
_deact_lock  = threading.Lock()
_deact_count = 0

# Cooldown: despues de programar, pausar keep-alive para que no desactive la pulsera
_PROGRAM_COOLDOWN = 5  # segundos de gracia para retirar la pulsera
_last_program_time = 0.0

# Estadisticas de programaciones
_stats_lock           = threading.Lock()
_total_programmed     = 0
_total_deactivated    = 0
_last_error: str | None = None


def get_device():
    global device, _usb_connected
    if device is None or not is_connected(device):
        if device is not None:
            log.warning("Programador USB desconectado, reconectando...")
        disconnect(device)
        device = connect()
        if device:
            _usb_connected = True
            log.info("Programador USB conectado")
        else:
            _usb_connected = False
            log.warning("Programador USB no encontrado")
    return device


# ---------------------------------------------------------------------------
# Keep-alive: lector siempre activo
# ---------------------------------------------------------------------------

_ZERO_PACKET        = build_packet(red_min=0, green_min=0, blue_min=0)
_keep_alive_running = True


def _keep_alive_loop():
    """
    Hilo de fondo: envia R=0,G=0,B=0 periodicamente.
    - Mantiene el IR del lector encendido (luz intermitente visible).
    - Cuando una pulsera en rojo se acerca, responde OK -> se apaga.
    - El contador _deact_count se incrementa por cada pulsera desactivada.
    - Respeta cooldown despues de programar para no desactivar la pulsera recien programada.
    """
    global device, _deact_count, _total_deactivated
    while _keep_alive_running:
        # Cooldown: no enviar paquete de desactivacion si recien se programo una pulsera
        if time.time() - _last_program_time < _PROGRAM_COOLDOWN:
            time.sleep(0.3)
            continue

        acquired = device_lock.acquire(blocking=False)
        if acquired:
            try:
                dev = get_device()
                if dev:
                    try:
                        send_packet(dev, _ZERO_PACKET)
                        # Esperar respuesta breve: OK = pulsera desactivada
                        resp = read_response(dev, timeout=0.2)
                        if resp == 'OK':
                            with _deact_lock:
                                _deact_count += 1
                            with _stats_lock:
                                _total_deactivated += 1
                            log.info("Pulsera desactivada por lector (keep-alive OK)")
                    except Exception:
                        pass
            finally:
                device_lock.release()
        # Pausa: 0.3s + 0.2s lectura = ~500ms por ciclo
        time.sleep(0.3)


# ---------------------------------------------------------------------------
# Heartbeat: notificar al servidor que esta terminal esta viva
# ---------------------------------------------------------------------------

_heartbeat_running = True


def _heartbeat_loop():
    """
    Hilo de fondo: envia POST /api/agents/{agent_id}/heartbeat cada 30s.
    Si falla, loguea warning pero no se detiene.
    """
    import httpx as _httpx
    from config import SERVER_HOST, SERVER_PORT, AGENT_ID

    url = f"http://{SERVER_HOST}:{SERVER_PORT}/api/agents/{AGENT_ID}/heartbeat"

    import socket as _socket
    try:
        _my_host = _socket.gethostbyname(_socket.gethostname())
    except Exception:
        _my_host = "127.0.0.1"

    while _heartbeat_running:
        time.sleep(HEARTBEAT_INTERVAL)
        try:
            headers = {"Authorization": f"Bearer {_AGENT_TOKEN}"}
            r = _httpx.post(url, headers=headers, json={"host": _my_host, "port": PORT}, timeout=5)
            if r.status_code == 200:
                log.debug(f"Heartbeat OK")
            else:
                log.warning(f"Heartbeat rechazado: HTTP {r.status_code}")
        except Exception as e:
            log.warning(f"Heartbeat fallido: {e}")


# ---------------------------------------------------------------------------
# Endpoints — publicos (sin autenticacion)
# ---------------------------------------------------------------------------

@app.route('/status')
def status():
    """Estado basico — sin autenticacion (usado para verificar conectividad)."""
    # Usa _usb_connected (variable) en vez de consultar el device directo.
    # Asi no bloquea con device_lock ni interfiere con /program.
    return jsonify({
        'agent':                'running',
        'programmer_connected': _usb_connected,
        'uptime_seconds':       int(time.time() - start_time),
        'keep_alive':           'active',
    })


# ---------------------------------------------------------------------------
# Endpoints — protegidos con Bearer token
# ---------------------------------------------------------------------------

@app.route('/info')
def info():
    """Informacion extendida del agente: version, uptime, estadisticas."""
    err = _check_auth()
    if err:
        return err

    with device_lock:
        dev = get_device()
    with _stats_lock:
        programmed  = _total_programmed
        deactivated = _total_deactivated
        last_err    = _last_error
    return jsonify({
        'version':              _AGENT_VERSION,
        'uptime_seconds':       int(time.time() - start_time),
        'programmer_connected': dev is not None,
        'total_programmed':     programmed,
        'total_deactivated':    deactivated,
        'last_error':           last_err,
    })


@app.route('/deactivations')
def deactivations():
    """
    Retorna cuantas pulseras fueron desactivadas desde la ultima consulta
    y resetea el contador.
    """
    err = _check_auth()
    if err:
        return err

    global _deact_count
    with _deact_lock:
        count = _deact_count
        _deact_count = 0
    return jsonify({'count': count})


@app.route('/program_count')
def program_count():
    """
    Retorna el total de pulseras programadas desde que arranco el agente
    (sin resetear el contador).
    """
    err = _check_auth()
    if err:
        return err

    with _stats_lock:
        programmed = _total_programmed
    return jsonify({'total_programmed': programmed})


@app.route('/program', methods=['POST'])
def program():
    """
    Programa una pulsera con los minutos indicados.

    Body JSON:
      red_minutes:   int
      green_minutes: int
      blue_minutes:  int
      timeout:       int (default 30)
      session_id:    str (opcional)
    """
    err = _check_auth()
    if err:
        return err

    global _total_programmed, _last_error

    data = request.get_json()
    if not data:
        return jsonify({'status': 'error', 'message': 'JSON body required'}), 400

    red     = data.get('red_minutes',   0)
    green   = data.get('green_minutes', 0)
    blue    = data.get('blue_minutes',  0)
    timeout = data.get('timeout',      30)
    session = data.get('session_id',  '')

    log.info(f"Programando pulsera: G={green}m B={blue}m R={red}m timeout={timeout}s session={session}")

    global _usb_connected

    with device_lock:
        dev = get_device()
        if dev is None:
            msg = 'Programmer not connected'
            with _stats_lock:
                _last_error = msg
            log.error(msg)
            return jsonify({'status': 'error', 'message': msg}), 503

        result, detail = program_wristband(dev, red, green, blue, timeout)

        # Si hubo error de USB, marcar como desconectado
        if result == 'error':
            _usb_connected = False

    if result == 'ok':
        global _last_program_time
        _last_program_time = time.time()
        with _stats_lock:
            _total_programmed += 1
        log.info(f"Pulsera programada OK a las {detail} (keep-alive pausado {_PROGRAM_COOLDOWN}s)")
        return jsonify({'status': 'ok', 'programmed_at': detail})
    elif result == 'timeout':
        msg = 'Timeout esperando pulsera'
        with _stats_lock:
            _last_error = msg
        log.warning(msg)
        return jsonify({'status': 'timeout'}), 408
    else:
        with _stats_lock:
            _last_error = detail
        log.error(f"Error programando pulsera: {detail}")
        return jsonify({'status': 'error', 'message': detail}), 500


# ---------------------------------------------------------------------------
# Arranque
# ---------------------------------------------------------------------------

if __name__ == '__main__':
    # Registrar con servidor o cargar token del archivo
    token = _register_with_server()

    print("=" * 55)
    print("  ICEBERG Hardware Agent")
    print(f"  http://{HOST}:{PORT}")
    print(f"  Terminal: {AGENT_ID}")
    print(f"  Version {_AGENT_VERSION}")
    print("=" * 55)
    print()
    print(f"[AUTH] Token: {token[:8]}...{token[-4:]}")
    print(f"[AUTH] Archivo: {_TOKEN_FILE}")
    print()

    dev = get_device()
    if dev:
        print("[OK] Programador USB conectado")
    else:
        print("[!]  Programador USB no detectado (se reintentara)")

    print()
    print(f"[LOG] Archivo de log: {_LOG_FILE}")
    print("[IR]  Keep-alive activo -> R=0,G=0,B=0 cada 500ms")
    print("      Pulseras en rojo se desactivaran al acercarse al lector")
    print()
    print("Endpoints publicos:")
    print(f"  GET  /status")
    print("Endpoints protegidos (Bearer token):")
    print(f"  GET  /info")
    print(f"  GET  /deactivations")
    print(f"  GET  /program_count")
    print(f"  POST /program")
    print()

    # Hilo de keep-alive
    t = threading.Thread(target=_keep_alive_loop, daemon=True)
    t.start()

    # Hilo de heartbeat al servidor
    t2 = threading.Thread(target=_heartbeat_loop, daemon=True)
    t2.start()
    print(f"[HB]  Heartbeat cada {HEARTBEAT_INTERVAL}s -> /api/agents/{AGENT_ID}/heartbeat")
    print()

    app.run(host=HOST, port=PORT, debug=False)
