#!/usr/bin/env python3

#########################################
#
# MyCIDR, un quizz d'entrainement au CIDR
#
# Par Thibaut Maquet
# www.mail6m.com
# 
# Version 1.0, Fev 2026
#
#########################################

import random
import ipaddress


#########################################
# Adresse IP au hasard
#########################################

def generer_ip_aleatoire():
    r = random.random()

    if r < 0.50:
        return f"192.168.{random.randint(0, 255)}.{random.randint(0, 255)}"
    elif r < 0.70:
        return f"10.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}"
    elif r < 0.80:
        return f"172.{random.randint(16, 31)}.{random.randint(0, 255)}.{random.randint(0, 255)}"
    else:
        while True:
            oct1 = random.randint(1, 223)  
            if oct1 != 127:                
                break
        oct2 = random.randint(0, 255)
        oct3 = random.randint(0, 255)
        oct4 = random.randint(0, 255)
        return f"{oct1}.{oct2}.{oct3}.{oct4}"

#########################################
# PREFIXE
#########################################

#
# Prefixe au hasard
#
def generer_prefixe_aleatoire():
    r = random.random()
    if r < 0.05:
        return random.randint(1, 8)
    elif r < 0.50:
        return random.randint(9, 23)
    else:
        return random.randint(24, 31)  # /32 exclu

#
# Genere trois autres prefixes
#
def generer_trois_autres_prefixes(n):
    if not (1 <= n <= 31):
        raise ValueError("Le nombre initial doit être entre 1 et 31.")
    candidats = set()
    operations = [lambda x: x + 1, lambda x: x - 1, lambda x: x + 2, lambda x: x - 2,
                  lambda x: x + 3, lambda x: x - 3, lambda x: x + random.randint(4, 10),
                  lambda x: x - random.randint(4, 10)]
    while len(candidats) < 3:
        op = random.choice(operations)
        nouveau = op(n)
        if 1 <= nouveau <= 31 and nouveau != n:
            candidats.add(nouveau)
        if len(candidats) + len(set(range(1, 32)) - {n}) == 0:
            break
    while len(candidats) < 3:
        autre = random.randint(1, 31)
        if autre != n:
            candidats.add(autre)
    return list(candidats)

#
# Question sur le prefixe
#
def generer_question_netmask_prefixe():
    bonne_reponse = generer_prefixe_aleatoire()
    bon_mask = prefixe_vers_netmask(bonne_reponse)
    mauvaises_reponses = generer_trois_autres_prefixes(bonne_reponse)
    return {
        "question": f"Quel est le prefixe correspondant au netmask {bon_mask} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# NETMASK
#########################################

#
# Netmask au hasard
#
def generer_netmask_aleatoire():
    prefix = generer_prefixe_aleatoire()
    return prefixe_vers_netmask(prefix)	

#
# Prefixe vers masque
#
def prefixe_vers_netmask(prefix):
    if not (0 <= prefix <= 32):
        raise ValueError("Préfixe doit être entre 0 et 32")
    mask = (0xffffffff << (32 - prefix)) & 0xffffffff
    return ".".join(str((mask >> i) & 0xff) for i in (24, 16, 8, 0))

#
# Question sur le netmask
#
def generer_question_prefixe_netmask():
    bon_prefixe = generer_prefixe_aleatoire()
    bonne_reponse = prefixe_vers_netmask(bon_prefixe)
    prefixes_faux = generer_trois_autres_prefixes(bon_prefixe)
    mauvaises_reponses = [prefixe_vers_netmask(p) for p in prefixes_faux]
    return {
        "question": f"Quel est le netmask correspondant au préfixe /{bon_prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# NOMBRE DE MACHINES
#########################################

#
# Nombre de machines utilisables
#
def usable_hosts(prefix: int) -> int:
    if not (0 <= prefix <= 32):
        raise ValueError("Le préfixe doit être entre 0 et 32 inclus.")
    if prefix == 32:
        return 0 
    elif prefix == 31:
        return 2
    else:
        return (1 << (32 - prefix)) - 2  # 2^(32−prefix) − 2

#
# Nombres incorrectes
#
def wrong_usable_hosts_answers(prefix: int) -> list:
    if not (0 <= prefix <= 32):
        raise ValueError("Le préfixe doit être entre 0 et 32.")
    if prefix == 32:
        correct = 0
    elif prefix == 31:
        correct = 2
    else:
        correct = (1 << (32 - prefix)) - 2
    total_ips = 1 << (32 - prefix)  # 2^(32−prefix)
    candidates = set()
    candidates.add(total_ips)
    candidates.add(total_ips - 1)
    if prefix <= 30:
        candidates.add((1 << prefix) - 2)
    else:
        candidates.add(1)
    fallback = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
    for x in fallback:
        if len(candidates) >= 10:
            break
        candidates.add(x)
    wrongs = [x for x in candidates if x != correct and isinstance(x, int) and x >= 0]
    wrongs.sort()
    return wrongs[:3]
#
# Question sur le nombre de machines
#
def generer_question_nombre_machines():
    prefixe = generer_prefixe_aleatoire()
    bonne_reponse = usable_hosts(prefixe) 
    mauvaises_reponses = wrong_usable_hosts_answers(prefixe) 
    return {
        "question": f"Quel est le nombre de machines utilisables avec le préfixe /{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }


#########################################
# ADRESSE RESEAU
#########################################

#
# Calcul de l'adresse réseau 
#
def calculer_adresse_reseau(ip, prefix):
    if not (0 <= prefix <= 32):
        raise ValueError("Le préfixe doit être entre 0 et 32")
    octets = list(map(int, ip.split(".")))
    if len(octets) != 4 or any(o < 0 or o > 255 for o in octets):
        raise ValueError("Adresse IP invalide")
    ip_int = (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3]
    mask = (0xffffffff << (32 - prefix)) & 0xffffffff
    net_int = ip_int & mask
    reseau = ".".join(str((net_int >> i) & 0xff) for i in (24, 16, 8, 0))
    return reseau

#
# Mauvaises adresses IP
#
def wrong_network_address_answers(ip_str, prefix):
    try:
        host_ip = ipaddress.IPv4Address(ip_str)
        network = ipaddress.IPv4Network((host_ip, prefix), strict=False)
    except Exception as e:
        raise ValueError(f"Adresse ou préfixe invalide : {e}")
    correct = str(network.network_address)
    candidates = set()
    candidates.add(ip_str)
    if network.num_addresses > 1:
        candidates.add(str(network.network_address + 1))
    candidates.add(str(network.broadcast_address))
    if network.num_addresses > 2:
        candidates.add(str(network.broadcast_address - 1))
    for delta in (-1, 1):
        p2 = prefix + delta
        if 0 <= p2 <= 32:
            try:
                fake_net = ipaddress.IPv4Network((host_ip, p2), strict=False)
                candidates.add(str(fake_net.network_address))
            except:
                pass
    base = list(network.network_address.packed)
    if len(base) == 4:
        last = base[-1]
        for offset in [1, 16, 32, 64]:
            new_last = (last + offset) % 256
            fake_ip = ".".join(str(b) for b in base[:-1] + [new_last])
            candidates.add(fake_ip)
    wrongs = [addr for addr in candidates if addr != correct]
    seen = set()
    result = []
    for addr in wrongs:
        if addr not in seen:
            seen.add(addr)
            result.append(addr)
        if len(result) == 3:
            break
    fallback = ["0.0.0.0", "255.255.255.255", "192.0.2.0", "127.0.0.1"]
    for addr in fallback:
        if len(result) >= 3:
            break
        if addr != correct and addr not in seen:
            result.append(addr)
    return result[:3]

#
# Question sur l'adresse réseau
#
def generer_question_adresse_reseau():
    adresse_ip = generer_ip_aleatoire()
    prefixe = generer_prefixe_aleatoire()
    bonne_reponse = calculer_adresse_reseau(adresse_ip, prefixe)
    mauvaises_reponses = wrong_network_address_answers(adresse_ip, prefixe)
    return {
        "question": f"Quelle est l'adresse réseau de {adresse_ip}/{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }


#########################################
# BROADCAST
#########################################

#
# Calcul du broadcast
#
def calculer_broadcast(ip, prefix):
    if not (0 <= prefix <= 32):
        raise ValueError("Le préfixe doit être entre 0 et 32")
    octets = list(map(int, ip.split(".")))
    if len(octets) != 4 or any(o < 0 or o > 255 for o in octets):
        raise ValueError("Adresse IP invalide")
    ip_int = (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3]
    mask = (0xffffffff << (32 - prefix)) & 0xffffffff
    wildcard = (~mask) & 0xffffffff
    broadcast_int = (ip_int & mask) | wildcard
    return ".".join(str((broadcast_int >> i) & 0xff) for i in (24, 16, 8, 0))

#
# Mauvais broadcast
#
def wrong_broadcast_address_answers(ip_str, prefix):
    try:
        host_ip = ipaddress.IPv4Address(ip_str)
        network = ipaddress.IPv4Network((host_ip, prefix), strict=False)
    except Exception as e:
        raise ValueError(f"Adresse ou préfixe invalide : {e}")
    correct = str(network.broadcast_address)
    candidates = set()
    candidates.add(ip_str)
    candidates.add(str(network.network_address))
    if network.num_addresses > 2:
        candidates.add(str(network.broadcast_address - 1))
    elif network.num_addresses == 2:  # /31
        candidates.add(str(network.network_address))  # pas de broadcast classique
    if network.num_addresses > 1:
        candidates.add(str(network.network_address + 1))
    for delta in (-1, 1):
        p2 = prefix + delta
        if 0 <= p2 <= 32:
            try:
                fake_net = ipaddress.IPv4Network((host_ip, p2), strict=False)
                candidates.add(str(fake_net.broadcast_address))
            except:
                pass
    base = list(network.broadcast_address.packed)
    if len(base) == 4:
        last = base[-1]
        for offset in [-1, -16, 1, 16]:
            new_last = (last + offset) % 256
            fake_ip = ".".join(str(b) for b in base[:-1] + [new_last])
            candidates.add(fake_ip)
    wrongs = [addr for addr in candidates if addr != correct]
    seen = set()
    result = []
    for addr in wrongs:
        if addr not in seen:
            seen.add(addr)
            result.append(addr)
        if len(result) == 3:
            break
    fallback = ["255.255.255.255", "0.0.0.0", "192.0.2.255", "127.0.0.1"]
    for addr in fallback:
        if len(result) >= 3:
            break
        if addr != correct and addr not in seen:
            result.append(addr)
    return result[:3]

#
# Question sur le broadcast
#
def generer_question_broadcast():
    adresse_ip = generer_ip_aleatoire()
    prefixe = generer_prefixe_aleatoire()
    bonne_reponse = calculer_broadcast(adresse_ip, prefixe)
    mauvaises_reponses = wrong_broadcast_address_answers(adresse_ip, prefixe)
    return {
        "question": f"Quelle est l'adresse de broadcast de {adresse_ip}/{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# PREMIERE ADRESSE UTILISABLE
#########################################

#
# Calcul de la premiere IP
#
def calculer_premiere_ip(ip, prefix):
    if not (0 <= prefix <= 32):
        raise ValueError("Le préfixe doit être entre 0 et 32")
    octets = list(map(int, ip.split(".")))
    if len(octets) != 4 or any(o < 0 or o > 255 for o in octets):
        raise ValueError("Adresse IP invalide")
    ip_int = (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3]
    mask = (0xffffffff << (32 - prefix)) & 0xffffffff
    net_int = ip_int & mask
    if prefix == 32:
        premiere = net_int
    elif prefix == 31:
        premiere = net_int
    else:
        premiere = net_int + 1
    return ".".join(str((premiere >> i) & 0xff) for i in (24, 16, 8, 0))

# 
# Mauvaises premieres adresses 
#
def wrong_first_usable_answers(ip_str, prefix):
    try:
        host_ip = ipaddress.IPv4Address(ip_str)
        network = ipaddress.IPv4Network((host_ip, prefix), strict=False)
    except Exception as e:
        raise ValueError(f"Adresse ou préfixe invalide : {e}")
    if network.num_addresses == 1:
        correct = None  
    elif network.num_addresses == 2:
        correct = str(network.network_address)
    else:
        correct = str(network.network_address + 1)
    candidates = set()
    candidates.add(str(network.network_address))
    candidates.add(ip_str)
    if network.num_addresses > 3:
        candidates.add(str(network.network_address + 2))
    candidates.add(str(network.broadcast_address))
    if network.num_addresses > 2:
        candidates.add(str(network.broadcast_address - 1))
    if correct:
        base = list(ipaddress.IPv4Address(correct).packed)
        last = base[-1]
        for offset in [-1, 2, 16]:
            new_last = (last + offset) % 256
            fake_ip = ".".join(str(b) for b in base[:-1] + [new_last])
            candidates.add(fake_ip)
    wrongs = [addr for addr in candidates if correct is None or addr != correct]
    seen = set()
    result = []
    for addr in wrongs:
        if addr not in seen:
            seen.add(addr)
            result.append(addr)
        if len(result) == 3:
            break
    fallback = ["0.0.0.0", "255.255.255.255", "127.0.0.1", "192.0.2.1"]
    for addr in fallback:
        if len(result) >= 3:
            break
        if (correct is None or addr != correct) and addr not in seen:
            result.append(addr)
    return result[:3]

#
# Question sur la premiere adresse utilisable
#
def generer_question_premiere_adresse():
    adresse_ip = generer_ip_aleatoire()
    prefixe = generer_prefixe_aleatoire()
    bonne_reponse = calculer_premiere_ip(adresse_ip, prefixe)
    mauvaises_reponses = wrong_first_usable_answers(adresse_ip, prefixe)
    return {
        "question": f"Quelle est la première adresse utilisable de {adresse_ip}/{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# DERNIERE ADRESSE UTILISABLE
#########################################

#
# Calcul de la derniere ip
#
def calculer_derniere_ip(ip, prefix):
    if not (0 <= prefix <= 32):
        raise ValueError("Le préfixe doit être entre 0 et 32")
    octets = list(map(int, ip.split(".")))
    if len(octets) != 4 or any(o < 0 or o > 255 for o in octets):
        raise ValueError("Adresse IP invalide")
    ip_int = (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3]
    mask = (0xffffffff << (32 - prefix)) & 0xffffffff
    wildcard = (~mask) & 0xffffffff
    broadcast_int = (ip_int & mask) | wildcard
    if prefix == 32:
        derniere = ip_int  
    elif prefix == 31:
        derniere = broadcast_int
    else:
        derniere = broadcast_int - 1
    return ".".join(str((derniere >> i) & 0xff) for i in (24, 16, 8, 0))

#
# Mauvaises dernières adresses
#
def wrong_last_usable_answers(ip_str, prefix):
    try:
        host_ip = ipaddress.IPv4Address(ip_str)
        network = ipaddress.IPv4Network((host_ip, prefix), strict=False)
    except Exception as e:
        raise ValueError(f"Adresse ou préfixe invalide : {e}")
    if network.num_addresses == 1:
        correct = None  
    elif network.num_addresses == 2:
        correct = str(network.broadcast_address)
    else:
        correct = str(network.broadcast_address - 1)
    candidates = set()
    candidates.add(str(network.broadcast_address))
    candidates.add(ip_str)
    if network.num_addresses > 3:
        candidates.add(str(network.broadcast_address - 2))
    candidates.add(str(network.network_address))
    if network.num_addresses > 2:
        candidates.add(str(network.network_address + 1))
    if correct:
        base = list(ipaddress.IPv4Address(correct).packed)
        last = base[-1]
        for offset in [-2, 1, -16]:
            new_last = (last + offset) % 256
            fake_ip = ".".join(str(b) for b in base[:-1] + [new_last])
            candidates.add(fake_ip)
    wrongs = [addr for addr in candidates if correct is None or addr != correct]
    seen = set()
    result = []
    for addr in wrongs:
        if addr not in seen:
            seen.add(addr)
            result.append(addr)
        if len(result) == 3:
            break
    fallback = ["255.255.255.255", "0.0.0.0", "127.0.0.1", "192.0.2.254"]
    for addr in fallback:
        if len(result) >= 3:
            break
        if (correct is None or addr != correct) and addr not in seen:
            result.append(addr)
    return result[:3]

#
# Question sur la dernière adresse utilisable
#
def generer_question_derniere_adresse():
    adresse_ip = generer_ip_aleatoire()
    prefixe = generer_prefixe_aleatoire()
    bonne_reponse = calculer_derniere_ip(adresse_ip, prefixe)
    mauvaises_reponses = wrong_last_usable_answers(adresse_ip, prefixe)
    return {
        "question": f"Quelle est la dernière adresse utilisable de {adresse_ip}/{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# NON APPARTENANCE AU RESEAU
#########################################

#
# Les adresses dans le range
#
def valid_ips_in_network(ip_str, prefix):
    try:
        host_ip = ipaddress.IPv4Address(ip_str)
        network = ipaddress.IPv4Network((host_ip, prefix), strict=False)
    except Exception as e:
        raise ValueError(f"Entrée invalide : {e}")
    if network.num_addresses == 1:
        all_valid = [str(network.network_address)]
    elif network.num_addresses == 2:
        all_valid = [str(network.network_address), str(network.broadcast_address)]
    else:
        all_valid = [
            str(ip) for ip in network.hosts()
        ]
        if len(all_valid) < 3:
            all_valid = [str(ip) for ip in network]
    if len(all_valid) == 0:
        all_valid = [str(network.network_address)]
    result = []
    for _ in range(3):
        result.append(random.choice(all_valid))
    unique_result = list(dict.fromkeys(result))  # préserve l'ordre
    while len(unique_result) < 3:
        unique_result.append(random.choice(all_valid))
    return unique_result[:3]

#
# Une adresse hors du range
#
def invalid_ip_just_outside(ip_str, prefix):
    try:
        host_ip = ipaddress.IPv4Address(ip_str)
        network = ipaddress.IPv4Network((host_ip, prefix), strict=False)
    except Exception as e:
        raise ValueError(f"Entrée invalide : {e}")
    net_addr = int(network.network_address)
    bcast_addr = int(network.broadcast_address)
    if net_addr > 0:
        candidate = net_addr - 1
    elif bcast_addr < (2**32 - 1):
        candidate = bcast_addr + 1
    else:
        raise ValueError("Impossible de générer une adresse hors réseau pour 0.0.0.0/0")
    return str(ipaddress.IPv4Address(candidate))

#
# Question sur la non-appartenance d'une adresse
#
def generer_question_non_appartenance_adresse():
    adresse_ip = generer_ip_aleatoire()
    prefixe = generer_prefixe_aleatoire()
    mauvaises_reponses = valid_ips_in_network(adresse_ip, prefixe)
    reseau = calculer_adresse_reseau(adresse_ip, prefixe)
    bonne_reponse = invalid_ip_just_outside(adresse_ip, prefixe)
    return {
        "question": f"Quelle adresse n'appartient pas au réseau {reseau}/{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# APPARTENANCE AU RESEAU
#########################################

#
# Adresses hors du range
#
def invalid_ips_outside_network(ip_str, prefix):
    try:
        host_ip = ipaddress.IPv4Address(ip_str)
        network = ipaddress.IPv4Network((host_ip, prefix), strict=False)
    except Exception as e:
        raise ValueError(f"Entrée invalide : {e}")
    net_int = int(network.network_address)
    bcast_int = int(network.broadcast_address)
    block_size = 1 << (32 - prefix)
    cand1 = None
    if net_int - 2 >= 0:
        cand1 = str(ipaddress.IPv4Address(net_int - 2))
    elif net_int - 1 >= 0:
        cand1 = str(ipaddress.IPv4Address(net_int - 1))
    else:
        cand1 = "192.0.2.1"  # RFC
    cand2 = None
    if bcast_int + 3 < 2**32:
        cand2 = str(ipaddress.IPv4Address(bcast_int + 3))
    elif bcast_int + 2 < 2**32:
        cand2 = str(ipaddress.IPv4Address(bcast_int + 2))
    elif bcast_int + 1 < 2**32:
        cand2 = str(ipaddress.IPv4Address(bcast_int + 1))
    else:
        cand2 = "203.0.113.1"  # RFC
    cand3 = None
    prev_start = net_int - block_size
    if prev_start >= 0:
        cand3 = str(ipaddress.IPv4Address(prev_start))
    else:
        next_start = bcast_int + 1
        if next_start + block_size - 1 < 2**32:
            cand3 = str(ipaddress.IPv4Address(next_start))
    if cand3 is None:
        cand3 = "198.51.100.1"
    def ensure_outside(ip_str, net):
        addr = ipaddress.IPv4Address(ip_str)
        return str(addr) if addr not in net else "192.0.2.99"
    cand1 = ensure_outside(cand1, network)
    cand2 = ensure_outside(cand2, network)
    cand3 = ensure_outside(cand3, network)
    candidates = [cand1, cand2, cand3]
    unique = []
    seen = set()
    for ip in candidates:
        if ip not in seen:
            seen.add(ip)
            unique.append(ip)
    fallbacks = ["192.0.2.10", "192.0.2.11", "192.0.2.12", "203.0.113.10"]
    for fb in fallbacks:
        if len(unique) >= 3:
            break
        if fb not in seen:
            unique.append(fb)
            seen.add(fb)
    return unique[:3]

#
# Question sur l'appartenance d'une adresse à un réseau
#
def generer_question_appartenance_adresse():
    adresse_ip = generer_ip_aleatoire()
    prefixe = generer_prefixe_aleatoire()
    mauvaises_reponses = invalid_ips_outside_network(adresse_ip, prefixe)
    reseau = calculer_adresse_reseau(adresse_ip, prefixe)
    return {
        "question": f"Quelle adresse appartient au réseau {reseau}/{prefixe} ?",
        "bonne_reponse": adresse_ip,
        "mauvaises_reponses": mauvaises_reponses
    }


#########################################
# SOUS-RESEAUX
#########################################

#
# Générer un sous-préfixe à partir d'un préfixe
#
def generer_P2(P1, max_delta=6):
    if not (1 <= P1 <= 31):
        raise ValueError("P1 doit être entre 1 et 31 (inclus).")
    P2_min = P1 + 1
    P2_max = min(P1 + max_delta, 32)
    if P2_min > 32:
        raise ValueError("P1 est trop grand pour avoir un P2 valide.")
    return random.randint(P2_min, P2_max)

#
# Mauvais sous-reseau
#
def distracteurs_nb_sous_reseaux(P1, P2):
    if not (0 <= P1 < P2 <= 32):
        raise ValueError("Doit vérifier : 0 <= P1 < P2 <= 32")
    delta = P2 - P1
    bonne = 1 << delta  
    distracteurs = set()
    distracteurs.add(delta)
    distracteurs.add(1)
    distracteurs.add(bonne * 2)
    if bonne >= 2:
        distracteurs.add(bonne // 2)
    if P2 <= 30:
        nb_adresses = 1 << (32 - P2)
        distracteurs.add(nb_adresses)
    elif P2 == 31:
        distracteurs.add(2)
    else:  # P2 == 32
        distracteurs.add(1)
    mauvaises = [x for x in distracteurs if isinstance(x, int) and x != bonne and x >= 0]
    fallback = [0, 2, 4, 8, 16, 32, 64]
    for x in fallback:
        if len(mauvaises) >= 5:
            break
        if x != bonne:
            mauvaises.append(x)
    seen = set()
    unique = []
    for x in mauvaises:
        if x not in seen:
            seen.add(x)
            unique.append(x)
    random.shuffle(unique)
    return unique[:3]

#
# Calcul du nombre de sous-réseau
#
def calculer_nb_sous_reseaux(P1, P2):
    if not (0 <= P1 < P2 <= 32):
        raise ValueError("Doit vérifier : 0 <= P1 < P2 <= 32")
    return 1 << (P2 - P1)

#
# Question sur le nombre de sous réseau dans un réseau parent
#
def generer_question_sous_reseau():
    adresse_ip = generer_ip_aleatoire()
    prefixe = generer_prefixe_aleatoire()
    petit_prefixe = generer_P2(prefixe)
    bonne_reponse = calculer_nb_sous_reseaux(prefixe,petit_prefixe)
    mauvaises_reponses = distracteurs_nb_sous_reseaux(prefixe,petit_prefixe)
    reseau = calculer_adresse_reseau(adresse_ip, prefixe)
    return {
        "question": f"Combien peut on faire de /{petit_prefixe} dans le réseau {reseau}/{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# VLSM
#########################################

#
# Découpage VLSM
#
def decoupage_vlsm(adresse_ip: str, prefixe: int, nb_machines: int) -> int:
    if not 1 <= prefixe <= 32:
        raise ValueError("Le préfixe doit être compris entre 1 et 32")
    if nb_machines < 1:
        raise ValueError("Le nombre de machines doit être >= 1")
    try:
        reseau = ipaddress.IPv4Network(f"{adresse_ip}/{prefixe}", strict=False)
    except ValueError as e:
        raise ValueError(f"Adresse IP ou préfixe invalide : {e}")
    adresses_necessaires = nb_machines + 2  
    if adresses_necessaires > 2 ** (32 - prefixe):
        raise ValueError(
            f"Impossible : le réseau {reseau} ({2 ** (32 - prefixe)} adresses) "
            f"ne peut pas contenir {adresses_necessaires} adresses requises."
        )
    bits_hotes = (adresses_necessaires - 1).bit_length()
    nouveau_prefixe = 32 - bits_hotes
    if nouveau_prefixe < prefixe:
        raise ValueError(
            f"Impossible de créer des sous-réseaux avec {nb_machines} machines : "
            f"le réseau de base est trop petit."
        )
    bits_empruntes = nouveau_prefixe - prefixe
    nb_sous_reseaux = 2 ** bits_empruntes
    return nb_sous_reseaux

#
# Cohérence VLSM
#
def generer_donnees_vlsm_coherentes(prefixe_max=22):
    prefixe_max_securise = min(prefixe_max, 22)
    prefixe_parent = random.randint(prefixe_max_securise, 28)
    bits_hotes_parents = 32 - prefixe_parent
    bits_empruntes = random.randint(1, 4)
    nouveau_prefixe = prefixe_parent + bits_empruntes
    bits_hotes_enfants = 32 - nouveau_prefixe
    capacite_enfant = 2 ** bits_hotes_enfants
    seuil_min = (capacite_enfant // 2) + 1
    seuil_max = capacite_enfant - 2
    if seuil_max <= seuil_min:
        nb_machines = seuil_max if seuil_max > 0 else 1
    else:
        nb_machines = random.randint(seuil_min, seuil_max)
    return prefixe_parent, nb_machines

#
# Distracteurs VLSM
#
def generer_distracteurs_vlsm(prefixe_parent, bonne_reponse):
    distracteurs = set()
    bits_empruntes = bonne_reponse.bit_length() - 1
    if bits_empruntes > 0:
        distracteurs.add(bits_empruntes)
    distracteurs.add(bonne_reponse * 2)
    if bonne_reponse // 2 > 0:
        distracteurs.add(bonne_reponse // 2)
    nouveau_prefixe = prefixe_parent + bits_empruntes
    nb_hotes_max = 2 ** (32 - nouveau_prefixe)
    distracteurs.add(nb_hotes_max)
    distracteurs.add(nb_hotes_max - 2) # Nombre d'adresses utiles
    candidats = [d for d in distracteurs if d != bonne_reponse and d > 0]
    fallbacks = [1, 2, 4, 8, 16, 32, 64, 128]
    for f in fallbacks:
        if len(candidats) >= 3:
            break
        if f != bonne_reponse and f not in candidats:
            candidats.append(f)
    random.shuffle(candidats)
    return candidats[:3]

#
# Question sur le VLSM
#
def generer_question_vlsm():
    adresse_ip = generer_ip_aleatoire()
    prefixe, nb_machines = generer_donnees_vlsm_coherentes()
    reseau = calculer_adresse_reseau(adresse_ip, prefixe)
    bonne_reponse = decoupage_vlsm(reseau, prefixe, nb_machines)
    mauvaises_reponses = generer_distracteurs_vlsm(prefixe, bonne_reponse)
    return {
        "question": f"Combien peut on faire de sous-réseaux de {nb_machines} machines dans le réseau {reseau}/{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# Wildcards
#########################################

#
# Calcul d'un wildcard
#
def get_wildcard_mask(prefix):
    bits_hotes = 32 - prefix
    wildcard_int = (1 << bits_hotes) - 1
    return ".".join(map(str, [
        (wildcard_int >> 24) & 0xff,
        (wildcard_int >> 16) & 0xff,
        (wildcard_int >> 8) & 0xff,
        wildcard_int & 0xff
    ]))

#
# Mauvais wildcards
#
def generer_distracteurs_wildcard(prefix):
    bonne_reponse = get_wildcard_mask(prefix)
    distracteurs = set()
    if prefix + 1 <= 32:
        distracteurs.add(get_wildcard_mask(prefix + 1))
    if prefix - 1 >= 8:
        distracteurs.add(get_wildcard_mask(prefix - 1))
    full_mask = (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
    net_mask = ".".join(map(str, [
        (full_mask >> 24) & 0xff,
        (full_mask >> 16) & 0xff,
        (full_mask >> 8) & 0xff,
        full_mask & 0xff
    ]))
    distracteurs.add(net_mask)
    wildcard_int = (1 << (32 - prefix)) - 1
    err_int = wildcard_int + 1
    if err_int < (1 << 32):
        err_str = ".".join(map(str, [
            (err_int >> 24) & 0xff,
            (err_int >> 16) & 0xff,
            (err_int >> 8) & 0xff,
            err_int & 0xff
        ]))
        distracteurs.add(err_str)
    candidats = [d for d in distracteurs if d != bonne_reponse]
    fallbacks = ["0.0.0.255", "0.0.255.255", "0.0.0.15", "0.0.0.3", "0.0.0.63"]
    random.shuffle(fallbacks)
    for f in fallbacks:
        if len(candidats) >= 3:
            break
        if f != bonne_reponse and f not in candidats:
            candidats.append(f)
    random.shuffle(candidats)
    return candidats[:3]

#
# Question sur le wildcard
#
def generer_question_wildcard():
    adresse_ip = generer_ip_aleatoire()
    prefixe = generer_prefixe_aleatoire()
    bonne_reponse = get_wildcard_mask(prefixe)
    mauvaises_reponses = generer_distracteurs_wildcard(prefixe)
    reseau = calculer_adresse_reseau(adresse_ip, prefixe)
    return {
        "question": f"Quel est le wildcard du réseau {reseau}/{prefixe} ?",
        "bonne_reponse": bonne_reponse,
        "mauvaises_reponses": mauvaises_reponses
    }

#########################################
# GESTION DU QUIZZ
#########################################

#
# Melanger les réponses
#
def melanger_reponses(bonne_reponse: int, *reponses):
    if len(reponses) != 4:
        raise ValueError("Il faut exactement 4 réponses.")
    if not (1 <= bonne_reponse <= 4):
        raise ValueError("Le numéro de la bonne réponse doit être entre 1 et 4.")
    reponses_list = list(reponses)
    items = [(reponses_list[i], i + 1 == bonne_reponse) for i in range(4)]
    random.shuffle(items)
    nouvelles_reponses = [item[0] for item in items]
    nouvel_indice = next(i for i, item in enumerate(items, start=1) if item[1])
    return nouvelles_reponses, nouvel_indice

#
# Melanger le QCM
#
def melanger_qcm(bonne_reponse, mauvaises_reponses):
    if len(mauvaises_reponses) != 3:
        raise ValueError("Doit y avoir exactement 3 mauvaises réponses.")
    reponses = [bonne_reponse] + mauvaises_reponses
    indices = list(range(4))
    random.shuffle(indices)
    reponses_melangees = [reponses[i] for i in indices]
    position_bonne = indices.index(0) + 1  
    return reponses_melangees, position_bonne

#
# Poser une question
#
def poser_question(question_data):
    q = question_data["question"]
    bonne = question_data["bonne_reponse"]
    mauvaises = question_data["mauvaises_reponses"]

    reponses_melangees, pos_bonne = melanger_qcm(bonne, mauvaises)

    print(f"\n❓ {q}\n")
    for i, rep in enumerate(reponses_melangees, start=1):
        print(f"{i} → {rep}")
    while True:
        try:
            choix = int(input("\nVotre réponse (1-4) : "))
            if 1 <= choix <= 4:
                break
            print("Veuillez entrer un nombre entre 1 et 4.")
        except ValueError:
            print("Entrée invalide. Veuillez entrer un chiffre.")

    correct = (choix == pos_bonne)
    if correct:
        print("\n✅ Bravo ! C’est la bonne réponse ! 🎉")
    else:
        print(f"\n❌ Non, la bonne réponse était : {bonne} (option {pos_bonne})")
    return correct

#
# Lancer le quiz
#
def lancer_quiz(iterateur_questions, nb_questions=1):
    score = 0
    iterateur = iter(iterateur_questions)
    for i in range(nb_questions):
        print(f"\n{'='*50}")
        print(f"Question {i+1}/{nb_questions}")
        try:
            data = next(iterateur) 
        except StopIteration:
            print("Plus de questions disponibles.")
            break
        if poser_question(data):
            score += 1
    print(f"\n{'='*50}")
    print(f"📊 Score final : {score}/{nb_questions}")
    if nb_questions > 0:
        pct = (score / nb_questions) * 100
        print(f"→ {pct:.0f}% de réussite")
#
# Menu et boucle
#
def quiz_netmask():
    banque_questions = {
        1: ("Netmask à partir du préfixe CIDR", generer_question_prefixe_netmask),
        2: ("Préfixe CIDR vers netmask", generer_question_netmask_prefixe),
        3: ("Nombre de machines par Préfixe CIDR", generer_question_nombre_machines),
        4: ("Adresse réseau", generer_question_adresse_reseau),
        5: ("Adresse broadcast", generer_question_broadcast),
        6: ("Première adresse utilisable", generer_question_premiere_adresse),
        7: ("Dernière adresse utilisable", generer_question_derniere_adresse),
        8: ("Non appartenance au réseau", generer_question_non_appartenance_adresse),
        9: ("Appartenance au réseau", generer_question_appartenance_adresse),
        10: ("Nombre de sous-réseaux", generer_question_sous_reseau),
        11: ("Découpage VLSM", generer_question_vlsm),
        12: ("Wildcard", generer_question_wildcard),
    }
    print("🎯 Bienvenue dans le Quiz MyCIDR !\n")
    print("0. Mélange aléatoire de tous les thèmes")
    for num, (desc, _) in banque_questions.items():
        print(f"{num}. {desc}")
    while True:
        try:
            choix = int(input(f"\nChoisissez un thème (0-{len(banque_questions)}) : "))
            if 0 <= choix <= len(banque_questions):
                break
            print(f"Veuillez choisir un numéro entre 0 et {len(banque_questions)}.")
        except ValueError:
            print("Entrée invalide.")
    nb = int(input("Combien de questions voulez-vous ? (ex: 3) : "))
    if choix == 0:
        fonctions = [f for _, f in banque_questions.values()]
    else:
        fonctions = [banque_questions[choix][1]]  
    questions = []
    for _ in range(nb):
        func = random.choice(fonctions)
        question = func()
        questions.append(question)
    lancer_quiz(questions, nb_questions=nb)

#
# main sweet main
#
if __name__ == "__main__":
    quiz_netmask()
