"""
Script permettant de générer une carte SVG de l'API de ScoDoc

Écrit par Matthias HARTMANN
"""

import re
import sys
import unicodedata
import xml.etree.ElementTree as ET

from flask import render_template

from app.auth.models import Permission


class COLORS:
    """
    Couleurs utilisées pour les éléments de la carte
    """

    BLUE = "rgb(114,159,207)"  # Couleur de base / élément simple
    GREEN = "rgb(165,214,165)"  # Couleur route GET / valeur query
    PINK = "rgb(230,156,190)"  # Couleur route POST
    GREY = "rgb(224,224,224)"  # Couleur séparateur


class Token:
    """
    Classe permettant de représenter un élément de l'API
    Exemple :

    /ScoDoc/api/test

    Token(ScoDoc)-> Token(api) -> Token(test)

    Chaque token peut avoir des enfants (Token) et des paramètres de query
    Chaque token dispose
        d'un nom (texte écrit dans le rectangle),
        d'une méthode (GET ou POST) par défaut GET,
        d'une func_name (nom de la fonction associée à ce token)
        [OPTIONNEL] d'une query (dictionnaire {param: <type:nom_param>})

    Un token est une leaf si il n'a pas d'enfants.
        Une LEAF possède un `?` renvoyant vers la doc de la route

    Il est possible de forcer un token à être une pseudo LEAF en mettant force_leaf=True
        Une PSEUDO LEAF possède aussi un `?` renvoyant vers la doc de la route
        tout en ayant des enfants.
    """

    def __init__(self, name, method="GET", query=None, leaf=False):
        self.children: list["Token"] = []
        self.name: str = name
        self.method: str = method
        self.query: dict[str, str] = query or {}
        self.force_leaf: bool = leaf
        self.func_name = ""

    def add_child(self, child):
        """
        Ajoute un enfant à ce token
        """
        self.children.append(child)

    def find_child(self, name):
        """
        Renvoie l'enfant portant le nom `name` ou None si aucun enfant ne correspond
        """
        for child in self.children:
            if child.name == name:
                return child
        return None

    def __repr__(self, level=0):
        """
        représentation textuelle simplifiée de l'arbre
        (ne prend pas en compte les query, les méthodes, les func_name, ...)
        """
        ret = "\t" * level + f"({self.name})\n"
        for child in self.children:
            ret += child.__repr__(level + 1)
        return ret

    def is_leaf(self):
        """
        Renvoie True si le token est une leaf, False sinon
        (i.e. s'il n'a pas d'enfants)
        (force_leaf n'est pas pris en compte ici)
        """
        return len(self.children) == 0

    def get_height(self, y_step):
        """
        Renvoie la hauteur de l'élément en prenant en compte la hauteur de ses enfants
        """
        # Calculer la hauteur totale des enfants
        children_height = sum(child.get_height(y_step) for child in self.children)

        # Calculer la hauteur des éléments de la query
        query_height = len(self.query) * (y_step * 1.33)

        # La hauteur totale est la somme de la hauteur des enfants et des éléments de la query
        height = children_height + query_height
        if height == 0:
            height = y_step
        return height

    def to_svg_group(
        self,
        x_offset: int = 0,
        y_offset: int = 0,
        x_step: int = 150,
        y_step: int = 50,
        parent_coords: tuple[tuple[int, int], tuple[int, int]] = None,
        parent_children_nb: int = 0,
    ):
        """
        Transforme un token en un groupe SVG
        (récursif, appelle la fonction sur ses enfants)
        """

        group = ET.Element("g")  # groupe principal
        color = COLORS.BLUE
        if self.is_leaf():
            if self.method == "GET":
                color = COLORS.GREEN
            elif self.method == "POST":
                color = COLORS.PINK

        # Création du rectangle avec le nom du token et placement sur la carte
        element = _create_svg_element(self.name, color)
        element.set("transform", f"translate({x_offset}, {y_offset})")
        group.append(element)

        # On récupère les coordonnées de début et de fin de l'élément pour les flèches
        current_start_coords, current_end_coords = _get_anchor_coords(
            element, x_offset, y_offset
        )
        # Préparation du lien vers la doc de la route
        href = "#" + self.func_name
        if self.query and not href.endswith("-query"):
            href += "-query"
        question_mark_group = _create_question_mark_group(current_end_coords, href)

        # Ajout de la flèche partant du parent jusqu'à l'élément courant
        if parent_coords and parent_children_nb > 1:
            arrow = _create_arrow(parent_coords, current_start_coords)
            group.append(arrow)

        # Ajout du `/` si le token n'est pas une leaf (ne prend pas en compte force_leaf)
        if not self.is_leaf():
            slash_group = _create_svg_element("/", COLORS.GREY)
            slash_group.set(
                "transform",
                f"translate({x_offset + _get_element_width(element)}, {y_offset})",
            )
            group.append(slash_group)
        # Ajout du `?` si le token est une leaf et possède une query
        if self.is_leaf() and self.query:
            slash_group = _create_svg_element("?", COLORS.GREY)
            slash_group.set(
                "transform",
                f"translate({x_offset + _get_element_width(element)}, {y_offset})",
            )
            group.append(slash_group)

        # Actualisation des coordonnées de fin
        current_end_coords = _get_anchor_coords(group, 0, 0)[1]

        # Gestion des éléments de la query
        # Pour chaque élément on va créer :
        # (param) (=) (valeur) (&)
        query_y_offset = y_offset
        query_sub_element = ET.Element("g")
        for key, value in self.query.items():
            # Création d'un sous-groupe pour chaque élément de la query
            sub_group = ET.Element("g")

            # On décale l'élément de la query vers la droite par rapport à l'élément parent
            translate_x = x_offset + _get_group_width(group) + x_step // 2

            # création élément (param)
            param_el = _create_svg_element(key, COLORS.BLUE)
            param_el.set(
                "transform",
                f"translate({translate_x}, {query_y_offset})",
            )
            sub_group.append(param_el)

            # Ajout d'une flèche partant de l'élément "query" vers le paramètre courant
            coords = (
                current_end_coords,
                _get_anchor_coords(param_el, translate_x, query_y_offset)[0],
            )
            sub_group.append(_create_arrow(*coords))

            # création élément (=)
            equal_el = _create_svg_element("=", COLORS.GREY)
            # On met à jour le décalage en fonction de l'élément précédent
            translate_x += _get_element_width(param_el)
            equal_el.set(
                "transform",
                f"translate({translate_x}, {query_y_offset})",
            )
            sub_group.append(equal_el)

            # création élément (value)
            value_el = _create_svg_element(value, COLORS.GREEN)
            # On met à jour le décalage en fonction des éléments précédents
            translate_x += _get_element_width(equal_el)
            value_el.set(
                "transform",
                f"translate({translate_x}, {query_y_offset})",
            )
            sub_group.append(value_el)
            # Si il y a qu'un seul élément dans la query, on ne met pas de `&`
            if len(self.query) == 1:
                query_sub_element.append(sub_group)
                continue

            # création élément (&)
            ampersand_group = _create_svg_element("&", "rgb(224,224,224)")
            # On met à jour le décalage en fonction des éléments précédents
            translate_x += _get_element_width(value_el)
            ampersand_group.set(
                "transform",
                f"translate({translate_x}, {query_y_offset})",
            )
            sub_group.append(ampersand_group)

            # On décale le prochain élément de la query vers le bas
            query_y_offset += y_step * 1.33
            # On ajoute le sous-groupe (param = value &) au groupe de la query
            query_sub_element.append(sub_group)

        # On ajoute le groupe de la query à l'élément principal
        group.append(query_sub_element)

        # Gestion des enfants du Token

        # On met à jour les décalages en fonction des éléments précédents
        y_offset = query_y_offset
        current_y_offset = y_offset

        # Pour chaque enfant, on crée un groupe SVG de façon récursive
        for child in self.children:
            # On décale l'enfant vers la droite par rapport à l'élément parent
            # Si il n'y a qu'un enfant, alors on colle l'enfant à l'élément parent
            rel_x_offset = x_offset + _get_group_width(group)
            if len(self.children) > 1:
                rel_x_offset += x_step

            # On crée le groupe SVG de l'enfant
            child_group = child.to_svg_group(
                rel_x_offset,
                current_y_offset,
                x_step,
                y_step,
                parent_coords=current_end_coords,
                parent_children_nb=len(self.children),
            )
            # On ajoute le groupe de l'enfant au groupe principal
            group.append(child_group)
            # On met à jour le décalage Y en fonction de la hauteur de l'enfant
            current_y_offset += child.get_height(y_step)

        # Ajout du `?` si le token est une pseudo leaf ou une leaf
        if self.force_leaf or self.is_leaf():
            group.append(question_mark_group)

        return group


def strip_accents(s):
    """Retourne la chaîne s séparant les accents et les caractères de base."""
    return "".join(
        c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn"
    )


def _create_svg_element(text, color="rgb(230,156,190)"):
    """
    Fonction générale pour créer un élément SVG simple
    (rectangle avec du texte à l'intérieur)

    text : texte à afficher dans l'élément
    color : couleur de l'élément
    """

    # Paramètres de style de l'élément
    padding = 5
    font_size = 16
    rect_height = 30
    rect_x = 10
    rect_y = 20

    # Estimation de la largeur du texte
    text_width = (
        len(text) * font_size * 0.6
    )  # On suppose que la largeur d'un caractère est 0.6 * font_size
    # Largeur du rectangle = Largeur du texte + padding à gauche et à droite
    rect_width = text_width + padding * 2

    # Création du groupe SVG
    group = ET.Element("g")

    # Création du rectangle
    ET.SubElement(
        group,
        "rect",
        {
            "x": str(rect_x),
            "y": str(rect_y),
            "width": str(rect_width),
            "height": str(rect_height),
            "style": f"fill:{color};stroke:black;stroke-width:2;fill-opacity:1;stroke-opacity:1",
        },
    )

    # Création du texte
    text_element = ET.SubElement(
        group,
        "text",
        {
            "x": str(rect_x + padding),
            "y": str(
                rect_y + rect_height / 2 + font_size / 2.5
            ),  # Ajustement pour centrer verticalement
            "font-family": "Courier New, monospace",
            "font-size": str(font_size),
            "fill": "black",
            "style": "white-space: pre;",
        },
    )

    # Ajout du texte à l'élément
    text_element.text = text

    return group


def _get_anchor_coords(element, x_offset, y_offset):
    """
    Récupération des coordonnées des points d'ancrage d'un élément SVG
    (début et fin de l'élément pour les flèches)
    (le milieu vertical de l'élément est utilisé pour les flèches)
    """
    bbox = _get_bbox(element, x_offset, y_offset)
    startX = bbox["x_min"]
    endX = bbox["x_max"]
    # Milieu vertical de l'élément
    y = bbox["y_min"] + (bbox["y_max"] - bbox["y_min"]) / 2
    return (startX, y), (endX, y)


def _create_arrow(start_coords, end_coords):
    """
    Création d'une flèche entre deux points
    """
    # On récupère les coordonnées de début et de fin de la flèche
    start_x, start_y = start_coords
    end_x, end_y = end_coords
    # On calcule le milieu horizontal de la flèche
    mid_x = (start_x + end_x) / 2

    # On crée le chemin de la flèche
    path_data = (
        f"M {start_x},{start_y} L {mid_x},{start_y} L {mid_x},{end_y} L {end_x},{end_y}"
    )
    # On crée l'élément path de la flèche
    path = ET.Element(
        "path",
        {
            "d": path_data,
            "style": "stroke:black;stroke-width:2;fill:none",
            "marker-end": "url(#arrowhead)",  # Ajout de la flèche à la fin du path
        },
    )
    return path


def _get_element_width(element):
    """
    Retourne la largueur d'un élément simple
    L'élément simple correspond à un rectangle avec du texte à l'intérieur
    on récupère la largueur du rectangle
    """
    rect = element.find("rect")
    if rect is not None:
        return float(rect.get("width", 0))
    return 0


def _get_group_width(group):
    """
    Récupère la largeur d'un groupe d'éléments
    on fait la somme des largeurs de chaque élément du groupe
    """
    return sum(_get_element_width(child) for child in group)


def _create_question_mark_group(coords, href):
    """
    Création d'un groupe SVG contenant un cercle et un lien vers la doc de la route
    le `?` renvoie vers la doc de la route
    """
    # Récupération du point d'ancrage de l'élément
    x, y = coords
    radius = 10  # Rayon du cercle
    y -= radius * 2
    font_size = 17  # Taille de la police

    group = ET.Element("g")

    # Création du cercle
    ET.SubElement(
        group,
        "circle",
        {
            "cx": str(x),
            "cy": str(y),
            "r": str(radius),
            "fill": COLORS.GREY,
            "stroke": "black",
            "stroke-width": "2",
        },
    )

    # Création du lien (a) vers la doc de la route
    link = ET.Element("a", {"href": href})

    # Création du texte `?`
    text_element = ET.SubElement(
        link,
        "text",
        {
            "x": str(x + 1),
            "y": str(y + font_size / 3),  # Ajustement pour centrer verticalement
            "text-anchor": "middle",  # Centrage horizontal
            "font-family": "Arial",
            "font-size": str(font_size),
            "fill": "black",
        },
    )
    text_element.text = "?"

    group.append(link)

    return group


def analyze_api_routes(app, endpoint_start: str) -> tuple:
    """Parcours de toutes les routes de l'application
    analyse docstrings
    """
    # Création du token racine
    api_map = Token("")

    doctable_lines: dict[str, dict] = {}

    for rule in app.url_map.iter_rules():
        # On ne garde que les routes de l'API / APIWEB
        if not rule.endpoint.lower().startswith(endpoint_start.lower()):
            continue

        # Transformation de la route en segments
        # ex : /ScoDoc/api/test -> ["ScoDoc", "api", "test"]
        segments = rule.rule.strip("/").split("/")

        # On positionne le token courant sur le token racine
        current_token = api_map

        # Récupération de la fonction associée à la route
        func = app.view_functions[rule.endpoint]
        doc_dict = _parse_doc_string(func.__doc__ or "")
        func_name = doc_dict.get("DOC_ANCHOR", [None])[0] or func.__name__
        # Pour chaque segment de la route
        for i, segment in enumerate(segments):
            # On cherche si le segment est déjà un enfant du token courant
            child = current_token.find_child(segment)

            # Si ce n'est pas le cas on crée un nouveau token et on l'ajoute comme enfant
            if child is None:
                # Si c'est le dernier segment, on marque le token comme une leaf
                # On utilise force_leaf car il est possible que le token ne soit que
                # momentanément une leaf
                # ex :
                #   - /ScoDoc/api/test/ -> ["ScoDoc", "api", "test"]
                #   - /ScoDoc/api/test/1 -> ["ScoDoc", "api", "test", "1"]
                # dans le premier cas test est une leaf, dans le deuxième cas test n'est pas une leaf
                # force_leaf permet de forcer le token à être une leaf même s'il a des enfants
                # permettant d'afficher le `?` renvoyant vers la doc de la route
                # car la route peut être utilisée sans forcément la continuer.

                if i == len(segments) - 1:
                    # Un Token sera query si parse_query_doc retourne un dictionnaire non vide
                    child = Token(
                        segment,
                        leaf=True,
                        query=parse_query_doc(func.__doc__ or ""),
                    )
                else:
                    child = Token(
                        segment,
                    )

                # On ajoute le token comme enfant du token courant
                # en donnant la méthode et le nom de la fonction associée
                child.func_name = func_name
                method: str = "POST" if "POST" in rule.methods else "GET"
                child.method = method
                current_token.add_child(child)

                href = func_name
                if child.query and not href.endswith("-query"):
                    href += "-query"

                # category

                category: str = func.__module__.replace("app.api.", "")
                mod_doc: str = sys.modules[func.__module__].__doc__ or ""
                mod_doc_dict: dict = _parse_doc_string(mod_doc)
                if mod_doc_dict.get("CATEGORY"):
                    category = mod_doc_dict["CATEGORY"][0].strip()

                permissions: str
                try:
                    permissions: str = ", ".join(
                        sorted(Permission.permissions_names(func.scodoc_permission))
                    )
                except AttributeError:
                    permissions = "Aucune permission requise"

                if func_name not in doctable_lines:
                    doctable_lines[func_name] = {
                        "method": method,
                        "nom": func_name,
                        "href": href,
                        "query": doc_dict.get("QUERY", "") != "",
                        "permission": permissions,
                        "description": doc_dict.get("", ""),
                        "params": doc_dict.get("PARAMS", ""),
                        "category": doc_dict.get("CATEGORY", [False])[0] or category,
                        "samples": doc_dict.get("SAMPLES"),
                    }

            # On met à jour le token courant pour le prochain segment
            current_token = child
        if func_name in doctable_lines:  # endpoint déjà ajouté, ajoute au besoin route
            doctable_lines[func_name]["routes"] = doctable_lines[func_name].get(
                "routes", []
            ) + [rule.rule]
    return api_map, doctable_lines


# point d'entrée de la commande `flask gen-api-map`
def gen_api_map(api_map: Token, doctable_lines: dict[str, dict]) -> str:
    """
    Fonction permettant de générer une carte SVG de l'API de ScoDoc
    Elle récupère les routes de l'API et les transforme en un arbre de Token
    puis génère un fichier SVG à partir de cet arbre
    """

    # On génère le SVG à partir de l'arbre de Token
    generate_svg(api_map.to_svg_group(), "/tmp/api_map.svg")
    print(
        "La carte a été générée avec succès. "
        + "Vous pouvez la consulter à l'adresse suivante : /tmp/api_map.svg"
    )

    # On génère le tableau à partir de doctable_lines
    table = _gen_table(sorted(doctable_lines.values(), key=lambda x: x["nom"]))
    _write_gen_table(table)
    return table


def _get_bbox(element, x_offset=0, y_offset=0):
    """
    Récupérer les coordonnées de la boîte englobante d'un élément SVG
    Utilisé pour calculer les coordonnées d'un élément SVG et pour avoir la taille
    total du SVG
    """
    # Initialisation des coordonnées de la boîte englobante
    bbox = {
        "x_min": float("inf"),
        "y_min": float("inf"),
        "x_max": float("-inf"),
        "y_max": float("-inf"),
    }

    # Parcours récursif des enfants de l'élément
    for child in element:
        # On récupère la transformation (position) de l'enfant
        # On met les OffSet par défaut à leur valeur donnée en paramètre
        transform = child.get("transform")
        child_x_offset = x_offset
        child_y_offset = y_offset

        # Si la transformation est définie, on récupère les coordonnées de translation
        # et on les ajoute aux offsets
        if transform:
            translate = transform.replace("translate(", "").replace(")", "").split(",")
            if len(translate) == 2:
                child_x_offset += float(translate[0])
                child_y_offset += float(translate[1])

        # On regarde ensuite la boite englobante de l'enfant
        # On met à jour les coordonnées de la boîte englobante en fonction de l'enfant
        # x_min, y_min, x_max, y_max.

        if child.tag == "rect":
            x = child_x_offset + float(child.get("x", 0))
            y = child_y_offset + float(child.get("y", 0))
            width = float(child.get("width", 0))
            height = float(child.get("height", 0))
            bbox["x_min"] = min(bbox["x_min"], x)
            bbox["y_min"] = min(bbox["y_min"], y)
            bbox["x_max"] = max(bbox["x_max"], x + width)
            bbox["y_max"] = max(bbox["y_max"], y + height)

        if len(child):
            child_bbox = _get_bbox(child, child_x_offset, child_y_offset)
            bbox["x_min"] = min(bbox["x_min"], child_bbox["x_min"])
            bbox["y_min"] = min(bbox["y_min"], child_bbox["y_min"])
            bbox["x_max"] = max(bbox["x_max"], child_bbox["x_max"])
            bbox["y_max"] = max(bbox["y_max"], child_bbox["y_max"])

    return bbox


def generate_svg(element, fname):
    """
    Génère un fichier SVG à partir d'un élément SVG
    """
    # On récupère les dimensions de l'élément racine
    bbox = _get_bbox(element)
    # On définit la taille du SVG en fonction des dimensions de l'élément racine
    width = bbox["x_max"] - bbox["x_min"] + 80
    height = bbox["y_max"] - bbox["y_min"] + 80

    # Création de l'élément racine du SVG
    svg = ET.Element(
        "svg",
        {
            "width": str(width),
            "height": str(height),
            "xmlns": "http://www.w3.org/2000/svg",
            "viewBox": f"{bbox['x_min'] - 10} {bbox['y_min'] - 10} {width} {height}",
        },
    )

    # Création du motif de la flèche pour les liens
    # (définition d'un marqueur pour les flèches)
    defs = ET.SubElement(svg, "defs")
    marker = ET.SubElement(
        defs,
        "marker",
        {
            "id": "arrowhead",
            "markerWidth": "10",
            "markerHeight": "7",
            "refX": "10",
            "refY": "3.5",
            "orient": "auto",
        },
    )
    ET.SubElement(marker, "polygon", {"points": "0 0, 10 3.5, 0 7"})

    # Ajoute un décalage vertical pour avoir un peu de padding en haut
    element.set("transform", "translate(0, 10)")

    # Ajout de l'élément principal à l'élément racine
    svg.append(element)

    # Écriture du fichier SVG
    tree = ET.ElementTree(svg)
    tree.write(fname, encoding="utf-8", xml_declaration=True)


def _parse_doc_string(doc_string: str) -> dict[str, list[str]]:
    """Parse doc string and extract a dict:
    {
        "" : description_lines,
        "keyword" : lines
    }

    In the docstring, each keyword is associated to a section like

    KEYWORD
    -------
    ...
    (blank line)

    All non blank lines not associated to a keyword go to description.
    """
    doc_dict = {}
    matches = re.finditer(
        r"^\s*(?P<kw>[A-Z_\-]+)$\n^\s*-+\n(?P<txt>(^(?!\s*$).+$\n?)+)",
        doc_string,
        re.MULTILINE,
    )
    description = ""
    i = 0
    for match in matches:
        start, end = match.span()
        description += doc_string[i:start]
        doc_dict[match.group("kw")] = [
            x.strip() for x in match.group("txt").split("\n") if x.strip()
        ]
        i = end

    description += doc_string[i:]
    doc_dict[""] = description.split("\n")
    return doc_dict


def _get_doc_lines(keyword, doc_string: str) -> list[str]:
    """
    Renvoie les lignes de la doc qui suivent le mot clé keyword
    Attention : s'arrête à la première ligne vide

    La doc doit contenir des lignes de la forme:

    KEYWORD
    -------
    ...

    """
    # Récupérer les lignes de la doc
    lines = [line.strip() for line in doc_string.split("\n")]
    # On cherche la ligne "KEYWORD" et on vérifie que la ligne suivante est "-----"
    # Si ce n'est pas le cas, on renvoie un dictionnaire vide
    try:
        kw_index = lines.index(keyword)
        kw_line = "-" * len(keyword)
        if lines[kw_index + 1] != kw_line:
            return []
    except ValueError:
        return []
    # On récupère les lignes de la doc qui correspondent au keyword (enfin on espère)
    kw_lines = lines[kw_index + 2 :]

    # On s'arrête à la première ligne vide
    first_empty_line: int
    try:
        first_empty_line: int = kw_lines.index("")
    except ValueError:
        first_empty_line = len(kw_lines)
    kw_lines = kw_lines[:first_empty_line]
    return kw_lines


def parse_query_doc(doc_string: str) -> dict[str, str]:
    """
    renvoie un dictionnaire {param: <type:nom_param>} (ex: {assiduite_id : <int:assiduite_id>})

    La doc doit contenir des lignes de la forme:

    QUERY
    -----
    param:<string:nom_param>
    param1:<int:num>
    param2:<array[string]:array_nom>

    Dès qu'une ligne ne respecte pas ce format (voir regex dans la fonction), on arrête de parser
    Attention, la ligne ----- doit être collée contre QUERY et contre le premier paramètre
    """
    query_lines: list[str] = _get_doc_lines("QUERY", doc_string)

    query = {}
    regex = re.compile(r"^(\w+):(<.+>)$")
    for line in query_lines:
        # On verifie que la ligne respecte le format attendu
        # Si ce n'est pas le cas, on arrête de parser
        parts = regex.match(line)
        if not parts:
            break
        # On récupère le paramètre et son type:nom
        param, type_nom_param = parts.groups()
        # On ajoute le paramètre au dictionnaire
        query[param] = type_nom_param

    return query


def _gen_table_line(doctable: dict = None):
    """
    Génère une ligne de tableau markdown

    | nom de la route| methode HTTP| Permission |
    """

    nom, method, permission = (
        doctable.get("nom", ""),
        doctable.get("method", ""),
        doctable.get("permission", ""),
    )

    if doctable is None:
        doctable = {}

    lien: str = doctable.get("href", nom)

    doctable["query"]: bool
    if doctable.get("query") and not lien.endswith("-query"):
        lien += "-query"

    nav: str = f"[{nom}]({'#'+lien})"

    table: str = "|"
    for string in [nav, method, doctable.get("permissions") or permission]:
        table += f" {string} |"

    return table


def _gen_table_head() -> str:
    """
    Génère la première ligne du tableau markdown
    """

    headers: str = "| Route | Méthode | Permission |"
    line: str = "|---|---|---|"

    return f"{headers}\n{line}\n"


def _gen_table(lines: list[dict]) -> str:
    """
    Génère un tableau markdown à partir d'une liste de lignes

    lines : liste de dictionnaire au format doc_lines.
    """
    table = _gen_table_head()
    table += "\n".join([_gen_table_line(line) for line in lines])
    return table


def _gen_csv_line(doc_line: dict) -> str:
    """
    Génère les lignes de tableau csv en fonction d'une route (doc_line)

    format :
    "entry_name";"url";"permission";"method";"content"
    """

    entry_name: str = doc_line.get("nom", "")
    method: str = doc_line.get("method", "GET")
    permission: str = (
        "UsersAdmin" if doc_line.get("permission") != "ScoView" else "ScoView"
    )

    samples: list[str] = doc_line.get("samples", [])
    csv_lines: list[str] = []
    for sample in samples:
        fragments = sample.split(";", maxsplit=1)
        if len(fragments) == 2:
            url, content = fragments
        elif len(fragments) == 1:
            url, content = fragments[0], ""
        else:
            raise ValueError(f"Error: sample invalide: {sample}")
        csv_line = f'"{entry_name}";"{url}";"{permission}";"{method}";'
        if content:
            csv_line += f'"{content}"'
        csv_lines.append(csv_line)

    return "\n".join(csv_lines)


def _gen_csv(lines: list[dict], filename: str = "/tmp/samples.csv") -> str:
    """
    Génère un fichier csv à partir d'une liste de lignes

    lines : liste de dictionnaire au format doc_lines.
    """
    csv = '"entry_name";"url";"permission";"method";"content"\n'
    csv += "\n".join(
        [_gen_csv_line(line) for line in lines if line.get("samples") is not None]
    )

    with open(filename, "w", encoding="UTF-8") as f:
        f.write(csv)

    print(
        f"Les samples ont été générés avec succès. Vous pouvez le consulter à l'adresse suivante : {filename}"
    )


def _write_gen_table(table: str, filename: str = "/tmp/api_table.md"):
    """Ecriture du fichier md avec la table"""
    with open(filename, "w", encoding="UTF-8") as f:
        f.write(table)
    print(
        f"Le tableau a été généré avec succès. Vous pouvez le consulter à l'adresse suivante : {filename}"
    )


def doc_route(doctable: dict) -> str:
    """Generate markdown doc for a route"""
    jinja_obj: dict = {}
    jinja_obj.update(doctable)
    jinja_obj["nom"] = doctable["nom"].strip()  # on retire les caractères blancs

    if doctable.get("samples") is not None:
        jinja_obj["sample"] = {
            "nom": f"{jinja_obj['nom']}.json",
            "href": f"{jinja_obj['nom']}.json.md",
        }

    jinja_obj["query"]: bool
    if jinja_obj["query"]:
        jinja_obj["nom"] += "(-query)"

    if doctable.get("params"):
        jinja_obj["params"] = []
        for param in doctable["params"]:
            frags = param.split(":", maxsplit=1)
            if len(frags) == 2:
                name, descr = frags
                jinja_obj["params"].append(
                    {"nom": name.strip(), "description": descr.strip()}
                )
            else:
                print(f"Warning: {doctable['nom']} : invalid PARAMS {param}")
    if doctable.get("description"):
        descr = "\n".join(s for s in doctable["description"])
        jinja_obj["description"] = descr.strip()

    return render_template("doc/apidoc.j2", doc=jinja_obj)


def gen_api_doc(app, endpoint_start="api."):
    "commande gen-api-doc"
    api_map, doctable_lines = analyze_api_routes(app, endpoint_start)

    mddoc: str = ""

    categories: dict = {}
    for value in doctable_lines.values():
        category = value["category"].capitalize()
        if category not in categories:
            categories[category] = []
        categories[category].append(value)

    # sort categories by name
    categories: dict = dict(
        sorted(categories.items(), key=lambda x: strip_accents(x[0]))
    )

    category: str
    routes: list[dict]
    for category, routes in categories.items():
        # sort routes by name
        routes.sort(key=lambda x: strip_accents(x["nom"]))

        mddoc += f"### API {category.capitalize()}\n\n"
        for route in routes:
            mddoc += doc_route(route)
            mddoc += "\n\n"

    table_api = gen_api_map(api_map, doctable_lines)
    mdpage = render_template("doc/ScoDoc9API.j2", doc_api=mddoc, table_api=table_api)
    _gen_csv(list(doctable_lines.values()))

    fname = "/tmp/ScoDoc9API.md"
    with open(fname, "w", encoding="utf-8") as f:
        f.write(mdpage)
    print(
        f"""La documentation API a été générée avec succès.
Vous pouvez la consulter à l'adresse suivante : {fname}.
Vous pouvez maintenant générer les samples avec `tools/test_api.sh --make-samples`.
"""
    )