#!/usr/bin/python3

# TODO : externaliser toutes les vars

import logging
import subprocess
import sys
import os
import re
from typing import List
from libscibian_networking import Networking

# todo: certaines variables sont déclarées 2 fois dans ce fichier python,
#       et le utils/variables de nft.
# todo: il faut trouver un moyen d'enlever cette duplication, ou au moins la réduire


def send_notification(title: str, message: str) -> None:
    icon_path = "/usr/share/icons/scibian/scalable/scibian-firewall.svg"
    cmd = "who | awk '$0 ~/\(:[0-9]\)/'"
    ps = subprocess.Popen(
        cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
    )
    who = ps.communicate()[0].decode("utf-8")
    # Check si la string who est vide
    if not who:
        logging.info("Aucune session utilisateur ouverte. Pas de notifications.")
        return
    for line in who.strip().split("\n"):
        fields = line.split()  # ex: ['g62081', ':1', '2022-06-14', '13:35', '(:1)']
        user = fields[0]
        user_id = subprocess.run(
            ["id", "-u", user], text=True, capture_output=True
        ).stdout.split("\n")[0]
        display = fields[1]
        dbus_address = "unix:path=/run/user/" + user_id + "/bus"

        subprocess.run(
            [
                "sudo",
                "-u",
                user,
                "--preserve-env",
                "notify-send",
                "-i",
                icon_path,
                title,
                message,
            ],
            env={"DBUS_SESSION_BUS_ADDRESS": dbus_address, "DISPLAY": display},
        )
        logging.info(f"Notification envoyée à {user} sur le display {display}.")


def notify_users(profile: str, action: str) -> None:
    profile_messages = {
        "up": {
            "external": "Réseau externe",
            "captive-portal": "Portail captif externe",
            "edf-vpn": "VPN EDF",
            "edf-internal": "Réseau interne EDF",
        },
        "down": {"edf-vpn": "Sortie du VPN EDF"},
    }
    send_notification("Profil de pare-feu", profile_messages.get(action).get(profile))


def run_profile_hooks(profile: str, arg: str) -> None:
    hookdir = os.path.join("/etc/scibian-firewall/hooks/", profile)
    if os.path.isdir(hookdir):
        res = subprocess.run(
            ["run-parts", "--verbose", "--arg=" + arg, hookdir],
            text=True,
            capture_output=True,
        )
        logging.info("resultat du lancement des hooks : ")
        logging.info(res)


def apply_profile(profile: str) -> None:
    path_to_firewall_profile = "/etc/scibian-firewall/" + profile
    res = subprocess.run(
        ["nft", "-f", path_to_firewall_profile], text=True, capture_output=True
    )
    logging.info("resultat de la commande : ")
    logging.info(res)
    run_profile_hooks(profile, "up")
    notify_users(profile, "up")


def is_applied_profile(profile: str) -> bool:
    res = subprocess.run(
        ["nft", "list", "table", "inet", profile], text=True, capture_output=True
    )
    if res.returncode == 0:
        logging.info("Profil " + profile + " is applied")
        return True
    else:
        logging.info("Profil " + profile + " is not applied")
        return False


def apply_external_profile() -> None:
    logging.info("application du profil EXTERNE")
    apply_profile("external")


def apply_captive_portal_profile() -> None:
    logging.info("application du profil CAPTIVE-PORTAL")
    apply_profile("captive-portal")


def apply_edf_internal_profile() -> None:
    logging.info("application du profil EDF-INTERNAL")
    apply_profile("edf-internal")


def apply_edf_vpn_profile() -> None:
    logging.info("application du profil EDF-VPN")
    apply_profile("edf-vpn")


def apply_edf_vdi_profile() -> None:
    logging.info("application du profil EDF-VDI")
    apply_profile("edf-vdi")


def remove_edf_vpn_profile() -> None:
    logging.info("Suppression du profil EDF-VPN")
    res = subprocess.run(
        ["nft", "delete", "table", "inet", "edf-vpn"], text=True, capture_output=True
    )
    notify_users("edf-vpn", "down")
    logging.info("résultat de la commande : ")
    logging.info(res)


def get_detector_ips() -> List:
    """
    La récupération de l'ip du détecteur de portails captifs se fait avec dig.
    dig peut retourner plusieurs ip.
    """
    hostname = "www.gstatic.com"

    logging.info("récupération des ip du détecteur de portails captifs: " + hostname)
    res = subprocess.run(
        ["dig", hostname, "+short", "+time=1", "+tries=2"],
        text=True,
        capture_output=True,
    )
    if res.returncode != 0 or not res.stdout.rstrip():
        logging.info("La récupération d'ip a échoué")
        raise Exception
    else:
        ips = res.stdout.rstrip().splitlines()
        return ips


def whitelist_ips_in_external_rules_for_web(ips: List) -> None:
    """
    Dans le profil externe, ajout dynamique des ips passées en paramètre
    sur les ports 80 et 443
    """
    for ip in ips:
        logging.info("ajout de l'ip " + ip + " à la whitelist du profil externe")
        subprocess.run(
            [
                "nft",
                "add",
                "rule",
                "inet external",
                "output",
                "oifname",
                '"wl*"',
                "ip",
                "daddr",
                ip,
                "tcp",
                "dport",
                "{80,443}",
                "accept",
            ],
            text=True,
            capture_output=True,
        )


def is_behind_captive_portal() -> bool:
    """Avant de lancer le check de vérification de la présence du portail captif,
    on récupère les ip du détecteur de portails captifs, puis les autorisons dans
    le profil externe
    """
    try:
        detector_ips = get_detector_ips()
        whitelist_ips_in_external_rules_for_web(detector_ips)
        net = Networking()
        logging.info("sommes nous derrière un portail captif ?")
        res = net.is_behind_captive_portal()
        logging.info("le sommes-nous ? " + str(res))
        return res
    except Exception as e:
        logging.info(
            "Une erreur s'est produite lors du check de la présence d'un portail captif, on retourne False"  # noqa: E501
        )
        logging.info(e)
        return False


def is_inside_edf_network() -> bool:
    lib_networking = Networking()
    logging.info("sommes nous sur le réseau edf ?")
    res = lib_networking.is_edf_network()
    logging.info("le sommes nous ? " + str(res))
    return res


def is_vdi() -> bool:
    try:
        # scibian-firewall est en dépendance de scibian-desktop-vdi-edf
        # pour déterminer si c'est une vdi on teste si ce paquet est installé
        result = subprocess.run(
            "dpkg -l | grep scibian-desktop-vdi-edf",
            shell=True,
            capture_output=True,
            text=True,
        )
        return result.returncode == 0
    except Exception as e:
        logging.error(f"{e}")
        return False


def dispatch_wifi_up() -> None:
    if is_inside_edf_network():
        logging.info("Le DSP est sur le réseau WIFI EDF")
        apply_edf_internal_profile()
    else:
        logging.info("Le DSP est sur un réseau externe")
        apply_external_profile()
        if is_behind_captive_portal():
            logging.info("De plus, il est derrière un portail captif")
            apply_captive_portal_profile()


def dispatch_ethernet_up() -> None:
    if is_inside_edf_network():
        logging.info("Le DSP est sur le réseau ethernet EDF")
        apply_edf_internal_profile()
    else:
        logging.info("Le DSP est sur un réseau externe, sans portail captif")
        apply_external_profile()
        if is_behind_captive_portal():
            logging.info("De plus, il est derrière un portail captif")
            apply_captive_portal_profile()
    # Application du profil VDI suite à l'application des autres profils
    # Pour échaper aux flush ruleset
    if is_vdi():
        apply_edf_vdi_profile()


def dispatch_vpn_up():
    # Faire le test de conformité TODO
    # Appliquer le profil edf-vpn
    apply_edf_vpn_profile()


def dispatch_vpn_down():
    # Quand tun0 down, on doit enlever le profil vpn pour ne pas gener les autres tests
    remove_edf_vpn_profile()


# Voir syntaxe acceptée nftables iifname
# https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes
def get_interfacename_nftables_file(variable: str) -> list:
    file = open("/etc/scibian-firewall/utils/variables", "r")
    interfaces = []
    lines = file.readlines()
    for index, line in enumerate(lines):
        # define ethernet_interfaces = "eth1"
        if re.match(
            '^define[ ]{0,1}%s[ ]{0,1}=[ ]{0,1}["][a-zA-Z0-9*]*["]$' % variable, line
        ):
            value = line.split("=")[1].split('"')[1]
            interfaces.append(value)
        # define ethernet_interfaces = eth1
        elif re.match(
            "^define[ ]{0,1}%s[ ]{0,1}=[ ]{0,1}[a-zA-Z0-9*]*$" % variable, line
        ):
            value = line.split("=")[1].strip()
            interfaces.append(value)
        # define ethernet_interfaces = {"eth2", "eth1"}
        elif re.match(
            '^define[ ]{0,1}%s[ ]{0,1}=[ ]{0,1}[{][a-zA-Z0-9", ]*[}]$' % variable, line
        ):
            value = line.split("=")[1].strip()
            interfaces = (
                line.split("=")[1]
                .strip()
                .replace("{", "")
                .replace("}", "")
                .replace('"', "")
                .split(",")
            )
    file.close()

    if not interfaces:
        logging.info("La récupération de la variable " + variable + " a échoué")
    else:
        return interfaces


def dispatch_up(interface: str) -> None:
    if any(
        re.match("^%s$" % i.replace("*", ".*"), interface)
        for i in get_interfacename_nftables_file("vpn_interface")
    ):
        dispatch_vpn_up()
    elif any(
        re.match("^%s$" % i.replace("*", ".*"), interface)
        for i in get_interfacename_nftables_file("wifi_interfaces")
    ):
        dispatch_wifi_up()
    elif any(
        re.match("^%s$" % i.replace("*", ".*"), interface)
        for i in get_interfacename_nftables_file("ethernet_interfaces")
    ):
        dispatch_ethernet_up()


def dispatch_down(interface: str) -> None:
    if any(
        re.match("^%s$" % i.replace("*", ".*"), interface)
        for i in get_interfacename_nftables_file("vpn_interface")
    ):
        dispatch_vpn_down()


def main(argv: List[str]) -> None:
    interface = argv[0]
    action = argv[1]

    logging.info("\n")  # Nouveau bloc de log, on le sépare d'un saut de ligne
    if action == "up":
        logging.info(
            "Entrée dans scibian-firewall, if : " + interface + ", action : " + action
        )
        dispatch_up(interface)
    elif action == "down":
        logging.info(
            "Entrée dans scibian-firewall, if : " + interface + ", action : " + action
        )
        dispatch_down(interface)

    sys.exit(0)


# Todo gérer mieux les logs que ca, avec un logrotate en +
if __name__ == "__main__":
    logging.basicConfig(
        format="%(asctime)s %(levelname)-8s %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
        filename="/var/log/scibian-firewall.log",
        level=logging.INFO,
    )
    main(sys.argv[1:])
