"""

    Version 1.0
    Date 27/03/2025
    Auteur : Stéphan Peccini

    Ce script permet de récupérer un graphe DOT syntaxiquement correct, pour
    le formater selon la structure hiérarchique souhaitée.

    Exemple d'utilisation :
    -----------------------
    Le script ihh.py met à jour les données ihh en fonction des nouvelles informations.
    Pour faciliter le travail, tout est fait avec un graphe NetworkX et ensuite sauvé
    au format DOT sans aucune structure hiérarchique.
    Il suffit alors de passer le fichier résultat de ihh.py pour avoir un fichier DOT
    conforme aux attentes.

"""

import sys
import networkx as nx
from networkx.drawing.nx_agraph import read_dot
import logging

logging.basicConfig(
    filename="beautify_debug.log",
    filemode="w",
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)


def formater_noeuds_par_niveau(schema, niveau, indentation=4):
    """
    Génère la syntaxe DOT avec une hiérarchie complète de sous-graphes imbriqués:
    - Sous-graphes pour les nœuds du niveau spécifié (0, 1, 2)
    - Sous-graphes pour les nœuds de niveau 10 liés à ces nœuds
    - Sous-graphes pour les nœuds de niveau 11 liés aux nœuds de niveau 10
    - Pour les nœuds de niveau 11, inclut leurs nœuds destination (sauf niveau 99)
    """
    # Convertir le niveau en chaîne si nécessaire
    niveau_str = str(niveau)

    # Identifier les nœuds du niveau spécifié
    noeuds_niveau = [noeud for noeud, attr in schema.nodes(data=True)
                    if attr.get('niveau') == niveau_str]

    # Récupérer les attributs de ces nœuds
    attributs_niveau = {noeud: schema.nodes[noeud] for noeud in noeuds_niveau}

    # Définir les couleurs selon la légende
    couleurs_operations = {
        "Assemblage": "#a0d6ff",
        "Niveau Composant": "#b3ffe0",
        "Fabrication": "#ffe0cc",
        "Extraction": "#ffd699",
        "Reserves": "#ffd699",
        "Pays": "#e6f2ff",
        "Zone": "#e6f2ff",
        "Entreprise": "#f0f0f0"
    }

    # Fonction pour déterminer l'opération et la couleur
    def determiner_operation_et_couleur(nom_noeud):
        operation = nom_noeud.split('_')[0] if '_' in nom_noeud else nom_noeud
        couleur = "#ffffff"  # Blanc par défaut
        for op_cle, op_couleur in couleurs_operations.items():
            if op_cle in operation:
                couleur = op_couleur
                break
        return operation, couleur

    # Fonction pour formater une arête avec ses attributs
    def formater_arete(source, dest, edge_attrs, indent_level):
        edge_str = f"{indent_level}{source} -> {dest} ["
        edge_attrs_formatted = []
        for edge_attr, edge_val in edge_attrs.items():
            if edge_val:
                edge_attrs_formatted.append(f'{edge_attr}="{edge_val}"')
        edge_str += ", ".join(edge_attrs_formatted)
        edge_str += "];\n"
        return edge_str

    # Fonction pour formater un nœud avec ses attributs
    def formater_noeud(nom_noeud, attributs_noeud, indent_level):
        node_str = f"{indent_level}{nom_noeud} ["
        attrs_formattés = []
        for attr_nom, attr_val in attributs_noeud.items():
            if attr_val and attr_nom in ordre_attributs:
                attrs_formattés.append(f'{attr_nom}="{attr_val}"')
        node_str += ", ".join(attrs_formattés)
        node_str += "];\n"
        return node_str

    # Fonction pour trouver toutes les relations sortantes d'un nœud
    def trouver_relations_sortantes(noeud_source):
        relations = []
        for s, d, attrs in schema.edges(data=True):
            if s == noeud_source:
                relations.append((s, d, attrs))

        # Suppression des doublons en convertissant le dictionnaire en tuple trié
        relations_sans_doublons = set((a, b, tuple(sorted(c.items()))) for a, b, c in relations)
        # Reconversion en dictionnaire
        relations_finales = [(a, b, dict(c)) for a, b, c in relations_sans_doublons]

        return relations_finales

    # Définir les niveaux d'indentation
    indent = " " * (indentation + 4)
    indent_inner = " " * (indentation + 8)
    indent_inner2 = " " * (indentation + 12)
    indent_inner3 = " " * (indentation + 16)

    # Préparer la chaîne de sortie
    sortie = f"\n{indent}// Sous-graphes pour les nœuds de niveau {niveau} avec leurs relations\n"

    # Définir l'ordre des attributs
    ordre_attributs = ["ihh_pays", "ihh_acteurs", "ivc", "isg", "label", "niveau", "fillcolor", "orphelin", "shape", "style", "fontname"]

    # Formater chaque nœud comme un subgraph avec ses relations sortantes
    for nom_noeud, attributs in attributs_niveau.items():
        # Débuter le sous-graphe principal
        sortie += f"{indent}subgraph cluster_{nom_noeud} {{\n"
        sortie += f"{indent_inner}label=\"{nom_noeud}\";\n"

        # Ajouter les autres attributs du sous-graphe
        for attr_nom, attr_valeur in attributs.items():
            if attr_valeur and attr_nom in ["style", "fillcolor", "color", "fontname"]:
                sortie += f"{indent_inner}{attr_nom}=\"{attr_valeur}\";\n"

        # Ajouter le nœud lui-même dans le sous-graphe
        sortie += formater_noeud(nom_noeud, attributs, indent_inner)

        # Trouver toutes les relations dont ce nœud est la source
        relations_sortantes = []
        sous_graphes_niveau_10 = {}  # Dictionnaire pour stocker les sous-graphes de niveau 10

        for source, dest, edge_attrs in trouver_relations_sortantes(nom_noeud):
            # Vérifier si la destination est un nœud de niveau 10
            dest_attrs = schema.nodes[dest]
            # est_niveau_10 = dest_attrs.get('niveau') == "10"
            est_niveau_10 = str(dest_attrs.get('niveau')) in {"10", "1010"}

            # Formater l'arête pour le sous-graphe parent
            edge_str = formater_arete(source, dest, edge_attrs, indent_inner)
            relations_sortantes.append(edge_str)

            # Traiter les nœuds de niveau 10
            if est_niveau_10 and dest not in sous_graphes_niveau_10:
                # Début du sous-graphe de niveau 10
                operation, couleur = determiner_operation_et_couleur(dest)
                sg_str = f"\n{indent_inner}subgraph cluster_{dest} {{\n"
                sg_str += f"{indent_inner2}label=\"{dest}\";\n"

                # Couleur de remplissage
                if 'fillcolor' in dest_attrs and dest_attrs['fillcolor']:
                    sg_str += f"{indent_inner2}fillcolor=\"{dest_attrs['fillcolor']}\";\n"
                else:
                    sg_str += f"{indent_inner2}fillcolor=\"{couleur}\";\n"

                sg_str += f"{indent_inner2}style=\"filled\";\n"

                # Ajouter le nœud de niveau 10 lui-même
                sg_str += formater_noeud(dest, dest_attrs, indent_inner2)

                # Collecter les relations sortantes du nœud de niveau 10
                relations_niveau_10 = trouver_relations_sortantes(dest)
                sous_graphes_niveau_11 = {}  # Pour stocker les sous-graphes de niveau 11

                if relations_niveau_10:
                    sg_str += f"\n{indent_inner2}// Relations sortantes du nœud de niveau 10\n"

                    for src, dst, rel_attrs in relations_niveau_10:
                        # Ajouter la relation
                        sg_str += formater_arete(src, dst, rel_attrs, indent_inner2)

                        # Vérifier si la destination est un nœud de niveau 11
                        if dst in schema.nodes:
                            dst_attrs = schema.nodes[dst]
                            dst_niveau = dst_attrs.get('niveau', '')

                            # Créer un sous-graphe pour les nœuds de niveau 11
                            if (dst_niveau == "11" or dst_niveau == "1011") and dst not in sous_graphes_niveau_11:
                                # Début du sous-graphe de niveau 11
                                dst_operation, dst_couleur = determiner_operation_et_couleur(dst)
                                dst_sg = f"\n{indent_inner2}subgraph cluster_{dst} {{\n"
                                dst_sg += f"{indent_inner3}label=\"{dst}\";\n"

                                # Couleur de remplissage
                                if 'fillcolor' in dst_attrs and dst_attrs['fillcolor']:
                                    dst_sg += f"{indent_inner3}fillcolor=\"{dst_attrs['fillcolor']}\";\n"
                                else:
                                    dst_sg += f"{indent_inner3}fillcolor=\"{dst_couleur}\";\n"

                                dst_sg += f"{indent_inner3}style=\"filled\";\n"

                                # Ajouter le nœud de niveau 11 lui-même
                                dst_sg += formater_noeud(dst, dst_attrs, indent_inner3)

                                # Collecter les relations sortantes du nœud de niveau 11
                                relations_niveau_11 = trouver_relations_sortantes(dst)
                                noeuds_destination = {}  # Pour stocker les nœuds destination
                                relations_additionnelles = []  # Pour stocker les relations des nœuds destination

                                if relations_niveau_11:
                                    dst_sg += f"\n{indent_inner3}// Relations sortantes du nœud de niveau 11\n"

                                    for n11_src, n11_dst, n11_attrs in relations_niveau_11:
                                        # Ajouter la relation
                                        dst_sg += formater_arete(n11_src, n11_dst, n11_attrs, indent_inner3)

                                        # Ajouter le nœud destination sauf s'il est de niveau 99
                                        if n11_dst in schema.nodes:
                                            n11_dst_attrs = schema.nodes[n11_dst]
                                            n11_dst_niveau = n11_dst_attrs.get('niveau', '')

                                            if n11_dst_niveau != "99" and n11_dst not in noeuds_destination and n11_dst != dst:
                                                noeuds_destination[n11_dst] = n11_dst_attrs

                                                # Collecter les relations sortantes du nœud destination
                                                for dest_src, dest_dst, dest_attrs in trouver_relations_sortantes(n11_dst):
                                                    relations_additionnelles.append((dest_src, dest_dst, dest_attrs))

                                # Ajouter les nœuds destination
                                for dest_nom, dest_attrs in noeuds_destination.items():
                                    dst_sg += formater_noeud(dest_nom, dest_attrs, indent_inner3)

                                # Ajouter les relations additionnelles
                                if relations_additionnelles:
                                    dst_sg += f"\n{indent_inner3}// Relations des nœuds destination\n"
                                    for rel_src, rel_dst, rel_attrs in relations_additionnelles:
                                        dst_sg += formater_arete(rel_src, rel_dst, rel_attrs, indent_inner3)

                                # Fermer le sous-graphe de niveau 11
                                dst_sg += f"{indent_inner2}}}\n"

                                # Ajouter à la collection
                                sous_graphes_niveau_11[dst] = dst_sg

                # Ajouter les sous-graphes de niveau 11 au sous-graphe de niveau 10
                if sous_graphes_niveau_11:
                    sg_str += "".join(sous_graphes_niveau_11.values())

                # Fermer le sous-graphe de niveau 10
                sg_str += f"{indent_inner}}}\n"

                # Ajouter à la collection
                sous_graphes_niveau_10[dest] = sg_str

        # Ajouter les relations au sous-graphe principal
        if relations_sortantes:
            sortie += f"\n{indent_inner}// Relations sortantes\n"
            sortie += "".join(relations_sortantes)

        # Ajouter les sous-graphes de niveau 10
        if sous_graphes_niveau_10:
            sortie += "\n"
            sortie += "".join(sous_graphes_niveau_10.values())

        # Terminer le sous-graphe principal
        sortie += f"{indent}}}\n\n"

    return sortie

def generer_rank_same(schema, indentation=4):
    """
    Génère des instructions rank=same pour les nœuds de même niveau dans un graphe DOT.

    Args:
        schema: Le graphe NetworkX chargé depuis un fichier DOT
        indentation: Nombre d'espaces pour l'indentation (par défaut 4)

    Returns:
        Une chaîne contenant les instructions rank=same formatées pour le fichier DOT
    """
    # Dictionnaire pour stocker les nœuds par niveau
    noeuds_par_niveau = {}

    # Parcourir tous les nœuds du graphe
    for noeud, attrs in schema.nodes(data=True):
        niveau = attrs.get('niveau')
        if niveau:  # Si le nœud a un attribut niveau
            if niveau not in noeuds_par_niveau:
                noeuds_par_niveau[niveau] = []
            noeuds_par_niveau[niveau].append(noeud)

    # Préparer l'indentation
    indent = " " * indentation

    # Générer les instructions rank=same
    sortie = "\n" + indent + "// Alignement des nœuds par niveau\n"

    # Trier les niveaux numériquement
    for niveau, noeuds in sorted(noeuds_par_niveau.items(), key=lambda x: int(x[0]) if x[0].isdigit() else 99):
        if noeuds:  # S'il y a des nœuds pour ce niveau
            sortie += indent + "{ rank=same; "
            sortie += "; ".join(noeuds)
            sortie += "; }\n"

    return sortie

def formater_attributs(attributs):
    # Filtrer les attributs non vides et les formater
    formatted_attrs = []
    for attr, value in attributs.items():
        if value:  # Ne pas inclure les attributs vides
            # Ajouter des guillemets pour les valeurs textuelles (sauf pour certains attributs)
            if attr in ('shape', 'style') or value.isdigit():
                formatted_attrs.append(f"{attr}={value}")
            else:
                formatted_attrs.append(f'{attr}="{value}"')

    # Gérer le cas spécial pour style=filled si nécessaire
    if 'style' not in attributs or not attributs['style']:
        formatted_attrs.append("style=filled")

    return formatted_attrs

def main(fichier_entree, fichier_sortie):
    # Charger le graphe DOT avec NetworkX
    try:
        schema = read_dot(fichier_entree)
    except Exception as e:
        logging.error(f"Erreur de lecture DOT : {e}", exc_info=True)
        print(f"Erreur à l'ouverture du fichier DOT : {e}")
        return


    sortie = """digraph Hierarchie_Composants_Electroniques_Simplifiee {
    // Configuration globale
    graph [compound="True", rankdir="TB", ranksep="10.0"]
    node [fontname="Arial", shape=box, style=filled];
    edge [fontname="Arial", fontsize=10, style=filled];
"""

    # Subgraph ASSEMBLAGE
    sortie += """
    // Niveau Assemblage
    subgraph cluster_assemblage {
        label="ASSEMBLAGE";
        bgcolor="#f0f0f0";
        node [fillcolor="#a0d6ff"];
"""
    # Ajout des informations pour tous les nœudss de niveau 0
    sortie += formater_noeuds_par_niveau(schema, "0")
    sortie += formater_noeuds_par_niveau(schema, "1000")
    sortie += """    }"""

    # Subgraph COMPOSANTS
    sortie += """
    // Niveau Composants
    subgraph cluster_composants {
        label="Composants";
        bgcolor="#f0f0f0";
        node [fillcolor="#a0d6ff"];"""
        # Ajout des informations pour tous les nœudss de niveau 1
    sortie += formater_noeuds_par_niveau(schema, "1")
    sortie += formater_noeuds_par_niveau(schema, "1001")
    sortie += """    }"""

    # Subgraph MATÉRIAUX
    sortie += """
    // Niveau Matériaux
    subgraph cluster_materiaux {
        label="Matériaux";
        bgcolor="#f0f0f0";
        node [fillcolor="#a0d6ff"];"""
    # Ajout des informations pour tous les nœudss de niveau 2
    sortie += formater_noeuds_par_niveau(schema, "2")
    sortie += """    }"""

        # Subgraph PAYS GÉOGRAPHIQUES
    sortie += """
    // Niveau Pays géographiques
    subgraph cluster_pays_geographiques {
        label="Pays géographiques";
        bgcolor="#f0f0f0";
        node [fillcolor="#a0d6ff"];"""
    # Ajout des informations pour tous les nœudss de niveau 99
    sortie += formater_noeuds_par_niveau(schema, "99")
    sortie += """    }"""

    # Ajouter les instructions rank=same pour l'alignement horizontal
    sortie += generer_rank_same(schema)
    sortie += """
    // Légende
    subgraph cluster_legende {
        label="LÉGENDE";
        bgcolor="white";
        node [shape=box, style=filled, width=0.2, height=0.2];
        L1 [label="Fabrication des wafers", fillcolor="#f0fff0"];
        L2 [label="Assemblage", fillcolor="#a0d6ff"];
        L3 [label="Niveau Composant", fillcolor="#b3ffe0"];
        L4 [label="Fabrication N3 (Matériaux)", fillcolor="#ffe0cc"];
        L5 [label="Fabrication N3 (Terres Rares)", fillcolor="#ffd699"];
        L6 [label="Extraction/Réserves", fillcolor="#ffd699"];
        L7 [label="Pays/Zone geographique", fillcolor="#e6f2ff"];
        L8 [label="Entreprise", fillcolor="#f0f0f0"];
        L9 [label="Liens réserves", color="red", fontcolor="white"];
        L10 [label="Liens opération(traitement, fabrication, assemblage)", color="purple", fontcolor="white"];
        L11 [label="Liens extraction", color="orange", fontcolor="white"];
        L12 [label="Liens d'origine géographique", color="darkgreen", fontcolor="white"];
        L13 [label="Liens origine des minerais", color="darkblue", fontcolor="white"];
    }
}
        """

    try:
        with open(f"{fichier_sortie}", "w", encoding="utf-8") as f:
            print(f"{sortie}", file=f)
    except FileNotFoundError:
        print(f"Erreur : Le chemin vers '{fichier_sortie}' n'existe pas")
    except PermissionError:
        print(f"Erreur : Permissions insuffisantes pour écrire dans '{fichier_sortie}'")
    except IOError as e:
        print(f"Erreur d'E/S lors de l'écriture dans le fichier : {e}")
    except Exception as e:
        print(f"Erreur inattendue : {e}")
    else:
        print(f"Écriture dans '{fichier_sortie}' réussie")

if len(sys.argv) != 3:
    print("Usage: python script.py fichier_entree.dot fichier_sortie.dot")
else:
    fichier_entree = sys.argv[1]
    fichier_sortie = sys.argv[2]
    main(fichier_entree, fichier_sortie)
