#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Bot Telegram para los SAI Delta InsightPower.

/estado    -> botones para consultar DELTA1 o DELTA2 (respuesta desde cache).
Boton Actualizar -> fuerza consulta SNMP nueva.
Vigilancia -> sondea en segundo plano, alimenta la cache y avisa
              solo cuando un SAI cambia de estado.

Compatible Python 3.5. Requiere delta_status.py y formato.py al lado.
"""

import os
import sys
import threading
import time
import traceback

import requests

from delta_status import leer_sai
from formato import formatear_telegram

RUTA = os.path.dirname(os.path.abspath(__file__))
RUTA_ENV = os.path.join(RUTA, ".env")


def cargar_env(ruta):
    valores = {}
    if not os.path.exists(ruta):
        return valores
    with open(ruta, "r") as f:
        for linea in f:
            linea = linea.strip()
            if not linea or linea.startswith("#") or "=" not in linea:
                continue
            clave, _, valor = linea.partition("=")
            valores[clave.strip()] = valor.strip().strip('"').strip("'")
    return valores


ENV = cargar_env(RUTA_ENV)

TOKEN = ENV.get("SAI_BOT_TOKEN", "")
COMMUNITY = ENV.get("SAI_COMMUNITY", "")
HOST = ENV.get("SAI_HOST", "")

CHATS_OK = set()
for trozo in ENV.get("SAI_CHATS", "").split(","):
    trozo = trozo.strip()
    if trozo:
        CHATS_OK.add(int(trozo))

SAIS = [
    ("DELTA1", int(ENV.get("SAI_PUERTO_1", "16161"))),
    ("DELTA2", int(ENV.get("SAI_PUERTO_2", "16162"))),
]

# cada cuantos segundos sondea el hilo de vigilancia
INTERVALO = int(ENV.get("SAI_INTERVALO", "60"))
# cuanto vale un dato cacheado antes de considerarlo viejo
CACHE_TTL = int(ENV.get("SAI_CACHE_TTL", "90"))
# sondeos fallidos seguidos antes de avisar de que no responde
FALLOS_PARA_AVISAR = 3

API = "https://api.telegram.org/bot" + TOKEN + "/"
TIMEOUT_POLL = 30

ESTADO_PREVIO = {}
FALLOS = {}

# etiqueta -> (timestamp, datos)
CACHE = {}
CACHE_LOCK = threading.Lock()


# ------------------------------------------------------------- cache

def guardar_cache(nombre, datos):
    with CACHE_LOCK:
        CACHE[nombre] = (time.time(), datos)


def leer_cache(nombre):
    """Devuelve (ts, datos) si hay dato fresco, o None."""
    with CACHE_LOCK:
        entrada = CACHE.get(nombre)
    if not entrada:
        return None
    ts, datos = entrada
    if (time.time() - ts) > CACHE_TTL:
        return None
    return ts, datos


def obtener(nombre, puerto, forzar=False):
    """Devuelve (datos, ts, de_cache)."""
    if not forzar:
        entrada = leer_cache(nombre)
        if entrada is not None:
            return entrada[1], entrada[0], True

    datos = leer_sai(HOST, puerto, COMMUNITY)
    guardar_cache(nombre, datos)
    return datos, time.time(), False


# ------------------------------------------------------------- telegram

def api(metodo, **params):
    try:
        r = requests.post(API + metodo, json=params, timeout=TIMEOUT_POLL + 10)
        datos = r.json()
    except Exception:
        traceback.print_exc()
        return None
    if not datos.get("ok"):
        print("API {0} error: {1}".format(metodo, datos.get("description")))
        return None
    return datos.get("result")


def teclado_menu():
    fila = []
    for etiqueta, _puerto in SAIS:
        fila.append({"text": etiqueta, "callback_data": "sai:" + etiqueta})
    return {"inline_keyboard": [fila]}


def teclado_resultado(destino):
    return {
        "inline_keyboard": [[
            {"text": "\U0001F504 Actualizar", "callback_data": "new:" + destino},
            {"text": "⬅ Volver", "callback_data": "menu"},
        ]]
    }


def enviar(chat_id, texto, teclado=None):
    return api("sendMessage", chat_id=chat_id, text=texto,
               reply_markup=teclado, parse_mode="HTML", protect_content=True)


def editar(chat_id, message_id, texto, teclado=None):
    return api("editMessageText", chat_id=chat_id, message_id=message_id,
               text=texto, reply_markup=teclado, parse_mode="HTML")


def difundir(texto):
    """Manda el mismo mensaje a todos los chats autorizados."""
    for chat_id in CHATS_OK:
        enviar(chat_id, texto)


# ------------------------------------------------------------- consulta

def autorizado(chat_id):
    return chat_id in CHATS_OK


def texto_estado(etiqueta, forzar=False):
    for nombre, puerto in SAIS:
        if nombre == etiqueta:
            datos, ts, de_cache = obtener(nombre, puerto, forzar)
            texto = formatear_telegram(datos, nombre, ts)
            if de_cache:
                edad = int(time.time() - ts)
                texto += "\n<i>Dato de hace {0} s. Pulsa Actualizar para " \
                         "consultar el SAI ahora.</i>".format(edad)
            return texto
    return "SAI desconocido: " + str(etiqueta)


def menu(chat_id, message_id=None):
    texto = "⚡ <b>Estado de los SAI</b>\n\nElige uno:"
    if message_id:
        editar(chat_id, message_id, texto, teclado_menu())
    else:
        enviar(chat_id, texto, teclado_menu())


def procesar_mensaje(msg):
    chat_id = msg["chat"]["id"]
    texto = msg.get("text", "").strip().lower()
    if "@" in texto:
        texto = texto.split("@")[0]

    if not autorizado(chat_id):
        print("chat NO autorizado: {0} ({1})".format(chat_id, texto))
        return

    print("chat {0}: {1}".format(chat_id, texto))

    if texto in ("/estado", "/start", "/sai"):
        menu(chat_id)


def procesar_callback(cb):
    chat_id = cb["message"]["chat"]["id"]
    message_id = cb["message"]["message_id"]

    api("answerCallbackQuery", callback_query_id=cb["id"])

    if not autorizado(chat_id):
        print("callback NO autorizado: {0}".format(chat_id))
        return

    dato = cb.get("data", "")

    if dato == "menu":
        menu(chat_id, message_id)
        return

    # sai: -> permite cache   new: -> fuerza consulta
    if dato.startswith("sai:") or dato.startswith("new:"):
        forzar = dato.startswith("new:")
        destino = dato.split(":", 1)[1]
        if forzar:
            editar(chat_id, message_id, "Consultando " + destino + "...")
        editar(chat_id, message_id, texto_estado(destino, forzar),
               teclado_resultado(destino))


# ------------------------------------------------------------- vigilancia

def clave_estado(d):
    if not d.get("ok"):
        return "sin_respuesta"
    if d.get("en_bateria"):
        return "bateria"
    if d.get("en_bypass"):
        return "bypass"
    if d.get("bat_critica"):
        return "bat_critica"
    return "normal"


def cabecera(nombre, previo, nuevo):
    if nuevo == "bateria":
        return ("\U0001F534 <b>{0} EN DESCARGA</b>\n"
                "Ha entrado en bateria.").format(nombre)

    if nuevo == "bypass":
        return ("\U0001F7E0 <b>{0} EN BYPASS</b>\n"
                "La carga esta sin proteccion.").format(nombre)

    if nuevo == "bat_critica":
        return ("\U0001F7E0 <b>{0} BATERIA CRITICA</b>").format(nombre)

    if nuevo == "sin_respuesta":
        return ("⚠️ <b>{0} NO RESPONDE</b>\n"
                "Sin contestacion SNMP.").format(nombre)

    # nuevo == "normal"
    if previo == "bateria":
        return ("\U0001F7E2 <b>{0} RED RESTABLECIDA</b>\n"
                "Ha vuelto a red electrica.").format(nombre)
    if previo == "sin_respuesta":
        return ("\U0001F7E2 <b>{0} VUELVE A RESPONDER</b>").format(nombre)
    return ("\U0001F7E2 <b>{0} NORMAL</b>").format(nombre)


def revisar(nombre, puerto):
    d = leer_sai(HOST, puerto, COMMUNITY)
    guardar_cache(nombre, d)          # alimenta la cache para /estado

    nuevo = clave_estado(d)

    # histeresis: no avisar al primer fallo suelto de red
    if nuevo == "sin_respuesta":
        FALLOS[nombre] = FALLOS.get(nombre, 0) + 1
        if FALLOS[nombre] < FALLOS_PARA_AVISAR:
            return
    else:
        FALLOS[nombre] = 0

    previo = ESTADO_PREVIO.get(nombre)

    # primer sondeo tras arrancar
    if previo is None:
        ESTADO_PREVIO[nombre] = nuevo
        print("vigilancia {0}: estado inicial {1}".format(nombre, nuevo))
        if nuevo != "normal":
            difundir(cabecera(nombre, previo, nuevo) + "\n\n"
                     + formatear_telegram(d, nombre))
        return

    if nuevo == previo:
        return

    ESTADO_PREVIO[nombre] = nuevo
    print("vigilancia {0}: {1} -> {2}".format(nombre, previo, nuevo))
    difundir(cabecera(nombre, previo, nuevo) + "\n\n"
             + formatear_telegram(d, nombre))


def vigilar():
    print("Vigilancia activa cada {0} s, cache {1} s".format(
        INTERVALO, CACHE_TTL))
    while True:
        for nombre, puerto in SAIS:
            try:
                revisar(nombre, puerto)
            except Exception:
                traceback.print_exc()
        time.sleep(INTERVALO)


# ------------------------------------------------------------- main

def main():
    if not TOKEN or not COMMUNITY or not HOST:
        print("Falta SAI_BOT_TOKEN, SAI_COMMUNITY o SAI_HOST en .env")
        sys.exit(1)

    if not CHATS_OK:
        print("SAI_CHATS vacio en .env: define el chat_id del grupo")
        sys.exit(1)

    yo = api("getMe")
    if not yo:
        print("No se pudo contactar con Telegram. Revisa el token.")
        sys.exit(1)

    print("Bot arrancado: @{0}".format(yo.get("username")))
    print("Chats autorizados: {0}".format(sorted(CHATS_OK)))

    hilo = threading.Thread(target=vigilar)
    hilo.daemon = True
    hilo.start()

    offset = None
    while True:
        try:
            updates = api("getUpdates", offset=offset, timeout=TIMEOUT_POLL)
            if updates is None:
                time.sleep(5)
                continue

            for up in updates:
                offset = up["update_id"] + 1
                if "message" in up:
                    procesar_mensaje(up["message"])
                elif "callback_query" in up:
                    procesar_callback(up["callback_query"])

        except KeyboardInterrupt:
            print("parado por el usuario")
            break
        except Exception:
            traceback.print_exc()
            time.sleep(5)


if __name__ == "__main__":
    main()
