# -*- coding: utf-8 -*-
"""
Obtención de enlaces de descarga por aplicación.

Cada función obtener_enlace_* devuelve la URL del instalador (o None) para una
aplicación concreta. obtener_enlace_descarga orquesta scraping genérico y
redirige a lógica específica (DigiKam, SourceForge /latest/download).
"""

import json
import logging
import os
import re
from urllib.parse import unquote, urljoin

import requests
from bs4 import BeautifulSoup

from . import config
from . import http_utils
from . import sourceforge
from . import utils

# -----------------------------------------------------------------------------
# DigiKam (múltiples mirrors)
# -----------------------------------------------------------------------------


def obtener_enlace_digikam(url_base):
    """Resuelve la URL de descarga de digiKam (files.kde.org o download.kde.org)."""
    try:
        headers = {"User-Agent": config.DEFAULT_HEADERS["User-Agent"]}
        pattern_exe = r"digiKam-(\d+\.\d+\.\d+)-Qt6-Win64\.exe$"
        files_url = "https://files.kde.org/digikam/"
        try:
            resp = requests.get(files_url, headers=headers, timeout=15)
            soup = BeautifulSoup(resp.content, "html.parser")
            mejor_version = None
            mejor_enlace = None
            for a in soup.find_all("a", href=True):
                href = a["href"]
                match = re.search(pattern_exe, href)
                if match:
                    version = match.group(1)
                    if mejor_version is None or utils.comparar_versiones(version, mejor_version) > 0:
                        mejor_version = version
                        mejor_enlace = urljoin(files_url, href)
            if mejor_enlace:
                logging.info(f"DigiKam encontrado en files.kde.org: {mejor_enlace}")
                return mejor_enlace
        except Exception as e:
            logging.warning(f"Error accediendo a files.kde.org: {e}")
        stable_url = "http://download.kde.org/stable/digikam/"
        try:
            resp = requests.get(stable_url, headers=headers, timeout=15)
            soup = BeautifulSoup(resp.content, "html.parser")
            versiones = []
            for a in soup.find_all("a", href=True):
                match = re.search(r"^(\d+\.\d+\.\d+)/$", a["href"])
                if match:
                    versiones.append(match.group(1))
            if versiones:
                version_mayor = max(versiones, key=lambda v: [int(x) for x in v.split(".")])
                version_url = urljoin(stable_url, f"{version_mayor}/")
                resp2 = requests.get(version_url, headers=headers, timeout=15)
                soup2 = BeautifulSoup(resp2.content, "html.parser")
                for a in soup2.find_all("a", href=True):
                    if re.search(r"digiKam-.*-Qt6-Win64\.exe$", a["href"]):
                        full_url = urljoin(version_url, a["href"])
                        logging.info(f"DigiKam encontrado en stable: {full_url}")
                        return full_url
        except Exception as e:
            logging.warning(f"Error accediendo a download.kde.org: {e}")
        return None
    except Exception as e:
        utils.registrar_error(f"Error en obtener_enlace_digikam: {e}")
        return None


# -----------------------------------------------------------------------------
# GitHub / GitLab
# -----------------------------------------------------------------------------


def obtener_enlace_obs_github():
    """Obtiene el instalador de OBS Studio desde GitHub Releases (Windows-x64-Installer.exe)."""
    try:
        api_url = "https://api.github.com/repos/obsproject/obs-studio/releases/latest"
        headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/vnd.github.v3+json"}
        r = requests.get(api_url, headers=headers, timeout=15)
        r.raise_for_status()
        data = r.json()
        for asset in data.get("assets", []):
            name = asset.get("name", "")
            if "Windows-x64-Installer.exe" in name:
                url = asset.get("browser_download_url", "")
                if url:
                    logging.info(f"OBS Studio encontrado en GitHub: {url}")
                    return url
        logging.warning("No se encontró el instalador Windows x64 de OBS Studio en GitHub Releases")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo OBS Studio desde GitHub: {e}")
        return None


def obtener_enlace_github_release(repo, asset_pattern, nombre):
    """Obtiene un asset de la última release de un repo GitHub (regex sobre el nombre)."""
    try:
        api_url = f"https://api.github.com/repos/{repo}/releases/latest"
        headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/vnd.github.v3+json"}
        r = requests.get(api_url, headers=headers, timeout=15)
        r.raise_for_status()
        data = r.json()
        for asset in data.get("assets", []):
            name = asset.get("name", "")
            if re.search(asset_pattern, name):
                url = asset.get("browser_download_url", "")
                if url:
                    logging.info(f"{nombre} encontrado en GitHub ({repo}): {url}")
                    return url
        logging.warning(f"No se encontró asset '{asset_pattern}' para {nombre} en GitHub {repo}")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo {nombre} desde GitHub ({repo}): {e}")
        return None


# -----------------------------------------------------------------------------
# Aplicaciones con scraping específico (7-Zip, Blender, LibreOffice, R, etc.)
# -----------------------------------------------------------------------------


def obtener_enlace_7zip():
    """Enlace de descarga de 7-Zip (página oficial, .exe x64)."""
    try:
        url = "https://www.7-zip.org/download.html"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        soup = BeautifulSoup(r.content, "html.parser")
        for a in soup.find_all("a", href=True):
            href = a["href"]
            if re.search(r"7z\d+-x64\.exe$", href):
                full_url = urljoin(url, href)
                logging.info(f"7-Zip encontrado: {full_url}")
                return full_url
        logging.warning("No se encontró el enlace de descarga de 7-Zip")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de 7-Zip: {e}")
        return None


def obtener_enlace_blender():
    """Enlace del MSI de Blender para Windows x64 (página principal + mirror)."""
    try:
        url = "https://www.blender.org/download/"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        soup = BeautifulSoup(r.content, "html.parser")
        landing_url = None
        for a in soup.find_all("a", href=True):
            href = a["href"]
            if re.search(r"blender-[\d.]+-windows-x64\.msi", href):
                landing_url = urljoin(url, href)
                break
        if not landing_url:
            logging.warning("No se encontró el enlace de descarga de Blender")
            return None
        r2 = requests.get(landing_url, headers=headers, timeout=15)
        soup2 = BeautifulSoup(r2.content, "html.parser")
        for a in soup2.find_all("a", href=True):
            href = a["href"]
            if re.search(r"blender-[\d.]+-windows-x64\.msi$", href):
                logging.info(f"Blender encontrado (mirror): {href}")
                return href
        match = re.search(r"blender-([\d.]+)-windows-x64\.msi", landing_url)
        if match:
            version = match.group(1)
            major_minor = ".".join(version.split(".")[:2])
            direct_url = f"https://download.blender.org/release/Blender{major_minor}/blender-{version}-windows-x64.msi"
            logging.info(f"Blender fallback URL: {direct_url}")
            return direct_url
        logging.warning("No se encontró el enlace real de descarga de Blender")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de Blender: {e}")
        return None


def obtener_enlace_libreoffice():
    """MSI de LibreOffice Windows x86_64 (listado estable Document Foundation)."""
    try:
        url = "https://download.documentfoundation.org/libreoffice/stable/?C=M;O=D"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        soup = BeautifulSoup(r.content, "html.parser")
        versiones = []
        for a in soup.find_all("a", href=True):
            match = re.search(r"^(\d+\.\d+\.\d+)/$", a["href"])
            if match:
                versiones.append(match.group(1))
        if not versiones:
            logging.warning("No se encontraron versiones de LibreOffice")
            return None
        version_mayor = max(versiones, key=lambda v: [int(x) for x in v.split(".")])
        msi_url = f"https://download.documentfoundation.org/libreoffice/stable/{version_mayor}/win/x86_64/LibreOffice_{version_mayor}_Win_x86-64.msi"
        check = requests.head(msi_url, headers=headers, timeout=15, allow_redirects=True)
        if check.status_code == 200:
            logging.info(f"LibreOffice encontrado: {msi_url}")
            return msi_url
        logging.warning(f"LibreOffice MSI no encontrado en: {msi_url}")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de LibreOffice: {e}")
        return None


def obtener_enlace_r():
    """Enlace de descarga de R para Windows desde CRAN."""
    try:
        url = "https://cran.r-project.org/bin/windows/base/"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        soup = BeautifulSoup(r.content, "html.parser")
        for a in soup.find_all("a", href=True):
            href = a["href"]
            if re.search(r"R-[\d.]+-win\.exe$", href):
                full_url = urljoin(url, href)
                logging.info(f"R encontrado: {full_url}")
                return full_url
        logging.warning("No se encontró el enlace de descarga de R")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de R: {e}")
        return None


def obtener_enlace_rstudio():
    """Enlace de RStudio Desktop para Windows (posit.co)."""
    try:
        url = "https://posit.co/download/rstudio-desktop/"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        soup = BeautifulSoup(r.content, "html.parser")
        for a in soup.find_all("a", href=True):
            href = a["href"]
            if re.search(r"RStudio-.*\.exe$", href) and "windows" in href.lower():
                logging.info(f"RStudio encontrado: {href}")
                return href
        logging.warning("No se encontró el enlace de descarga de RStudio")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de RStudio: {e}")
        return None


def obtener_enlace_octave():
    """GNU Octave para Windows x64 (ftpmirror.gnu.org, varios patrones de nombre)."""
    try:
        url = "https://ftpmirror.gnu.org/gnu/octave/windows/"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        soup = BeautifulSoup(r.text, "html.parser")
        patterns = [
            r"^octave-([\d.]+)-w64-installer\.exe$",
            r"^octave-([\d.]+)-w64-64-installer\.exe$",
            r"^octave-([\d.]+)-.*w64.*\.exe$",
        ]
        candidatos = []
        for a in soup.find_all("a", href=True):
            fname = os.path.basename(a["href"].rstrip("/"))
            for pat in patterns:
                m = re.match(pat, fname, re.IGNORECASE)
                if m:
                    candidatos.append((m.group(1), fname))
                    break
        if not candidatos:
            logging.warning("No se encontraron instaladores de Octave")
            return None
        mejor_version, mejor_fname = max(
            candidatos,
            key=lambda t: [int(x) for x in utils.limpiar_version(t[0]).split(".") if x.isdigit()],
        )
        full_url = urljoin(url, mejor_fname)
        logging.info(f"Octave encontrado: {full_url}")
        return full_url
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de Octave: {e}")
        return None


def obtener_enlace_pdfcreator():
    """PDFCreator: API de updates pdfforge.org (versión estable + sourceUrl)."""
    try:
        api_url = "https://update.pdfforge.org/pdfcreator/updates"
        headers = {
            "User-Agent": config.DEFAULT_HEADERS["User-Agent"],
            "Accept": "application/json",
        }
        logging.info("PDFCreator: Consultando API de updates...")
        r = requests.get(api_url, headers=headers, timeout=15)
        r.raise_for_status()
        data = r.json()
        if not isinstance(data, list) or len(data) == 0:
            logging.warning("PDFCreator: La API no devolvió datos válidos")
            return None
        versiones_estables = [v for v in data if v.get("isStable", False)]
        if not versiones_estables:
            logging.warning("PDFCreator: No se encontraron versiones estables en la API")
            return None
        versiones_estables.sort(
            key=lambda v: [
                int(x) for x in utils.limpiar_version(v.get("version", "0")).split(".") if x.isdigit()
            ],
            reverse=True,
        )
        mejor_version = versiones_estables[0]
        version_num = mejor_version.get("version", "")
        downloads = mejor_version.get("downloads", [])
        if not downloads:
            logging.warning(f"PDFCreator: La versión {version_num} no tiene descargas disponibles")
            return None
        for download in downloads:
            filename = download.get("filename", "")
            source_url = download.get("sourceUrl", "")
            if filename.lower().endswith(".exe") and "setup" in filename.lower():
                logging.info(f"PDFCreator encontrado en API: {source_url} (v{version_num})")
                return source_url
        for download in downloads:
            filename = download.get("filename", "")
            source_url = download.get("sourceUrl", "")
            if filename.lower().endswith(".exe"):
                logging.info(f"PDFCreator encontrado en API: {source_url} (v{version_num})")
                return source_url
        if downloads:
            source_url = downloads[0].get("sourceUrl", "")
            if source_url:
                logging.info(f"PDFCreator encontrado en API (fallback): {source_url} (v{version_num})")
                return source_url
        logging.warning(f"PDFCreator: No se encontró URL de descarga válida para la versión {version_num}")
        return None
    except requests.RequestException as e:
        utils.registrar_error(f"Error obteniendo PDFCreator desde la API: {e}")
        return None
    except (json.JSONDecodeError, KeyError, ValueError) as e:
        utils.registrar_error(f"Error parseando respuesta de la API de PDFCreator: {e}")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de PDFCreator: {e}")
        return None


def obtener_enlace_qgis():
    """QGIS para Windows: MSI estable (excluye 3.99.x dev)."""
    try:
        url = "https://qgis.org/downloads/"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        all_msi = re.findall(r"(QGIS-OSGeo4W-([\d.]+)-(\d+)\.msi)", r.text)
        stable_msi = [m for m in all_msi if not m[1].startswith("3.99")]
        if not stable_msi:
            url2 = "https://qgis.org/downloads/"
            r2 = requests.get(url2, headers=headers, timeout=15)
            soup = BeautifulSoup(r2.content, "html.parser")
            for a in soup.find_all("a", href=True):
                href = a["href"]
                if re.search(r"\.msi$", href) and "QGIS" in href:
                    full_url = urljoin(url2, href)
                    logging.info(f"QGIS encontrado: {full_url}")
                    return full_url
            logging.warning("No se encontró el enlace de descarga de QGIS")
            return None
        mejor_version = max(
            set(m[1] for m in stable_msi),
            key=lambda v: [int(x) for x in v.split(".")],
        )
        msi_match = re.search(rf"QGIS-OSGeo4W-{re.escape(mejor_version)}-\d+\.msi", r.text)
        if msi_match:
            full_url = urljoin(url, msi_match.group(0))
            logging.info(f"QGIS encontrado: {full_url}")
            return full_url
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de QGIS: {e}")
        return None


def obtener_enlace_virtualbox():
    """VirtualBox para Windows (wiki Downloads)."""
    try:
        url = "https://www.virtualbox.org/wiki/Downloads"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        soup = BeautifulSoup(r.content, "html.parser")
        for a in soup.find_all("a", href=True):
            href = a["href"]
            if re.search(r"VirtualBox-.*-Win\.exe$", href):
                logging.info(f"VirtualBox encontrado: {href}")
                return href
        logging.warning("No se encontró el enlace de descarga de VirtualBox")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de VirtualBox: {e}")
        return None


def obtener_enlace_linphone():
    """Linphone Windows x64 (listado de releases o redirect latest_app_win64)."""
    try:
        url = "https://www.linphone.org/releases/windows/app/"
        headers = config.DEFAULT_HEADERS.copy()
        r = requests.get(url, headers=headers, timeout=15)
        pattern = r"(Linphone-([\d.]+)-win64\.exe)(?!\.sha)"
        matches = re.findall(pattern, r.text)
        if not matches:
            redir_url = "https://www.linphone.org/releases/windows/latest_app_win64"
            r2 = requests.head(redir_url, headers=headers, timeout=15, allow_redirects=True)
            if r2.url and r2.url != redir_url:
                logging.info(f"Linphone encontrado (redirect): {r2.url}")
                return r2.url
            return None
        mejor_version = None
        mejor_filename = None
        for filename, version in matches:
            if mejor_version is None or utils.comparar_versiones(version, mejor_version) > 0:
                mejor_version = version
                mejor_filename = filename
        if mejor_filename:
            full_url = url + mejor_filename
            logging.info(f"Linphone encontrado: {full_url} (version {mejor_version})")
            return full_url
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de Linphone: {e}")
        return None


def obtener_enlace_notepadpp():
    """Notepad++ instalador x64 desde GitHub Releases."""
    try:
        api_url = "https://api.github.com/repos/notepad-plus-plus/notepad-plus-plus/releases/latest"
        headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/vnd.github.v3+json"}
        r = requests.get(api_url, headers=headers, timeout=15)
        r.raise_for_status()
        data = r.json()
        for asset in data.get("assets", []):
            name = asset.get("name", "")
            if re.search(r"npp\.[\d.]+\.Installer\.x64\.exe$", name):
                url = asset.get("browser_download_url", "")
                if url:
                    logging.info(f"Notepad++ encontrado en GitHub: {url}")
                    return url
        logging.warning("No se encontró el instalador x64 de Notepad++ en GitHub Releases")
        return None
    except Exception as e:
        utils.registrar_error(f"Error obteniendo Notepad++ desde GitHub: {e}")
        return None


def _extraer_version_inkscape(texto):
    """
    Extrae versión X.Y o X.Y.Z de un texto (nombre de asset, release name, tag).
    Prefiere X.Y.Z si hay varias. Retorna None si no hay match.
    """
    if not texto:
        return None
    # Patrón que captura X.Y.Z o X.Y (evitar solo X)
    matches = re.findall(r"\b(\d+)\.(\d+)(?:\.(\d+))?\b", texto)
    if not matches:
        # Tag estilo INKSCAPE_1_4_2
        m = re.search(r"(\d+)[_.](\d+)(?:[_.](\d+))?", texto)
        if m:
            partes = [m.group(1), m.group(2)]
            if m.group(3):
                partes.append(m.group(3))
            return ".".join(partes)
        return None
    # Preferir la que tenga 3 componentes (patch)
    con_patch = [m for m in matches if len(m) == 3 and m[2]]
    if con_patch:
        return ".".join(con_patch[0])
    return ".".join(matches[0][:2])


def obtener_enlace_inkscape():
    """
    Inkscape Windows x64: obtiene la versión desde la API de GitLab y devuelve
    la URL de descarga directa del instalador .exe para que wget la resuelva.

    inkscape.org devuelve 403 a peticiones Python, por eso se evita scrapear
    esa web. La URL construida es resuelta correctamente por wget.
    """
    try:
        headers = {"User-Agent": config.DEFAULT_HEADERS["User-Agent"]}
        version = None

        # 1) Obtener versión desde GitLab API (puede devolver solo major.minor)
        try:
            api_url = "https://gitlab.com/api/v4/projects/inkscape%2Finkscape/releases"
            r = requests.get(api_url, headers=headers, timeout=15)
            r.raise_for_status()
            releases = r.json()
            if releases:
                tag = releases[0].get("tag_name", "")
                # Tag estilo INKSCAPE_1_4_2 o INKSCAPE_1_4
                m = re.search(r"INKSCAPE_(\d+)_(\d+)(?:_(\d+))?", tag)
                if m:
                    partes = [m.group(1), m.group(2)]
                    if m.group(3):
                        partes.append(m.group(3))
                    version = ".".join(partes)
                    logging.info(f"Inkscape: versión obtenida desde GitLab: {version}")
        except Exception as e:
            logging.warning(f"Inkscape: error obteniendo versión desde GitLab: {e}")

        if not version:
            utils.registrar_error("Inkscape: no se pudo obtener la versión desde GitLab")
            return None

        # 2) Construir URL de descarga directa (wget seguirá la redirección)
        # inkscape.org/release/inkscape-X.Y.Z/windows/64-bit/dl/exe/ redirige al .exe real
        download_url = f"https://inkscape.org/release/inkscape-{version}/windows/64-bit/dl/exe/"
        logging.info(f"Inkscape {version}: enlace construido: {download_url}")
        return download_url

    except Exception as e:
        utils.registrar_error(f"Error obteniendo enlace de Inkscape: {e}")
        return None


# -----------------------------------------------------------------------------
# Genérico: scraping + SourceForge /latest/download
# -----------------------------------------------------------------------------


def _normalizar_href(href, url_original, url_response):
    """Convierte href relativo o incompleto en URL absoluta."""
    if href.startswith("//"):
        return "https:" + href
    if href.startswith("/"):
        base_url = url_response.split("//")[0] + "//" + url_response.split("//")[1].split("/")[0]
        return urljoin(base_url, href)
    return urljoin(url_original + "/", href)


def _obtener_enlace_sourceforge_latest(url, extension, pattern, headers):
    """Maneja URLs SourceForge que terminan en /latest/download (scraping files/)."""
    m = re.match(r"https://sourceforge.net/projects/([^/]+)/files/latest/download", url)
    if not m:
        return None
    proyecto = m.group(1)
    files_url = f"https://sourceforge.net/projects/{proyecto}/files/"
    candidatos = sourceforge.buscar_mejor_enlace_sourceforge(
        files_url, extension, pattern, headers
    )
    mejor_version = None
    mejor_href = None
    for version, href in candidatos:
        if mejor_version is None or utils.comparar_versiones(version, mejor_version) > 0:
            mejor_version = version
            mejor_href = href
    if mejor_href:
        return urljoin(files_url, mejor_href)
    logging.warning(f"No se encontró enlace de descarga válido en SourceForge para {url}")
    return None


def obtener_enlace_descarga(url, filtro, pattern=None, extension=None, nombre=None):
    """
    Orquesta la obtención del enlace: DigiKam, SourceForge /latest/download o
    scraping genérico de la página con filtros BeautifulSoup.
    """
    headers = config.DEFAULT_HEADERS.copy()
    if nombre and nombre.lower() == "digikam":
        return obtener_enlace_digikam(url)
    if not filtro and "sourceforge.net" not in url:
        return url
    try:
        if "sourceforge.net" in url and url.endswith("/latest/download"):
            return _obtener_enlace_sourceforge_latest(url, extension, pattern, headers)
        response = requests.get(url, headers=headers, timeout=15)
        logging.info(
            f"Scraping {nombre}: HTTP {response.status_code}, Content-Length: {len(response.content)} bytes"
        )
        soup = BeautifulSoup(response.content, "html.parser")
        link = soup.find("a", **filtro) if filtro else None
        if link:
            return _normalizar_href(link["href"], url, response.url)
        if "sourceforge.net" in url:
            for a in soup.find_all("a", href=True):
                if re.search(r"/download$", a["href"]) and re.search(r"\d", a["href"]):
                    return urljoin(url, a["href"])
        all_links = soup.find_all("a", href=True)
        logging.warning(
            f"No se encontró enlace con filtro para {nombre}. Total enlaces: {len(all_links)}. "
            f"Primeros hrefs: {[a['href'][:80] for a in all_links[:5]]}"
        )
        return None
    except (requests.RequestException, ValueError, KeyError) as e:
        utils.registrar_error(f"Error al obtener el enlace de descarga para {url}: {e}")
        return None
