import hashlib
import json
import os
import re
from collections import deque
from urllib.parse import urldefrag, urljoin, urlparse

import requests
from bs4 import BeautifulSoup

from base_de_donnees_vectorielle.modele import Document


def telecharger_page(url: str) -> str:
    """
    Télécharge le contenu HTML d'une page.

    :param url: URL de la page à télécharger.
    :return: Code HTML brut de la page.
    :raises requests.HTTPError: En cas d'erreur HTTP.
    """
    reponse = requests.get(url,timeout=10)
    reponse.raise_for_status()
    return reponse.text

def extraire_texte_depuis_html(html: str) -> str:
    """
    Extrait le texte utile depuis un document HTML.

    Les balises script, style, header, footer, nav et aside
    sont supprimées avant extraction.

    :param html: Code HTML brut.
    :return: Texte extrait.
    """
    soupe = BeautifulSoup(html, "html.parser")
    for balise in soupe(["script", "style", "header", "footer", "nav", "aside"]):
        balise.decompose()
    texte = soupe.get_text(separator=" ", strip=True)
    return texte

def nettoyer_texte(texte: str) -> str:
    """
    Nettoie un texte en supprimant les espaces superflus.

    :param texte: Texte à nettoyer.
    :return: Texte normalisé.
    """
    texte = texte.replace("\xa0", " ")
    texte = re.sub(r"\s+", " ", texte)
    return texte.strip()

def creer_document(url: str) -> Document:
    """
    Crée un document à partir d'une URL.

    :param url: URL du document source.
    :return: Document contenant l'URL et le texte nettoyé.
    """
    html = telecharger_page(url)
    texte = extraire_texte_depuis_html(html)
    texte = nettoyer_texte(texte)

    return Document(url=url, texte=texte)

def nom_fichier_pour_url(url: str) -> str:
    """
    Construit un nom de fichier stable à partir d'une URL.

    :param url: URL source.
    :return: Nom de fichier JSON.
    """
    empreinte = hashlib.sha1(url.encode("utf-8")).hexdigest()
    return f"{empreinte}.json"

def sauvegarder_document(document: Document, repertoire: str = "corpus") -> str:
    """
    Sauvegarde un document dans un fichier JSON.

    :param document: Document à sauvegarder.
    :param repertoire: Répertoire de destination.
    :return: Chemin du fichier créé.
    """
    os.makedirs(repertoire, exist_ok=True)

    nom_fichier = nom_fichier_pour_url(document.url)
    chemin = os.path.join(repertoire, nom_fichier)

    contenu = {
        "url": document.url,
        "texte": document.texte,
    }
    with open(chemin, "w", encoding="utf-8") as fichier:
        json.dump(contenu,
                  fichier,
                  ensure_ascii=False,
                  indent=2
                  )
    return chemin

def normaliser_url(url: str) -> str:
    """
    Normalise une URL en supprimant les fragments.

    :param url: URL à normaliser.
    :return: URL normalisée.
    """
    url_sans_fragment, _ = urldefrag(url)
    return url_sans_fragment

def est_url_http(url: str) -> bool:
    """
    Indique si une URL utilise le protocole HTTP ou HTTPS.

    :param url: URL à tester.
    :return: Vrai si l'URL est en HTTP ou HTTPS.
    """
    schema = urlparse(url).scheme
    return schema in ("http", "https")

def est_lien_interne(url_reference: str, url_cible: str) -> bool:
    """
    Indique si une URL cible appartient au même site
    que l'URL de référence.

    :param url_reference: URL de départ du parcours.
    :param url_cible: URL candidate.
    :return: Vrai si le lien est interne.
    """
    domaine_reference = urlparse(url_reference).netloc
    domaine_cible = urlparse(url_cible).netloc
    return domaine_reference == domaine_cible

def extraire_liens(html: str, url_page: str) -> list[str]:
    """
    Extrait les liens présents dans une page HTML.

    Les liens relatifs sont transformés en liens absolus.
    Les fragments sont supprimés.

    :param html: Code HTML de la page.
    :param url_page: URL de la page d'origine.
    :return: Liste de liens absolus trouvés dans la page.
    """
    soupe = BeautifulSoup(html, "html.parser")
    liens: list[str] = []
    for balise_lien in soupe.find_all("a", href=True):
        href = balise_lien["href"].strip()
        if href:
            url_absolue = urljoin(url_page, href)
            url_absolue = normaliser_url(url_absolue)
            if est_url_http(url_absolue):
                liens.append(url_absolue)
    return liens


def parcourir_site(url_depart: str, nombre_pages: int) -> list[Document]:
    """
    Parcourt un site Web à partir d'une URL de départ
    et retourne les documents récupérés.

    Le parcours :
    - reste sur le même site ;
    - évite de visiter plusieurs fois la même URL ;
    - s'arrête dès que le nombre maximal de pages est atteint ;
    - ignore silencieusement les pages qui provoquent une erreur.

    :param url_depart: URL de départ du parcours.
    :param nombre_pages: Nombre maximal de pages à explorer.
    :return: Liste des documents récupérés.
    """
    if nombre_pages <= 0:
        return []
    url_depart = normaliser_url(url_depart)
    a_visiter: deque[str] = deque([url_depart])
    visitees: set[str] = set()
    documents: list[Document] = []
    while a_visiter and len(documents) < nombre_pages:
        url_courante = a_visiter.popleft()
        if url_courante in visitees:
            continue
        visitees.add(url_courante)
        try:
            html = telecharger_page(url_courante)
        except requests.RequestException:
            continue
        texte = extraire_texte_depuis_html(html)
        texte = nettoyer_texte(texte)
        if texte:
            documents.append(
                Document(
                    url=url_courante,
                    texte=texte,
                )
            )
        liens = extraire_liens(html=html,
                               url_page=url_courante,
                               )
        for lien in liens:
            if est_lien_interne(url_reference=url_depart,
                                url_cible=lien,
                                ):
                if lien not in visitees and lien not in a_visiter:
                    a_visiter.append(lien)

    return documents
