
from time import sleep, time
import network
import socket
import json


def decodifica_URL(s):
    #Versión ligera de unquote_plus.
    s = s.replace('+', ' ')
    res = []
    i = 0
    while i < len(s):
        if s[i] == '%' and i + 2 < len(s):
            try:
                res.append(chr(int(s[i+1:i+3], 16)))
                i += 3
            except:
                res.append(s[i])
                i += 1
        else:
            res.append(s[i])
            i += 1
    return ''.join(res)

class Conexion:

    def __init__(self, red=None, clave=None):
        # Desactivar modo Access Point por defecto
        ap = network.WLAN(network.AP_IF)
        ap.active(False)
        
        # Inicializar modo estación (STA)
        self.wlan = network.WLAN(network.STA_IF)
        self.wlan.active(True)
        
        # Credenciales
        self.red = red
        self.clave = clave
        
        # Tiempo de conexión
        self._tiempo_conexion = None

    def conectar(self, red=None, clave=None):
        #Conecta a una red WiFi. Si no se pasan parámetros, usa los guardados.
        if red is not None:
            self.red = red
            self.clave = clave
        
        if self.red is None or self.clave is None:
            raise ValueError("No se han proporcionado credenciales de red (SSID y contraseña).")
        
        if not self.wlan.isconnected():
            print(f"Conectando a la red: {self.red}...")
            self.wlan.connect(self.red, self.clave)
            self._tiempo_conexion = None  

    def estado(self):
        #Devuelve un string con el estado actual de la conexión.
        if self.wlan.isconnected():
            ip, subnet, gateway, dns = self.wlan.ifconfig()
            nombre_red = self.red 
            return f"Conectado a:\nRed: {nombre_red}\nIP: {ip}\nSUBNET: {subnet}\nGATEWAY: {gateway}\nDNS: {dns}"
        else:
            return "No conectado a ninguna red"

    def esperar(self, tiempo_final=30):
        #Espera hasta que se conecte o se agote el tiempo (en segundos).
        tiempo_transcurrido = tiempo_final
        while tiempo_transcurrido > 0:
            if self.wlan.isconnected():
                ip, subnet, gateway, dns = self.wlan.ifconfig()
                print(f'✅ Conectado a: {self.red}')
                print(f'IP: {ip}\nSUBNET: {subnet}\nGATEWAY: {gateway}\nDNS: {dns}')
                self._tiempo_conexion = time()
                return True
            else:
                print("Esperando conexión...")
                sleep(1)
                tiempo_transcurrido -= 1
        print("❌ Tiempo de espera agotado. No se pudo conectar.")
        return False

    def escanear(self, intentos=3, espera=0.5):
        #Escanea redes WiFi disponibles y devuelve un listado formateado.
        todas_redes = {}
        
        for i in range(intentos):
            try:
                redes = self.wlan.scan()
                for red in redes:
                    red_bytes, MAC, canal, potencia, cifrado, visible = red
                    clave = (red_bytes, MAC)
                    if clave not in todas_redes or potencia > todas_redes[clave][3]:
                        todas_redes[clave] = red
            except Exception as e:
                print(f"Error en escaneo {i+1}: {e}")
            if i < intentos - 1:
                sleep(espera)
        
        redes_unicas = sorted(todas_redes.values(), key=lambda x: x[3], reverse=True)

        if not redes_unicas:
            return "No se encontraron redes WiFi."

        lineas = ["{:<25} {:<6} {:<8} {:<10}".format("Nombre red", "Canal", "Potencia", "Seguridad")]
        lineas.append("-" * 52)

        for red in redes_unicas:
            red_bytes, MAC, canal, potencia, cifrado, visible = red
            try:
                nombre_red = red_bytes.decode('utf-8').strip() or "<Sin nombre>"
            except:
                nombre_red = "<No legible>"
            seguridad = "Abierta" if cifrado == 0 else "Cifrada"
            lineas.append("{:<25} {:<6} {:<8} {:<10}".format(nombre_red, canal, potencia, seguridad))

        return "\n".join(lineas)

    def desconectar(self):
        #Desconecta de la red WiFi actual.
        if self.wlan.isconnected():
            self.wlan.disconnect()
            sleep(0.5)
            if not self.wlan.isconnected():
                self._tiempo_conexion = None
                return "✅ Desconectado correctamente de la red WiFi."
            else:
                return "⚠️ No se pudo desconectar completamente."
        else:
            return "ℹ️ Ya estaba desconectado."

    def reconectar(self, max_intentos=3, espera=5):
        #Intenta reconectar a la red guardada.
        if self.red is None or self.clave is None:
            raise ValueError("No hay credenciales guardadas para reconectar.")
        
        intento = 0
        while intento < max_intentos and not self.wlan.isconnected():
            print(f"Intento {intento + 1} de {max_intentos} para reconectar a {self.red}...")
            self.wlan.connect(self.red, self.clave)
            sleep(espera)
            intento += 1
        
        if self.wlan.isconnected():
            self._tiempo_conexion = time()
            return True
        return False

    def info_red(self):
        #Devuelve información detallada de la red conectada (si el firmware lo permite).
        if not self.wlan.isconnected():
            return "No conectado."
        
        try:
            dato_nombre = self.wlan.config('essid')
            nombre_red = dato_nombre.decode() if isinstance(dato_nombre, bytes) else dato_nombre
            MAC = self.wlan.config('mac')
            potencia = self.wlan.status('rssi')
            canal = self.wlan.config('channel')
            MAC_str = ":".join(f"{b:02x}" for b in MAC) if MAC else "N/A"
            return {
                "Nombre red": nombre_red,
                "MAC": MAC_str,
                "Potencia": potencia,
                "Canal": canal
            }
        except Exception as e:
            return f"No se pudo obtener info detallada: {e}"

    def internet_activa(self, host="8.8.8.8", puerto=53, tiempo_final=3):
        #Verifica si hay acceso real a Internet.
        if not self.wlan.isconnected():
            return False
        try:
            s = socket.socket()
            s.settimeout(tiempo_final)
            s.connect((host, puerto))
            s.close()
            return True
        except:
            return False

    def tiempo_conexion(self):
        #Devuelve el tiempo (en segundos) desde que se conectó.
        if not self.wlan.isconnected() or self._tiempo_conexion is None:
            return 0
        return time() - self._tiempo_conexion

    def guardar_credenciales(self, archivo="wifi.json"):
        #Guarda las credenciales en un archivo JSON (sin codificar)
        if self.red and self.clave:
            try:
                with open(archivo, "w") as f:
                    json.dump({"red": self.red, "clave": self.clave}, f)
                print(f"Credenciales guardadas en {archivo}")
            except Exception as e:
                print(f"Error al guardar credenciales: {e}")
        else:
            print("No hay credenciales para guardar.")

    @classmethod
    def cargar_credenciales(cls, archivo="wifi.json"):
        #Carga credenciales desde un archivo JSON y devuelve una instancia.
        try:
            with open(archivo, "r") as f:
                data = json.load(f)
                return cls(red=data["red"], clave=data["clave"])
        except Exception as e:
            print(f"No se pudieron cargar credenciales desde {archivo}: {e}")
            return cls()  # instancia sin credenciales

    def activar_ap(self, nombre_red="Esp32_WiFi", clave="12345678"):
        #Activa el modo Access Point para configuración remota.
        ap = network.WLAN(network.AP_IF)
        ap.active(True)
        ap.config(essid=nombre_red, password=clave, authmode=network.AUTH_WPA_WPA2_PSK)
        ip = ap.ifconfig()[0]
        print(f"📶 Modo AP activado. Conéctate a '{nombre_red}'")
        return ap

    def diagnostico(self):
        #Muestra un reporte completo del estado de red.
        print("\n" + "="*50)
        print("🔍 DIAGNÓSTICO DE RED")
        print("="*50)
        print(self.estado())
        
        if self.wlan.isconnected():
            print("\n📊 Información detallada:")
            info = self.info_red()
            if isinstance(info, dict):
                for k, v in info.items():
                    print(f"  {k}: {v}")
            else:
                print(f"  {info}")
            
            print(f"\n🌐 Internet activa: {'Sí' if self.internet_activa() else 'No'}")
            print(f"⏱️  Tiempo conectado: {self.tiempo_conexion():.1f} segundos")
        else:
            print("\n📡 Escaneando redes cercanas...")
            print(self.escanear(intentos=2))
        print("="*50 + "\n")
        
        

    def navegador(self, red_ap="ConfigurarWiFi", clave_ap="12345678", puerto=80):
        #Crea un servidor y abre una pagina para cargar credenciales de red
        
        print("Iniciando modo configuración vía navegador...")
        ap = self.activar_ap(nombre_red=red_ap, clave=clave_ap)
        ip_ap = ap.ifconfig()[0]

        #Escaneo y generación de HTML 
        escaneo = self.escanear(intentos=2)
        lineas = escaneo.split('\n')[2:]
        redes = []
        for linea in lineas:
            if linea.strip() and not linea.startswith("-"):
                nombre_red = linea[:25].strip()
                if nombre_red and nombre_red not in ("<Sin nombre>", "<No legible>"):
                    ssid_js = nombre_red.replace("'", "\\'").replace('"', '\\"')
                    redes.append(ssid_js)

        red_guardada = self.red if self.red else ""

        html = f"""<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>WiFi Setup</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        body {{
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #f5f7fa;
            margin: 0;
            padding: 20px;
            display: flex;
            justify-content: center;
        }}
        .contenedor {{
            background: white;
            border-radius: 12px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
            padding: 24px;
            width: 100%;
            max-width: 500px;
        }}
        h2 {{
            text-align: center;
            color: #2c3e50;
            margin-top: 0;
        }}
        .noticia {{
            background: #fff8e1;
            border-left: 4px solid #ff9800;
            padding: 14px 16px;
            margin: 20px 0;
            border-radius: 6px;
            font-size: 14px;
            color: #5d4037;
        }}
        input, button {{
            width: 100%;
            padding: 12px;
            margin: 10px 0;
            box-sizing: border-box;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 16px;
        }}
        button {{
            background: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
            font-weight: bold;
        }}
        button:hover {{
            background: #45a049;
        }}
        h3 {{
            margin-top: 24px;
            color: #34495e;
            font-size: 18px;
        }}
        .caja-redes {{
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            max-height: 220px;
            overflow-y: auto;
            padding: 12px;
            background: #fafafa;
            margin-top: 8px;
        }}
        .redes {{
            padding: 10px;
            cursor: pointer;
            border-radius: 6px;
            margin-bottom: 6px;
            background: white;
            border: 1px solid #eee;
            transition: background 0.2s;
        }}
        .redes:hover {{
            background: #e8f5e9;
            border-color: #4CAF50;
        }}
    </style>
</head>
<body>
    <div class="contenedor">
        <h2>📶 Configurar Red WiFi</h2>
        
        <div class="noticia">
            ⚠️ <b>Importante:</b> Después de conectar la ESP32, <b>desconecta manualmente</b> la computadora 
            de la red "<i>{red_ap}</i>" y conéctala a la red que seleccionaste para tu placa.
        </div>

        <form method="POST" onsubmit="setTimeout(() => {{ window.close(); }}, 200);">
            <input type="text" name="red" value="{red_guardada}" placeholder="Nombre de la red (SSID)" required>
            <input type="password" name="clave" placeholder="Contraseña" required>
            <button type="submit" class="btn">Conectar ESP32 a la Red WiFi</button>
        </form>

        <h3>Redes disponibles:</h3>
        <div class="caja-redes">
"""
        for r in redes:
            html += f'            <div class="redes" onclick="document.getElementsByName(\'red\')[0].value=\'{r}\'">{r}</div>\n'
        html += """        </div>
    </div>
</body>
</html>"""
        s = socket.socket()
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((ip_ap, puerto))
        s.listen(1)
        print(f"\n🌐 Abre en tu navegador: http://{ip_ap}:{puerto}\n")

        cliente_ip = None  

        try:
            while True:
                conexion, addr = s.accept()
                cliente_ip = addr[0]
                print(f"Conexión desde: {cliente_ip}")

                try:
                    request = conexion.recv(1024).decode()
                except:
                    conexion.close()
                    continue

                if "POST /" in request:
                    #Procesar credenciales
                    body = request.split('\r\n\r\n', 1)[1]
                    if "red=" in body and "clave=" in body:
                        red_enc = body.split("red=")[1].split("&")[0]
                        clave_enc = body.split("clave=")[1].split("&")[0]
                        red = decodifica_URL(red_enc)
                        clave = decodifica_URL(clave_enc)

                        self.red = red
                        self.clave = clave
                        self.guardar_credenciales()
                        
                        
                        # Responder 
                        pagina_final = """HTTP/1.1 200 OK\r
    Content-Type: text/html\r

    <!DOCTYPE html>
    <html>
    <head><meta charset="UTF-8"><title>Conectando...</title></head>
    <body style="text-align:center;font-family:Arial;padding:30px;background:#f0fff0;">
    <h2>✅ Configuración recibida</h2>
    <p><b>➡️ Ahora, desconecta la computadora de """ + red_ap + """ y conéctala a la red """ + self.red+ """.</b></p>
    <p>Esto cerrará la AP de la Esp32 automáticamente cuando ya no haya dispositivos conectados.</p>
    <script>setTimeout(() => {{ window.close(); }}, 2000);</script>
    </body>
    </html>"""
                        
                        conexion.send(pagina_final)
                        conexion.close()

                        #Conectar a la red 
                        print(f"Conectando a: {red}...")
                        self.conectar(red=red, clave=clave)
                        if self.esperar(tiempo_final=15):
                            ip_asignada = self.wlan.ifconfig()[0]
                            print("✅ Conexión exitosa a la red objetivo.")
                        else:
                            print("❌ Falló la conexión.")
                        
 
 
                        #Esperar a que el cliente se desconecte del AP
                        print("Esperando que el cliente se desconecte del AP...")
                        for _ in range(90):  # Espera hasta 90 segundos
                            try:
                                stations = ap.status('stations')
                                if not stations:
                                    print("✅ Cliente desconectado. Apagando AP.")
                                    break
                            except Exception as e:
                                print("Advertencia: error al leer estaciones:", e)
                            sleep(1)
                        else:
                            print("⏳ tiempo_final (90s). Apagando AP de todas formas.")

                        # Apagar AP y salir
                        ap.active(False)
                        s.close()
                        print("📶 AP desactivado. Configuración completada.")
                        return True

                else:
                    # Solicitud GET
                    conexion.send("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + html)
                    conexion.close()

        except KeyboardInterrupt:
            print("\n⏹️ Configuración cancelada.")
        finally:
            s.close()
            if ap.active():
                ap.active(False)
                print("📶 AP desactivado.")
