# -*- coding: utf-8 -*-
import requests
import unicodedata
import re
import time

# ===============================================================
# FUENTES
# ===============================================================

FUENTES = [
    "http://wb9xxxwh.k34zyer.xyz/get.php?username=Syrge0913T&password=Pwpetpafe121&type=m3u_plus&output=ts",
    "http://ktivguth.msostvip.com/get.php?username=9LVMAQVJ&password=W62VGURD&type=m3u_plus&output=m3u8",
    "https://drive.google.com/uc?export=download&id=1FutBqDBBVka99fevlRpbQgidH1hlx9CN",
]

ARCHIVO_SALIDA = "/mnt/nvme/iptv/lista_filtrada.m3u"


# ===============================================================
# TELEGRAM
# ===============================================================

def enviar_telegram(mensaje):

    try:
        configuracion = {}

        with open(
            "/mnt/nvme/iptv/telegram.env",
            "r",
            encoding="utf-8"
        ) as f:
            for linea in f:
                if "=" in linea:
                    clave, valor = linea.strip().split("=", 1)
                    configuracion[clave] = valor

        token = configuracion.get("TELEGRAM_BOT_TOKEN")
        chat_id = configuracion.get("TELEGRAM_CHAT_ID")

        if not token or not chat_id:
            raise ValueError("Configuración incompleta")

        respuesta = requests.post(
            f"https://api.telegram.org/bot{token}/sendMessage",
            data={
                "chat_id": chat_id,
                "text": mensaje
            },
            timeout=10
        )

        respuesta.raise_for_status()
        return True

    except Exception:
        print("AVISO: No se pudo enviar la notificación de Telegram.")
        return False



# ===============================================================
# PATRONES DE BÚSQUEDA
# ===============================================================

PATRON_FUTBOL = re.compile(
    r'\b(FOOTBALL|FUTBOL|SOCCER|LALIGA|LA\s+LIGA|PREMIER|CHAMPIONS|UEFA|FIFA|'
    r'DAZN|BEIN|ESPN|EUROSPORT|SKY\s+SPORT|BT\s+SPORT|TNT\s+SPORT|ELEVEN\s+SPORT|'
    r'MOVISTAR\s+LIGA|MOVISTAR\s+SPORT|GOL\s+TV|GOL\s+PLAY|SETANTA|GO3\s+SPORT|'
    r'ARENA\s+SPORT|SPORT\s+KLUB|SWISS\s+SPORT|RTS\s+SPORT|SRF\s+SPORT)\b',
    re.IGNORECASE
)

PATRON_F1 = re.compile(
    r'\b(FORMULA\s*1|FORMULA\s+ONE|F1|MOTORSPORT|MOTORSPORTS|F1\s+TV|F1TV|'
    r'SKY\s+SPORTS\s+F1|SKY\s+F1|F1\s+LIVE)\b',
    re.IGNORECASE
)

PATRON_ADULT = re.compile(
    r'\b(ВЗРОСЛЫЕ|ADULT|XXX|PENTHOUSE|PLAYBOY|EROTIC|EROTICA|BLUE\s+HUSTLER|HUSTLER|'
    r'DORCEL|BABELAND|VIVID|WICKED|PRIVATE|SEX|SEXTV|PORNO|PORN|HARDCORE)\b',
    re.IGNORECASE
)


# ===============================================================
# PATRONES DE PAÍSES
# ===============================================================

GRUPOS_PAIS = {

    "Espana": [
        r'\bSPAIN\b',
        r'\bESPANA\b',
        r'\bESPAÑA\b',
        r'(^|[, ])ES:',
        r'\|ES\b',
        r'\bES\s*:'
    ],

    "Polonia": [
        r'\bPOLAND\b',
        r'\bPOLSKA\b',
        r'\bPOLISH\b',
        r'(^|[, ])PL:',
        r'\|PL\b',
        r'\bPL\s*:'
    ],

    "United Kingdom": [
        r'\bUNITED KINGDOM\b',
        r'\bUK\b',
        r'\bENGLAND\b',
        r'\bBRITAIN\b',
        r'(^|[, ])UK:',
        r'(^|[, ])GB:',
        r'\|UK\b',
        r'\|GB\b'
    ],

    "Latino": [
        r'\bLATINO\b',
        r'\bLATAM\b',
        r'\bLATIN\b',
        r'\bMEXICO\b',
        r'\bARGENTINA\b',
        r'\bCOLOMBIA\b',
        r'\bCHILE\b',
        r'\bPERU\b',
        r'\bVENEZUELA\b'
    ],
}


# ===============================================================
# ORDEN PRIORITARIO ESPAÑA
#
# Las cadenas principales aparecen primero.
# Todo lo que no esté aquí aparecerá después alfabéticamente.
# ===============================================================

ORDEN_ESPANA = [

    # 1. TDT GENERALISTA Y AUTONOMICAS
    "LA 1",
    "LA 1 CANARIAS",
    "TVE 1",
    "TVE1",
    "LA1",
    "LA 2",
    "TVE 2",
    "TVE2",
    "LA2",
    "ANTENA 3",
    "ANTENA3",
    "CUATRO",
    "TELECINCO",
    "TELE 5",
    "TELE5",
    "LA SEXTA",
    "LASEXTA",
    "LA6",
    "NEOX",
    "NOVA",
    "MEGA",
    "ATRESERIES",
    "FDF",
    "FACTORIA DE FICCION",
    "DIVINITY",
    "ENERGY",
    "BE MAD",
    "BEMAD",
    "TRECE",
    "DMAX",
    "PARAMOUNT",
    "TV3",
    "ETB",
    "TVG",
    "CANAL SUR",
    "ARAGON TV",
    "TELEMADRID",
    "À PUNT",
    "CMM",
    "TV CANARIA",
    "IB3",
    "TPA",
    "7TV",

    # 2. NOTICIAS
    "CANAL 24 HORAS",
    "24 HORAS",
    "CNN",
    "EURONEWS",
    "BLOOMBERG",
    "FRANCE 24",
    "SKY NEWS",

    # 3. MOVISTAR PLUS+
    "MOVISTAR",
    "M+",
    "M. LALIGA",
    "LALIGA TV",

    # 4. SERIES Y PELICULAS
    "AXN WHITE",
    "AXN",
    "CALLE 13",
    "SYFY",
    "WARNER",
    "COMEDY CENTRAL",
    "AMC",
    "FOX",
    "TCM",
    "HOLLYWOOD",
    "SUNDANCE",
    "SOMOS",
    "CINEMAX",
    "COSMO",
    "DARK",
    "XTRM",
    "PARAMOUNT NETWORK",

    # 5. DOCUMENTALES
    "NATIONAL GEOGRAPHIC",
    "NAT GEO",
    "DISCOVERY",
    "ODISEA",
    "HISTORY CHANNEL",
    "HISTORY",
    "ANIMAL PLANET",
    "CRIME INVESTIGATION",
    "BLAZE",
    "CANAL HISTORIA",

    # 6. COCINA
    "CANAL COCINA",
    "COCINA",

    # 7. ESTILO DE VIDA Y VIAJES
    "DECASA",
    "CANAL DECASA",
    "VIAJAR",
    "TRAVEL",
    "DIVINITY",

    # 8. INFANTILES
    "CLAN",
    "DISNEY CHANNEL",
    "DISNEY JUNIOR",
    "BOING",
    "BABY TV",
    "NICK JR",
    "NICKELODEON",

    # 9. DEPORTES
    "TELEDEPORTE",
    "EUROSPORT",
    "GOL PLAY",
    "GOL TV",
    "DAZN",
    "SPORT TV",
    "REAL MADRID TV",
    "BARCA TV",
]
# ===============================================================
# ORDEN POLONIA
# ===============================================================

ORDEN_POLONIA = [
    "TVP 1",
    "TVP 2",
    "TVP 3",
    "TVP",
    "TVN 24",
    "TVN7",
    "TVN",
    "POLSAT NEWS",
    "POLSAT SPORT",
    "POLSAT",
    "TTV",
    "TV4",
    "TV6"
]


# ===============================================================
# ORDEN UK
# ===============================================================

ORDEN_UK = [
    "BBC ONE",
    "BBC TWO",
    "BBC THREE",
    "BBC FOUR",
    "BBC NEWS",
    "BBC",
    "ITV 1",
    "ITV 2",
    "ITV 3",
    "ITV 4",
    "ITV",
    "CHANNEL 4",
    "CHANNEL 5",
    "SKY NEWS",
    "SKY ONE",
    "SKY ATLANTIC",
    "SKY MAX",
    "SKY"
]


# ===============================================================
# REGEX GROUP-TITLE
# ===============================================================

RE_GROUP = re.compile(
    r'(group-title=")[^"]*(")',
    re.IGNORECASE
)


# ===============================================================
# NORMALIZAR GRUPO
# ===============================================================

def normalizar_grupo(extinf_line, nombre_grupo):

    if RE_GROUP.search(extinf_line):

        return RE_GROUP.sub(
            r'\g<1>' + nombre_grupo + r'\g<2>',
            extinf_line,
            count=1
        )

    return extinf_line.replace(
        '#EXTINF:-1',
        f'#EXTINF:-1 group-title="{nombre_grupo}"',
        1
    )


# ===============================================================
# COMPROBAR SI ES CANAL EN VIVO
# ===============================================================

def es_canal_en_vivo(url):

    url_lower = url.lower()

    if "/movie/" in url_lower:
        return False

    if "/series/" in url_lower:
        return False

    if url_lower.endswith(
        (
            '.mp4',
            '.mkv',
            '.avi',
            '.mp3',
            '.mov',
            '.flv'
        )
    ):
        return False

    return True


# ===============================================================
# OBTENER CATEGORÍA
# ===============================================================

def obtener_categoria(extinf_line):

    texto = extinf_line.upper()

    # -----------------------------------------------------------
    # MOTOR / ADULTOS
    # -----------------------------------------------------------

    if PATRON_ADULT.search(texto):
        return "Motor"

    # -----------------------------------------------------------
    # -----------------------------------------------------------
    # MOVISTAR PLUS+: SOLO CANALES CON PREFIJO ES: O ES|
    # -----------------------------------------------------------

    if "," in extinf_line:
        nombre_movistar = extinf_line.rsplit(",", 1)[1].strip()
    else:
        nombre_movistar = extinf_line.strip()

    # Solo aceptar nombres que comiencen por ES: o ES|
    if re.match(r'^ES\s*[:|]\s*', nombre_movistar, re.IGNORECASE):

        nombre_movistar = re.sub(
            r'^ES\s*[:|]\s*',
            '',
            nombre_movistar,
            flags=re.IGNORECASE
        ).strip()

        if re.search(
            r'\bMOVISTAR\b|M\+|^M\.\s*(?:LALIGA|DEPORTES|VAMOS|ESTRENOS|CINE|SERIES)\b',
            nombre_movistar,
            re.IGNORECASE
        ):
            return "Movistar Plus+"

    # F1
    # -----------------------------------------------------------

    if PATRON_F1.search(texto):
        return "F1"

    # -----------------------------------------------------------
    # FÚTBOL
    # -----------------------------------------------------------

    if PATRON_FUTBOL.search(texto):
        return "Futbol"

    # -----------------------------------------------------------
    # PAÍSES
    # -----------------------------------------------------------

    for nombre_grupo, patrones in GRUPOS_PAIS.items():

        for patron in patrones:

            if re.search(
                patron,
                texto,
                re.IGNORECASE
            ):
                return nombre_grupo

    return None


# ===============================================================
# PESO DE ORDEN
# ===============================================================

def obtener_peso_orden(extinf_line, lista_prioridades):

    # IMPORTANTE:
    # Usamos SOLO el nombre real del canal, después de la última coma.
    # Ignoramos tvg-id y tvg-name porque la fuente puede tenerlos mal.
    if "," in extinf_line:
        nombre_canal = extinf_line.rsplit(",", 1)[1].strip()
    else:
        nombre_canal = extinf_line.strip()

    nombre_upper = nombre_canal.upper()

    # Quitar prefijo ES:
    nombre_upper = re.sub(
        r'^ES\s*[:|]\s*',
        '',
        nombre_upper
    ).strip()

    for indice, patron in enumerate(lista_prioridades):

        patron_upper = patron.upper()

        # Coincidencia exacta
        if nombre_upper == patron_upper:
            return indice

        # Coincidencia permitiendo calidad al final
        # Ejemplo:
        # LA 1
        # LA 1 HD
        # LA 1 FHD
        # LA 1 UHD
        # LA 1 4K
        if re.match(
            r'^' + re.escape(patron_upper) +
            r'(?:\s+(?:FHD|UHD|HD|SD|4K|HEVC))?$',
            nombre_upper
        ):
            return indice

    # Todo lo que no esté en la lista va después
    return 999

def clave_orden_movistar(canal):
    extinf_line = canal[0]
    nombre = extinf_line.rsplit(",", 1)[-1].strip().upper()
    nombre = re.sub(r'^ES\s*[:|]\s*', '', nombre, flags=re.IGNORECASE).strip()
    nombre = ''.join(
        c for c in unicodedata.normalize("NFD", nombre)
        if unicodedata.category(c) != "Mn"
    )

    # 0-12: cine y entretenimiento, en el orden solicitado.
    tematicos = [
        r'\bM\+\s*PLUS(?!\s*2)\+?\b|\bM\.\s*PLUS\b',
        r'\bORIGINALES?\b',
        r'\bESTRENOS?(?:\s+2)?\b',
        r'\bDOCUMENTALES?\b',
        r'\bINDIE\b',
        r'\bACCION\b',
        r'\bCOMEDIA\b',
        r'\bCINE\s+ESPANOL\b',
        r'\bDRAMA\b',
        r'\bCLASICOS?\b',
        r'\bHITS?\b',
        r'\bROMANCE\b',
        r'\bOTROS?\b',
    ]

    # M+ PLUS+ 2 debe ir justo después de M+ PLUS.
    if re.search(r'\bM\+\s*PLUS\+?\s*2\b', nombre):
        return (0, 1, 0, nombre)

    for prioridad, patron in enumerate(tematicos):
        if re.search(patron, nombre):
            return (prioridad, 0, 0, nombre)

    # 20: Liga, Liga 2 y canales LALIGA TV adicionales.
    if re.search(r'\bLALIGA\b|\bLA\s+LIGA\b|\bLIGA\b', nombre):
        if re.search(r'\bHYPERMOTION\b', nombre):
            return (21, 0, 0, nombre)

        if re.search(r'\bLIGA\s+DE\s+CAMPEONES\b|\bCHAMPIONS\b|\bLCAMPEONS\b', nombre):
            m = re.search(r'(?:LIGA\s+DE\s+CAMPEONES|CHAMPIONS|LCAMPEONS)\s*(\d+)?', nombre)
            n = int(m.group(1)) if m and m.group(1) else 1
            return (40, n, 0, nombre)

        if re.search(r'\bLALIGA\s*2\b|\bLA\s+LIGA\s*2\b|\bLIGA\s*2\b', nombre):
            return (20, 1, 0, nombre)

        m = re.search(r'\bLALIGA\s*TV\s*(\d+)', nombre)
        if m:
            return (20, 10 + int(m.group(1)), 0, nombre)

        if re.search(r'\bLALIGA\s*TV\b|\bLALIGATV\b', nombre):
            return (20, 3, 0, nombre)

        return (20, 0, 0, nombre)

    # 21: Hypermotion y variantes M2/M3.
    if re.search(r'\bHYPERMOTION\b', nombre):
        m = re.search(r'\bM\s*(\d+)\b', nombre)
        n = int(m.group(1)) if m else 1
        return (21, n, 0, nombre)

    # Otros bloques de fútbol.
    if re.search(r'\bCOPA\s+DEL\s+REY\b', nombre):
        return (22, 0, 0, nombre)

    if re.search(r'\bPRIMERA\s+FEDERACION\b', nombre):
        return (23, 0, 0, nombre)

    if re.search(r'\bCOPA\s+AMERICA\b', nombre):
        m = re.search(r'\bCOPA\s+AMERICA\s*(\d+)?', nombre)
        n = int(m.group(1)) if m and m.group(1) else 1
        return (24, n, 0, nombre)

    # Golf.
    if re.search(r'\bGOLF\b', nombre):
        m = re.search(r'\bGOLF\s*(\d+)?', nombre)
        n = int(m.group(1)) if m and m.group(1) else 1
        return (25, n, 0, nombre)

    # Vamos.
    if re.search(r'\bVAMOS\b', nombre):
        m = re.search(r'\bVAMOS\s*(\d+)?', nombre)
        n = int(m.group(1)) if m and m.group(1) else 1
        return (26, n, 0, nombre)

    # Baloncesto.
    if re.search(r'\bBALONCESTO\b', nombre):
        m = re.search(r'\bBALONCESTO\s*(\d+)?', nombre)
        n = int(m.group(1)) if m and m.group(1) else 1
        return (27, n, 0, nombre)

    # Deportes.
    if re.search(r'\bDEPORTES?\b', nombre):
        m = re.search(r'\bDEPORTES?\s*(\d+)?', nombre)
        n = int(m.group(1)) if m and m.group(1) else 0
        return (28, n, 0, nombre)

    # Champions: variantes numeradas juntas y en orden natural.
    if re.search(r'\bLIGA\s+DE\s+CAMPEONES\b|\bCHAMPIONS\b|\bLCAMPEONS\b', nombre):
        m = re.search(r'(?:LIGA\s+DE\s+CAMPEONES|CHAMPIONS|LCAMPEONS)\s*(\d+)?', nombre)
        n = int(m.group(1)) if m and m.group(1) else 1
        return (40, n, 0, nombre)

    if re.search(r'\bCERO\b', nombre):
        return (13, 0, 0, nombre)

    # Otros deportes conocidos, agrupados después de los bloques principales.
    otros_deportes = [
        r'\bDAZN\b',
        r'\bMOVISTAR\s+(?:SPORT|DEPORTES|LALIGA|CHAMPIONS)\b',
        r'\bFUTBOL\b',
        r'\bFUTBOL\b',
        r'\bMOTOR\b',
        r'\bFORMULA\s*1\b|\bF1\b',
        r'\bEUROSPORT\b',
        r'\bLALIGA(?:TV)?\s*BAR\b',
        r'\bVAMOS\s*BAR\b',
    ]

    for prioridad, patron in enumerate(otros_deportes):
        if re.search(patron, nombre):
            return (50, prioridad, 0, nombre)

    # Todo lo demás se conserva al final, por orden alfabético.
    return (999, 0, 0, nombre)

def clave_duplicado(extinf_line):
    """
    Genera una clave normalizada para detectar duplicados.

    Mantiene separadas las distintas calidades:
    LA 1 FHD
    LA 1 HD
    LA 1 SD

    Pero elimina duplicados exactos como:
    LA 1 HD
    LA 1 HD
    """

    if "," in extinf_line:
        nombre = extinf_line.rsplit(",", 1)[1].strip()
    else:
        nombre = extinf_line.strip()

    # Quitar prefijos de país
    nombre = re.sub(
        r'^(ES|PL|UK|GB)\s*:\s*',
        '',
        nombre,
        flags=re.IGNORECASE
    ).strip()

    # Normalizar espacios
    nombre = re.sub(
        r'\s+',
        ' ',
        nombre
    )

    # Normalizar calidad
    nombre = re.sub(
        r'\s+(FHD|UHD|HD|SD|4K|HEVC)\s*$',
        lambda m: " " + m.group(1).upper(),
        nombre,
        flags=re.IGNORECASE
    )

    return nombre.upper()

def eliminar_duplicados(canales_por_categoria):

    eliminados_total = 0

    for categoria, lista_canales in canales_por_categoria.items():

        vistos = set()
        canales_limpios = []

        for extinf, url, fuente in lista_canales:

            clave = clave_duplicado(extinf)

            if clave in vistos:
                eliminados_total += 1
                continue

            vistos.add(clave)

            canales_limpios.append(
                (
                    extinf,
                    url,
                    fuente
                )
            )

        canales_por_categoria[categoria] = canales_limpios

    return eliminados_total

def obtener_categoria_fuente(extinf_line, indice_fuente):

    # Las fuentes 1 y 2 mantienen su clasificación habitual.
    if indice_fuente != 3:
        return obtener_categoria(extinf_line)

    # La fuente 3 solo admite los grupos seleccionados.
    m = re.search(
        r'group-title="([^"]*)"',
        extinf_line,
        re.IGNORECASE
    )

    if not m:
        return None

    grupo = m.group(1).strip().casefold()

    categorias = {
        "испания | spain": "Espana",
        "испания|spain": "Espana",
        "польша | poland": "Polonia",
        "польша|poland": "Polonia",
        "взрослые": "Motor",
        "xxx": "Motor",
    }

    return categorias.get(grupo)


# ===============================================================
# PROCESAR UNA FUENTE
# ===============================================================

def procesar_respuesta(
    r,
    canales_por_categoria,
    contador_por_fuente,
    indice_fuente
):

    pending_extinf = None

    total_extinf = 0
    total_urls = 0
    total_guardados = 0
    total_descartados = 0

    for line in r.iter_lines():

        if not line:
            continue

        # -------------------------------------------------------
        # DECODIFICAR
        # -------------------------------------------------------

        try:

            line_str = line.decode(
                "utf-8"
            ).strip()

        except UnicodeDecodeError:

            line_str = line.decode(
                "latin-1",
                errors="replace"
            ).strip()

        # -------------------------------------------------------
        # EXTINF
        # -------------------------------------------------------

        if line_str.startswith("#EXTINF"):

            pending_extinf = line_str

            total_extinf += 1

            continue

        # -------------------------------------------------------
        # EXT-VLC-OPT
        # -------------------------------------------------------

        if line_str.startswith("#EXTVLCOPT"):

            continue

        # -------------------------------------------------------
        # URL
        # -------------------------------------------------------

        if (
            not line_str.startswith("#")
            and pending_extinf
        ):

            total_urls += 1

            if es_canal_en_vivo(line_str):

                categoria = obtener_categoria_fuente(
                    pending_extinf,
                    indice_fuente
                )

                if categoria in canales_por_categoria:

                    extinf_modificado = normalizar_grupo(
                        pending_extinf,
                        categoria
                    )

                    canales_por_categoria[
                        categoria
                    ].append(
                        (
                            extinf_modificado,
                            line_str,
                            indice_fuente
                        )
                    )

                    total_guardados += 1

                else:

                    total_descartados += 1

            pending_extinf = None

    # -----------------------------------------------------------
    # ESTADÍSTICAS
    # -----------------------------------------------------------

    contador_por_fuente[indice_fuente] = {

        "extinf": total_extinf,
        "urls": total_urls,
        "guardados": total_guardados,
        "descartados": total_descartados,

    }


# ===============================================================
# FUNCIÓN PRINCIPAL
# ===============================================================

def filtrar():

    inicio = time.monotonic()
    reintentos_fuentes = []

    headers = {

        "User-Agent":
            "Mozilla/5.0 "
            "(Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 "
            "(KHTML, like Gecko) "
            "Chrome/131.0 Safari/537.36",

        "Accept": "*/*",

    }


    # ===========================================================
    # CATEGORÍAS
    # ===========================================================

    canales_por_categoria = {

        "Espana": [],
        "Movistar Plus+": [],
        "Polonia": [],
        "United Kingdom": [],
        "Latino": [],
        "Futbol": [],
        "F1": [],
        "Motor": [],

    }


    contador_por_fuente = {}
    fallos_fuentes = []


    # ===========================================================
    # INICIO
    # ===========================================================

    print()
    print("=" * 65)
    print(" IPTV - PROCESANDO MÚLTIPLES FUENTES ")
    print("=" * 65)


    # ===========================================================
    # PROCESAR FUENTES
    # ===========================================================

    for i, url_fuente in enumerate(
        FUENTES,
        start=1
    ):

        print()
        print("-" * 65)
        print(f"FUENTE {i}")
        print("-" * 65)

        exito = False

        for intento in range(1, 4):

            # Eliminar posibles datos parciales de intentos anteriores.
            for categoria in canales_por_categoria:
                canales_por_categoria[categoria] = [
                    canal for canal in canales_por_categoria[categoria]
                    if canal[2] != i
                ]

            contador_por_fuente[i] = {
                "extinf": 0,
                "urls": 0,
                "guardados": 0,
                "descartados": 0
            }

            try:
                print(
                    f"Descargando fuente {i} "
                    f"(intento {intento}/3)..."
                )

                with requests.get(
                    url_fuente,
                    stream=True,
                    timeout=(15, 180),
                    headers=headers
                ) as r:

                    r.raise_for_status()

                    print(f"HTTP: {r.status_code}")
                    print(
                        "Content-Type:",
                        r.headers.get("content-type", "desconocido")
                    )

                    procesar_respuesta(
                        r,
                        canales_por_categoria,
                        contador_por_fuente,
                        i
                    )

                datos = contador_por_fuente[i]

                print()
                print(f"EXTINF leídos:     {datos['extinf']}")
                print(f"URLs encontradas:  {datos['urls']}")
                print(f"Canales guardados: {datos['guardados']}")
                print(f"Descartados:       {datos['descartados']}")

                exito = True
                if intento > 1:
                    reintentos_fuentes.append(
                        f'Fuente {i}: intento {intento}/3'
                    )
                break

            except Exception as e:
                print()
                print(
                    f"ERROR en fuente {i}, "
                    f"intento {intento}/3:"
                )
                print(e)

                # Descartar los canales parciales del intento fallido.
                for categoria in canales_por_categoria:
                    canales_por_categoria[categoria] = [
                        canal for canal in canales_por_categoria[categoria]
                        if canal[2] != i
                    ]

                contador_por_fuente[i] = {
                    "extinf": 0,
                    "urls": 0,
                    "guardados": 0,
                    "descartados": 0
                }

                if intento < 3:
                    print("Nuevo intento en 10 segundos...")
                    time.sleep(10)

        if not exito:
            fallos_fuentes.append(i)
            print(f"ERROR: fuente {i} falló tras 3 intentos.")
    # ===========================================================
    # ELIMINAR DUPLICADOS
    # ===========================================================

    print()
    print("Eliminando duplicados...")

    duplicados_eliminados = eliminar_duplicados(
        canales_por_categoria
    )

    print(
        f"Duplicados eliminados: {duplicados_eliminados}"
    )




    # ===========================================================
    # ORDENAR ESPAÑA
    # ===========================================================

    canales_por_categoria[
        "Espana"
    ].sort(
        key=lambda canal: (
            obtener_peso_orden(
                canal[0],
                ORDEN_ESPANA
            ),
            canal[0].upper()
        )
    )


    # ===========================================================
    # ===========================================================
    # ORDENAR MOVISTAR PLUS+: TEMATICOS Y DESPUES DEPORTIVOS
    # ===========================================================

    canales_por_categoria["Movistar Plus+"].sort(
        key=clave_orden_movistar
    )


    # ORDENAR POLONIA
    # ===========================================================

    canales_por_categoria[
        "Polonia"
    ].sort(
        key=lambda canal: (
            obtener_peso_orden(
                canal[0],
                ORDEN_POLONIA
            ),
            canal[0].upper()
        )
    )


    # ===========================================================
    # ORDENAR UK
    # ===========================================================

    canales_por_categoria[
        "United Kingdom"
    ].sort(
        key=lambda canal: (
            obtener_peso_orden(
                canal[0],
                ORDEN_UK
            ),
            canal[0].upper()
        )
    )


    # ===========================================================
    if fallos_fuentes:
        print()
        print("ERROR: Han fallado estas fuentes:", fallos_fuentes)
        print("Se conserva la lista anterior.")

        duracion = int(time.monotonic() - inicio)
        mensaje = (
            "❌ IPTV - ERROR DE ACTUALIZACIÓN\n"
            f"Fuentes fallidas: {fallos_fuentes}\n"
            "Se conserva la lista anterior.\n"
            f"Duración: {duracion // 60} min {duracion % 60} s"
        )
        enviar_telegram(mensaje)
        return

    # ESCRIBIR M3U
    # ===========================================================

    total = 0

    print()
    print(
        "Escribiendo lista final..."
    )

    with open(
        ARCHIVO_SALIDA,
        "w",
        encoding="utf-8"
    ) as f:

        f.write(
            "#EXTM3U\n"
        )

        for categoria, lista_canales in (
            canales_por_categoria.items()
        ):

            for extinf, url, fuente in lista_canales:

                f.write(
                    extinf + "\n"
                )

                f.write(
                    url + "\n"
                )

                total += 1


    # ===========================================================
    # RESULTADO
    # ===========================================================

    print()
    print("=" * 65)

    print(
        "LISTA GENERADA CORRECTAMENTE"
    )

    print(
        f"Archivo: {ARCHIVO_SALIDA}"
    )

    print(
        f"Total de canales: {total}"
    )

    print("=" * 65)


    # ===========================================================
    # RESUMEN POR FUENTE
    # ===========================================================

    print()
    print("RESUMEN POR FUENTE")
    print("-" * 65)

    for i, url_fuente in enumerate(
        FUENTES,
        start=1
    ):

        datos = contador_por_fuente.get(
            i,
            {
                "extinf": 0,
                "urls": 0,
                "guardados": 0,
                "descartados": 0
            }
        )

        print()
        print(
            f"Fuente {i}:"
        )

        print(
            f"  EXTINF:       {datos['extinf']}"
        )

        print(
            f"  URLs:         {datos['urls']}"
        )

        print(
            f"  Guardados:    {datos['guardados']}"
        )

        print(
            f"  Descartados:  {datos['descartados']}"
        )


    # ===========================================================
    # RESUMEN POR CATEGORÍA
    # ===========================================================

    print()
    print("RESUMEN POR CATEGORÍA")
    print("-" * 65)

    for categoria, lista in (
        canales_por_categoria.items()
    ):

        print(
            f"{categoria:20s}: "
            f"{len(lista):6d} canales"
        )


    # ===========================================================
    # DESGLOSE POR FUENTE
    # ===========================================================

    print()
    print("DESGLOSE POR CATEGORÍA Y FUENTE")
    print("-" * 65)

    for categoria, lista in (
        canales_por_categoria.items()
    ):

        fuente_1 = sum(
            1
            for _, _, fuente in lista
            if fuente == 1
        )

        fuente_2 = sum(
            1
            for _, _, fuente in lista
            if fuente == 2
        )

        print(
            f"{categoria:20s}: "
            f"Fuente 1 = {fuente_1:5d} | "
            f"Fuente 2 = {fuente_2:5d}"
        )


    print()
    print("=" * 65)


    # ===========================================================
    # NOTIFICACIÓN TELEGRAM - ÉXITO
    # ===========================================================

    duracion = int(time.monotonic() - inicio)

    mensaje = [
        "✅ IPTV - ACTUALIZACIÓN CORRECTA",
        f"Canales totales: {total}",
        f"Duplicados eliminados: {duplicados_eliminados}",
        f"Duración: {duracion // 60} min {duracion % 60} s",
        "",
        "RESUMEN POR FUENTE"
    ]

    for i in range(1, len(FUENTES) + 1):
        datos = contador_por_fuente.get(i, {})
        mensaje.append(
            f"Fuente {i}: {datos.get('guardados', 0)} canales guardados"
        )

    if reintentos_fuentes:
        mensaje.extend([
            "",
            "⚠️ Fuentes recuperadas tras reintentos:"
        ])
        mensaje.extend(reintentos_fuentes)

    enviar_telegram("\\n".join(mensaje))


# ===============================================================
# EJECUTAR
# ===============================================================

if __name__ == "__main__":

    filtrar()
