#!/usr/bin/env python3
"""
Point d'entrée du générateur de documents (chantier JOS-69).

Appelé par `App\\Services\\Reporting\\ReportGenerator` :

    python3 reporter/render.py --format pptx --out /chemin/rapport.pptx  < payload.json

Le payload arrive sur **l'entrée standard** et le seul écrit sur la sortie
standard est le chemin du fichier produit. Tout le reste — avertissements,
traces — part sur la sortie d'erreur, pour que l'appelant puisse lire la sortie
standard sans la nettoyer.

**Un rapport est une composition de blocs**, décrite par la clé `blocks` du
payload. Chaque bloc est rendu dans le même conteneur — une présentation, un
document, un classeur — puis l'ensemble est sauvegardé une fois. Sans la clé
`blocks`, on retombe sur le comportement d'origine : un planning couvrant tout
le périmètre, ce qui garde le chemin ClickUp historique en état de marche et
permet de tester le contrat de données sans passer par l'écran de composition.

Remplace `app.py` (interface Flask) et `main.py` (CLI interactive), qui ne sont
pas repris : choisir un périmètre, lancer, récupérer le fichier est désormais le
travail de Joseph.
"""

from __future__ import annotations

import argparse
import contextlib
import os
import sys
from dataclasses import replace

# Le dossier de ce fichier doit être dans le chemin d'import : Joseph l'appelle
# depuis la racine du projet, pas depuis `reporter/`.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from core.joseph_source import TYPE_BY_CODE, load, parse_date, parse_payload  # noqa: E402
from generators import dashboards, sections  # noqa: E402


def _default_blocks(payload: dict) -> list:
    """
    La composition implicite d'un payload sans `blocks` : un planning, sur tout
    le périmètre. C'est ce que produisait l'outil avant ce chantier.
    """
    return [{
        "type": "planning",
        "title": payload.get("scope", {}).get("label") or "Planning",
        "options": {"view": "gantt"},
    }]


def _page_mode(block, tasks, fallback):
    """
    Comment découper le planning en diapositives.

    Le choix de l'utilisateur commande — sauf dans un cas, où il ne veut rien
    dire : sur une liste **plate**, « une page par chantier » donne une page par
    ligne (mesuré à 22 diapositives d'une seule ligne sur le projet Joseph, quand
    le rapport est limité au premier niveau). On retombe alors sur le
    remplissage. Ce n'est pas contredire l'intention : une page par chantier n'a
    de sens que si un chantier a du contenu à montrer.
    """
    explicit = (block.get("options") or {}).get("page_mode") or fallback

    ids = set(task.id for task in tasks)
    has_parent_inside = any(task.parent_id in ids for task in tasks if task.parent_id)

    if explicit in ("chantier", "domain") and not has_parent_inside:
        return "fill"

    return explicit


def _render_planning(container, fmt, block, tasks, out, args, project_name=None):
    options = block.get("options") or {}
    scale = options.get("scale") or args.scale
    show_tasks = options.get("depth", "all") != "summary"
    title = block.get("title") or "Planning"
    page_mode = _page_mode(block, tasks, args.page_mode)

    # La période choisie devient la **fenêtre du planning**. Sans elle, les
    # générateurs la déduisent des dates des tâches et forcent aujourd'hui
    # dedans : le réglage de période n'avait alors aucun effet sur le Gantt, il
    # ne servait qu'aux listes d'éléments.
    block_range = block.get("range") or {}
    range_start = parse_date(block_range.get("from"))
    range_end = parse_date(block_range.get("to"))

    # **Le rendu dépend du format**, et non d'un réglage : le Gantt dessiné
    # demande une surface qu'un document A4 n'a pas, et un tableau daté de
    # cinquante colonnes n'a pas sa place sur une diapositive. Word et Excel
    # rendent donc le planning en tableau, PowerPoint en Gantt. C'est ce qui a
    # fait retirer l'option « Rendu » de l'écran, où elle proposait une
    # combinaison (« Gantt » + Word) qui n'existait pas.
    #
    # Import tardif et par format : charger python-pptx pour produire un Excel
    # coûterait lxml et Pillow pour rien, sur un hébergement où le temps
    # d'exécution est plafonné à 165 s.
    if fmt == "pptx":
        from generators.planning_pptx import generate_pptx

        generate_pptx(
            tasks, out,
            scale=scale,
            page_mode=page_mode,
            show_tasks=show_tasks,
            title=title,
            project_name=project_name,
            prs=container,
            save=False,
            range_start=range_start,
            range_end=range_end,
        )
    elif fmt == "docx":
        from generators.planning_docx import generate_gantt_table

        _docx_orientation(container, landscape=True)
        generate_gantt_table(
            tasks, out,
            title=title,
            scale=scale,
            range_start=range_start,
            range_end=range_end,
            doc=container,
            save=False,
            show_tasks=show_tasks,
        )
    else:
        from generators.planning_excel import generate_planning

        generate_planning(tasks, out, scale=scale, wb=container, save=False,
                          sheet_title=title[:31],
                          range_start=range_start, range_end=range_end)


def _docx_orientation(doc, landscape: bool):
    """
    Bascule le document en paysage — ou l'en fait revenir — **sans page vide**.

    Une nouvelle section n'est ouverte que si l'orientation change réellement.
    Sans cette garde, deux plannings consécutifs ouvriraient deux sections, donc
    une page blanche entre les deux ; et un rapport se terminant par un planning
    gagnerait une page portrait vide.
    """
    from docx.enum.section import WD_ORIENT, WD_SECTION
    from docx.shared import Cm

    current = doc.sections[-1]
    already = current.orientation == WD_ORIENT.LANDSCAPE

    if already == landscape:
        return

    section = doc.add_section(WD_SECTION.NEW_PAGE)

    if landscape:
        from generators.planning_docx import set_landscape

        set_landscape(section)
        return

    if section.page_width > section.page_height:
        section.page_width, section.page_height = section.page_height, section.page_width

    section.orientation = WD_ORIENT.PORTRAIT
    section.left_margin = Cm(1.8)
    section.right_margin = Cm(1.8)


def _new_container(fmt, title, subtitle=""):
    if fmt == "pptx":
        from core.design_kit import new_presentation

        return new_presentation()   # masque Oncopole — voir core/design_kit.py

    if fmt == "docx":
        # Styles, marges et page de garde posés ici, une fois : `generate_docx()`
        # ne les applique pas à un document qu'il n'a pas créé, sinon deux blocs
        # de planning produiraient deux couvertures.
        from generators.planning_docx import new_document

        return new_document(title, subtitle)

    from openpyxl import Workbook

    workbook = Workbook()
    # Le classeur neuf arrive avec une feuille vide dont aucun bloc ne veut :
    # chacun crée la sienne. La laisser produirait un onglet « Sheet » orphelin
    # en tête du fichier.
    workbook.remove(workbook.active)
    return workbook


def _tasks_for(block, tasks):
    """
    Les éléments que ce bloc couvre.

    Joseph envoie l'arbre entier dans `tasks[]` et, pour chaque bloc, la liste
    des identifiants qu'il montre — sélection et profondeur déjà résolues de
    l'autre côté. Refaire ce calcul ici demanderait de connaître la hiérarchie et
    les règles produit, alors que la question a déjà été tranchée.
    """
    ids = block.get("element_ids")

    if not ids:
        return tasks

    wanted = set(str(i) for i in ids)

    return [task for task in tasks if task.id in wanted]


def _tasks_for_types(block, tasks):
    """
    Les types d'éléments retenus par ce bloc — chantiers, jalons, tâches,
    réunions (retours du 25/09/2026). `options.types` porte les codes du
    contrat Joseph (`TYPE_BY_CODE`), vide ou absent valant « tous », même
    règle que `element_ids` ci-dessus.

    Appelé seulement pour les blocs Planning et Liste — un Focus a besoin de
    sa structure (chantiers et jalons y comptent toujours), et le
    Portefeuille ne travaille pas élément par élément.

    **Reparente plutôt que de couper net.** Exclure les chantiers ne doit pas
    faire disparaître les tâches qu'ils contenaient : `build_ordered_rows()`
    (planning_pptx.py, même logique côté docx/excel) construit sa hiérarchie
    en indexant les enfants par `parent_id`, et une tâche dont le chantier
    parent a été retiré de la liste — sans être rattachée à autre chose — ne
    se retrouve ni chantier-enfant ni orpheline : elle sort du document sans
    rien dire. Chaque tâche gardée remonte donc à son plus proche ancêtre lui
    aussi gardé (racine si aucun ne l'est), sur une **copie** — les `Task`
    de la liste d'origine sont partagés entre tous les blocs du rapport,
    les modifier en place contaminerait les blocs suivants.
    """
    codes = (block.get("options") or {}).get("types")

    if not codes:
        return tasks

    wanted = set(TYPE_BY_CODE[c] for c in codes if c in TYPE_BY_CODE)
    by_id = {task.id: task for task in tasks}

    def nearest_kept_ancestor(task):
        parent_id = task.parent_id
        while parent_id and parent_id in by_id:
            parent = by_id[parent_id]
            if parent.item_type in wanted:
                return parent_id
            parent_id = parent.parent_id
        return None

    return [replace(task, parent_id=nearest_kept_ancestor(task)) for task in tasks if task.item_type in wanted]


def _render_block(container, fmt, block, tasks, out, args, project_name=None, payload=None):
    kind = block.get("type")
    tasks = _tasks_for(block, tasks)

    if kind in ("planning", "elements"):
        tasks = _tasks_for_types(block, tasks)

    # Tout ce qui n'est pas un planning se lit en portrait : seul le tableau daté
    # a besoin de la largeur.
    if fmt == "docx" and kind != "planning":
        _docx_orientation(container, landscape=False)

    # Réservé au pptx : c'est le seul format où `project_name` a un endroit où
    # aller (le placeholder "contenu" du layout "Simple", voir `sections.py`)
    # — docx/xlsx n'ont pas cet équivalent, on ne leur invente pas un paramètre
    # qu'ils ignoreraient.
    extra = {"project_name": project_name} if fmt == "pptx" else {}

    if kind == "planning":
        _render_planning(container, fmt, block, tasks, out, args, **extra)
    elif kind == "elements":
        layout = (block.get("options") or {}).get("layout") or "table"

        if layout == "kanban":
            {"pptx": sections.render_kanban_pptx,
             "docx": sections.render_kanban_docx,
             "xlsx": sections.render_kanban_xlsx}[fmt](container, block, tasks, **extra)
        else:
            {"pptx": sections.render_elements_pptx,
             "docx": sections.render_elements_docx,
             "xlsx": sections.render_elements_xlsx}[fmt](container, block, tasks, **extra)
    elif kind == "focus" and fmt == "pptx":
        # Le tableau de bord de direction (retours du 23/09/2026) : PowerPoint
        # seulement — c'est le seul format où la mise en page porte le sens.
        # Word et Excel gardent la fiche détaillée ci-dessous.
        dashboards.render_focus(container, block, tasks, payload or {})
    elif kind == "portfolio" and fmt == "pptx":
        dashboards.render_portfolio(container, block, payload or {})
    elif kind == "focus":
        {"pptx": sections.render_focus_pptx,
         "docx": sections.render_focus_docx,
         "xlsx": sections.render_focus_xlsx}[fmt](container, block, tasks, **extra)
    else:
        # Un type déclaré côté Joseph mais pas rendu ici — `portfolio` hors
        # PowerPoint aujourd'hui. On le signale sans interrompre : le reste du rapport a
        # plus de valeur qu'un échec net.
        print("Bloc ignoré (type non rendu) : {!r}".format(kind), file=sys.stderr)


def main() -> int:
    parser = argparse.ArgumentParser(description="Génère un document depuis un payload Joseph.")
    parser.add_argument("--format", required=True, choices=("pptx", "docx", "xlsx"))
    parser.add_argument("--out", required=True, help="Chemin du fichier à produire")
    parser.add_argument("--title", default=None, help="Titre du document")
    parser.add_argument("--scale", default="semaine", help="Échelle par défaut du planning")
    parser.add_argument("--page-mode", default="chantier", choices=("chantier", "domain", "fill"))
    args = parser.parse_args()

    payload = load()
    tasks = parse_payload(payload)

    report = payload.get("report") or {}
    title = args.title or report.get("title") or payload.get("scope", {}).get("label") or "Rapport"
    blocks = payload.get("blocks") or _default_blocks(payload)

    # Toujours un seul projet aujourd'hui (`ReportPayload::build()` prend un
    # `$scope->project` obligatoire côté PHP) — absent, `project_name` reste
    # `None` et les placeholders concernés restent simplement vides.
    project_name = (payload.get("project") or {}).get("name")

    print("{} élément(s) retenu(s), {} bloc(s)".format(len(tasks), len(blocks)), file=sys.stderr)

    # Les générateurs écrivent leur propre compte rendu sur la sortie standard
    # (« ✓ PowerPoint généré → … »). On la détourne vers la sortie d'erreur le
    # temps de la génération, pour que la sortie standard ne porte **que** le
    # chemin du fichier. L'alternative aurait été de modifier les trois
    # générateurs — précisément ce qu'on cherche à éviter.
    with contextlib.redirect_stdout(sys.stderr):
        container = _new_container(args.format, title, report.get("range_label") or "")

        for block in blocks:
            _render_block(container, args.format, block, tasks, args.out, args, project_name, payload)

        os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
        container.save(args.out)

    if not os.path.isfile(args.out):
        print("Le générateur n'a produit aucun fichier.", file=sys.stderr)
        return 1

    # La seule chose sur la sortie standard : le chemin. L'appelant n'a rien à
    # analyser.
    print(args.out)
    return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:  # noqa: BLE001
        # Message court sur stderr plutôt qu'une trace de cent lignes : c'est ce
        # que Joseph remontera à l'utilisateur, et une trace y serait illisible.
        # La trace complète reste disponible en relançant à la main.
        print("{}: {}".format(type(exc).__name__, exc), file=sys.stderr)
        sys.exit(1)
