#!/usr/bin/env python3
"""
sipi_actualizador.py - Buscador de actualizaciones de SiPi.

Compara la version instalada localmente contra la version publicada en
el repositorio oficial del proyecto:

    https://github.com/epsiliusoficial/SiPi/tree/main/PUBLICACION

No usa la API de GitHub (que tiene un limite de peticiones bastante bajo
para pedidos sin autenticar, y a veces devuelve 403 por eso). En cambio
lee un solo archivo de texto plano, chico y estable: 'docs/CHANGELOG.md'
dentro de esa misma carpeta, servido por raw.githubusercontent.com. La
primera linea de ese archivo siempre trae una nota con el formato:

    "el codigo fuente de este paquete corresponde a la vX.Y.Z"

que es exactamente el mismo numero que la variable VERSION de sipi.py.
Este modulo no depende de ninguna libreria externa (solo stdlib) para
no agregarle una dependencia mas a un instalador que ya de por si tiene
que funcionar en maquinas de estudiantes con Python "pelado".
"""
import json
import os
import re
import socket
import time
import urllib.error
import urllib.request

CHANGELOG_URL = (
    "https://raw.githubusercontent.com/epsiliusoficial/SiPi/main/"
    "PUBLICACION/docs/CHANGELOG.md"
)
URL_REPOSITORIO = "https://github.com/epsiliusoficial/SiPi/tree/main/PUBLICACION"

# Cuanto esperar como maximo por una respuesta de red antes de darse por
# vencido -- un valor chico a proposito: esto se llama al arrancar el
# editor y NUNCA tiene que sentirse como que el editor "se cuelga"
# esperando internet.
TIMEOUT_SEGUNDOS = 5

# No tiene sentido golpear GitHub cada vez que se abre el editor si el
# usuario lo abre 20 veces por dia. Se guarda cuando fue la ultima vez
# que se pregunto, y una vez por dia alcanza.
INTERVALO_ENTRE_CHEQUEOS_AUTOMATICOS = 60 * 60 * 24  # 1 dia, en segundos

_PATRON_VERSION = re.compile(r"\bv(\d+(?:\.\d+){1,3})\b")


def _archivo_cache():
    if os.name == "nt":
        base = os.environ.get("APPDATA") or os.path.expanduser("~")
    else:
        base = os.environ.get("XDG_CONFIG_HOME") or os.path.join(os.path.expanduser("~"), ".config")
    carpeta = os.path.join(base, "sipi")
    try:
        os.makedirs(carpeta, exist_ok=True)
    except OSError:
        pass
    return os.path.join(carpeta, "actualizaciones.json")


def _leer_cache():
    try:
        with open(_archivo_cache(), "r", encoding="utf-8") as f:
            return json.load(f)
    except (OSError, ValueError):
        return {}


def _guardar_cache(datos):
    try:
        with open(_archivo_cache(), "w", encoding="utf-8") as f:
            json.dump(datos, f)
    except OSError:
        # Si no se puede persistir el cache no pasa nada grave: en el
        # peor caso se vuelve a preguntar en el proximo arranque.
        pass


def version_a_tupla(texto_version):
    """'41.24.0' -> (41, 24, 0). Ignora sufijos no numericos."""
    partes = []
    for trozo in re.findall(r"\d+", texto_version or ""):
        partes.append(int(trozo))
    return tuple(partes) if partes else (0,)


def obtener_version_remota(timeout=TIMEOUT_SEGUNDOS):
    """Descarga el CHANGELOG oficial y extrae el numero de version que
    dice tener el codigo publicado. Devuelve el string de version (por
    ejemplo '41.24.0') o None si no se pudo determinar (sin internet,
    GitHub caido, formato inesperado, etc.) -- nunca lanza una excepcion
    hacia afuera, porque esto se llama desde un hilo de fondo y un
    buscador de actualizaciones no tiene por que poder tumbar el editor."""
    try:
        pedido = urllib.request.Request(
            CHANGELOG_URL,
            headers={"User-Agent": "SiPi-Editor-Buscador-Actualizaciones/1.0"},
        )
        with urllib.request.urlopen(pedido, timeout=timeout) as respuesta:
            contenido = respuesta.read(4096).decode("utf-8", errors="replace")
    except (urllib.error.URLError, socket.timeout, ValueError, OSError):
        return None

    coincidencia = _PATRON_VERSION.search(contenido)
    if not coincidencia:
        return None
    return coincidencia.group(1)


def verificar_actualizacion(version_actual, forzar=False, timeout=TIMEOUT_SEGUNDOS):
    """Punto de entrada principal. Devuelve un diccionario:

        {
            "chequeado": bool,       # si efectivamente se llego a preguntar a la red
            "hay_actualizacion": bool,
            "version_actual": str,
            "version_remota": str | None,
            "url": str,
            "motivo_sin_chequeo": str | None,  # p.ej. "reciente" o "sin_conexion"
        }

    Si 'forzar' es False (chequeo automatico de fondo), respeta el
    intervalo minimo entre chequeos guardado en el cache -- para que el
    editor no le pegue a GitHub en cada arranque. Con 'forzar=True' (el
    usuario eligio "Buscar actualizaciones..." a mano) siempre pregunta
    a la red sin importar cuando fue la ultima vez.
    """
    resultado = {
        "chequeado": False,
        "hay_actualizacion": False,
        "version_actual": version_actual,
        "version_remota": None,
        "url": URL_REPOSITORIO,
        "motivo_sin_chequeo": None,
    }

    cache = _leer_cache()
    ahora = time.time()
    if not forzar:
        ultimo_chequeo = cache.get("ultimo_chequeo", 0)
        if ahora - ultimo_chequeo < INTERVALO_ENTRE_CHEQUEOS_AUTOMATICOS:
            resultado["motivo_sin_chequeo"] = "reciente"
            return resultado

    version_remota = obtener_version_remota(timeout=timeout)
    if version_remota is None:
        resultado["motivo_sin_chequeo"] = "sin_conexion"
        return resultado

    resultado["chequeado"] = True
    resultado["version_remota"] = version_remota
    cache["ultimo_chequeo"] = ahora
    cache["ultima_version_remota_vista"] = version_remota
    _guardar_cache(cache)

    if version_a_tupla(version_remota) > version_a_tupla(version_actual):
        resultado["hay_actualizacion"] = True
    return resultado


def ya_se_avisto_esta_version(version_remota):
    """True si ya se le mostro al usuario un aviso (no descartable a
    cada rato) para esta version puntual -- para que el banner de 'hay
    una actualizacion' no reaparezca en cada arranque si el usuario ya
    lo vio y decidio no actualizar todavia."""
    cache = _leer_cache()
    return cache.get("version_avisada") == version_remota


def marcar_version_avisada(version_remota):
    cache = _leer_cache()
    cache["version_avisada"] = version_remota
    _guardar_cache(cache)
