"""
generators/planning_docx.py
Génère un rapport Word (.docx) listant l'ensemble des chantiers, tâches et
sous-tâches des espaces/dossiers/listes sélectionnés.

Principe :
  - Les tâches sont groupées selon la hiérarchie ClickUp :
    Espace › Dossier (si présent) › Liste › Chantier/Tâche › Sous-tâche › ...
  - L'ordre d'affichage de ClickUp est conservé (ordre de récupération API,
    pas de tri alphabétique).
  - Chaque liste produit un tableau à 3 colonnes (Élément / Statut /
    Échéance) ; une ligne fusionnée sous chaque tâche affiche sa
    description quand elle existe.

Point d'entrée public : generate_docx(tasks, output_path, title=...)
"""

from __future__ import annotations  # PEP 585 (`list[Task]`) sans Python 3.9 : l'hébergement est en 3.7


from datetime import datetime

from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

from core.data_model import Task, TYPE_CHANTIER, TYPE_JALON


# ══════════════════════════════════════════════════════════════════
# PALETTE (cohérente avec les autres générateurs)
# ══════════════════════════════════════════════════════════════════

COLOR_BLUE      = RGBColor(0x23, 0x49, 0x64)
COLOR_BLUE_MID  = RGBColor(0x2E, 0x5F, 0x80)
COLOR_SLATE     = RGBColor(0x6B, 0x7A, 0x8D)
COLOR_TEXT      = RGBColor(0x1A, 0x27, 0x33)
COLOR_ORANGE    = RGBColor(0xF2, 0x97, 0x00)
COLOR_WHITE     = RGBColor(0xFF, 0xFF, 0xFF)
COLOR_SUCCESS   = RGBColor(0x1A, 0x6B, 0x4A)
COLOR_ERROR     = RGBColor(0x9E, 0x1A, 0x1A)

BG_HEADER   = "234964"
BG_CHANTIER = "EAF1F6"
BG_JALON    = "FFF3DC"


# ══════════════════════════════════════════════════════════════════
# Helpers XML (shading de cellule, bordures, répétition d'en-tête)
# ══════════════════════════════════════════════════════════════════

def _shade_cell(cell, hex_color: str):
    shd = OxmlElement("w:shd")
    shd.set(qn("w:val"), "clear")
    shd.set(qn("w:color"), "auto")
    shd.set(qn("w:fill"), hex_color)
    cell._tc.get_or_add_tcPr().append(shd)


def _repeat_header(row):
    tr = row._tr
    trPr = tr.get_or_add_trPr()
    tblHeader = OxmlElement("w:tblHeader")
    tblHeader.set(qn("w:val"), "true")
    trPr.append(tblHeader)


def _set_cell_margins(cell, top=40, bottom=40, left=80, right=80):
    tcPr = cell._tc.get_or_add_tcPr()
    mar = OxmlElement("w:tcMar")
    for tag, val in (("top", top), ("bottom", bottom), ("start", left), ("end", right)):
        node = OxmlElement(f"w:{tag}")
        node.set(qn("w:w"), str(val))
        node.set(qn("w:type"), "dxa")
        mar.append(node)
    tcPr.append(mar)

def _set_cell_border(cell, edge="bottom", size=6, color="234964", val="single"):
    """
    edge  : 'top', 'bottom', 'left'/'start', 'right'/'end'
    size  : épaisseur en 1/8e de point (6 = 0.75pt, 12 = 1.5pt...)
    color : hex sans #
    val   : 'single', 'double', 'dashed', 'none', ...
    """
    tcPr = cell._tc.get_or_add_tcPr()
    borders = tcPr.find(qn("w:tcBorders"))
    if borders is None:
        borders = OxmlElement("w:tcBorders")
        tcPr.append(borders)

    tag = qn(f"w:{edge}")
    node = borders.find(tag)
    if node is None:
        node = OxmlElement(f"w:{edge}")
        borders.append(node)

    node.set(qn("w:val"), val)
    node.set(qn("w:sz"), str(size))
    node.set(qn("w:space"), "0")
    node.set(qn("w:color"), color)


# ══════════════════════════════════════════════════════════════════
# Construction de la hiérarchie
# ══════════════════════════════════════════════════════════════════

def _build_tree(tasks: list[Task]) -> dict:
    """
    Regroupe les tâches par Espace > Dossier (ou None) > Liste, en
    conservant l'ordre de première apparition (= ordre ClickUp).
    """
    tree: dict = {}
    for t in tasks:
        space = t.space_name or "Espace"
        folder = t.folder_name or None
        lst = t.list_name or t.list_id
        tree.setdefault(space, {}).setdefault(folder, {}).setdefault(lst, []).append(t)
    return tree


def _flatten_hierarchy(list_tasks: list[Task]) -> list[tuple[Task, int]]:
    """
    Reconstruit l'arbre chantier/tâche/sous-tâche d'une liste et le
    linéarise en DFS, en triant chaque fratrie par `orderindex` (l'ordre
    d'affichage manuel ClickUp). L'ordre de récupération de l'API n'est
    PAS l'ordre d'affichage — ClickUp ne le respecte pas par défaut.
    Retourne une liste de tuples (task, depth).
    """
    ids_in_list = {t.id for t in list_tasks}
    children: dict = {}
    roots = []

    for seq, t in enumerate(list_tasks):
        t._seq = seq  # filet de sécurité pour un tri stable en cas d'égalité

    for t in list_tasks:
        parent = t.parent_id
        if parent and parent in ids_in_list:
            children.setdefault(parent, []).append(t)
        else:
            roots.append(t)

    roots.sort(key=lambda t: (t.orderindex, t._seq))
    for kids in children.values():
        kids.sort(key=lambda t: (t.orderindex, t._seq))

    result = []

    def dfs(task: Task, depth: int):
        result.append((task, depth))
        for child in children.get(task.id, []):
            dfs(child, depth + 1)

    for r in roots:
        dfs(r, 0)

    return result


# ══════════════════════════════════════════════════════════════════
# Formatage
# ══════════════════════════════════════════════════════════════════

def _fmt_date(dt) -> str:
    return dt.strftime("%d/%m/%Y") if dt else "—"


def _status_label(task: Task) -> str:
    if task.is_cancelled:
        return "Annulé"
    return (task.status or "—").strip().capitalize()


def _description_text(task: Task) -> str:
    for value in (task.description, task.description_gantt, task.note_contexte):
        if value:
            return str(value).strip()
    return ""


# ══════════════════════════════════════════════════════════════════
# Rendu Word
# ══════════════════════════════════════════════════════════════════

def _add_row(table, task: Task, depth: int):
    row = table.add_row()
    cell_name, cell_status, cell_due = row.cells

    is_chantier = task.item_type == TYPE_CHANTIER
    is_jalon = task.item_type == TYPE_JALON

    # -- Cellule nom (avec indentation de hiérarchie) --
    p = cell_name.paragraphs[0]
    p.paragraph_format.left_indent = Cm(0.45 * depth)
    prefix = "" if depth == 0 else "› "
    run = p.add_run(f"{prefix}{task.name}")
    run.font.size = Pt(10 if depth == 0 else 9.5)
    run.font.bold = is_chantier or depth == 0
    if is_jalon:
        run.font.color.rgb = COLOR_ORANGE
        run.font.bold = True
    elif is_chantier:
        run.font.color.rgb = COLOR_BLUE


    # -- Cellule statut --
    p2 = cell_status.paragraphs[0]
    p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run2 = p2.add_run(_status_label(task))
    run2.font.size = Pt(9.5)
    if task.is_done:
        run2.font.color.rgb = COLOR_SUCCESS
    elif task.is_at_risk:
        run2.font.color.rgb = COLOR_ERROR
        run2.font.bold = True
    elif task.is_waiting:
        run2.font.color.rgb = COLOR_SLATE

    # -- Cellule échéance --
    p3 = cell_due.paragraphs[0]
    p3.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run3 = p3.add_run(_fmt_date(task.due_date))
    run3.font.size = Pt(9.5)

    if is_chantier:
        for c in (cell_name, cell_status, cell_due):
            _shade_cell(c, BG_CHANTIER)
    elif is_jalon:
        for c in (cell_name, cell_status, cell_due):
            _shade_cell(c, BG_JALON)
    else :
        for c in (cell_name, cell_status, cell_due):
            _set_cell_border(c, edge="bottom", size=6, color="234964")

    for c in (cell_name, cell_status, cell_due):
        _set_cell_margins(c)

    # -- Ligne description (fusionnée), seulement si contenu --
    desc = _description_text(task)
    if desc:
        drow = table.add_row()
        merged = drow.cells[0].merge(drow.cells[1]).merge(drow.cells[2])
        dp = merged.paragraphs[0]
        dp.paragraph_format.left_indent = Cm(0.45 * depth + 0.3)
        drun = dp.add_run(desc)
        drun.font.size = Pt(9)
        drun.font.italic = True
        drun.font.color.rgb = COLOR_SLATE
        _set_cell_margins(merged, top=20, bottom=60)


def _add_table(doc: Document, rows: list[tuple[Task, int]]):
    table = doc.add_table(rows=1, cols=3)
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False

    widths = (Cm(11.0), Cm(3.0), Cm(3.0))
    for col, w in zip(table.columns, widths):
        col.width = w

    hdr = table.rows[0]
    _repeat_header(hdr)
    headers = ("Élément", "Statut", "Échéance")
    for cell, label, w in zip(hdr.cells, headers, widths):
        cell.width = w
        _shade_cell(cell, BG_HEADER)
        p = cell.paragraphs[0]
        if label != "Élément":
            p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = p.add_run(label)
        run.font.bold = True
        run.font.size = Pt(10)
        run.font.color.rgb = COLOR_WHITE
        _set_cell_margins(cell)

    for task, depth in rows:
        _add_row(table, task, depth)
        # Réapplique la largeur des colonnes (Word a tendance à l'oublier
        # sur les lignes ajoutées dynamiquement)
        for row in table.rows[-2:]:
            for cell, w in zip(row.cells, widths):
                cell.width = w

    return table


def _add_cover(doc: Document, title: str, scope_summary: list[str]):
    h = doc.add_heading(title, level=0)
    for run in h.runs:
        run.font.color.rgb = COLOR_BLUE

    p = doc.add_paragraph()
    run = p.add_run(f"Généré le {datetime.now().strftime('%d/%m/%Y à %H:%M')}")
    run.font.size = Pt(10)
    run.font.color.rgb = COLOR_SLATE

    if scope_summary:
        p2 = doc.add_paragraph()
        run2 = p2.add_run("Périmètre : " + ", ".join(scope_summary))
        run2.font.size = Pt(10)
        run2.font.color.rgb = COLOR_SLATE

    doc.add_paragraph()


def new_document(title: str, subtitle: str = ""):
    """
    Un document vide, stylé, avec sa page de garde (chantier JOS-69).

    Sert quand l'appelant compose plusieurs blocs : `generate_docx()` ne pose ni
    styles ni couverture sur un document qu'il n'a pas créé — sinon deux blocs
    de planning produiraient deux pages de garde. La couverture appartient au
    rapport, pas à l'un de ses blocs.
    """
    doc = Document()

    normal = doc.styles["Normal"]
    normal.font.name = "Calibri"
    normal.font.size = Pt(10)
    normal.font.color.rgb = COLOR_TEXT

    for section in doc.sections:
        section.left_margin = Cm(1.8)
        section.right_margin = Cm(1.8)
        section.top_margin = Cm(1.8)
        section.bottom_margin = Cm(1.8)

    _add_cover(doc, title, [subtitle] if subtitle else [])

    return doc


def generate_docx(tasks: list[Task], output_path: str, title: str = "Rapport de suivi",
                  doc=None, save: bool = True):
    """
    Point d'entrée public.

    Paramètres :
        tasks       — liste d'objets Task (déjà parsés depuis ClickUp)
        output_path — chemin du fichier .docx à écrire
        title       — titre affiché en page de garde
        doc         — document existant à compléter (Joseph, chantier JOS-69 :
                      un rapport est une composition de blocs, et le planning
                      n'est que l'un d'eux). `None` en crée un.
        save        — sauté quand l'appelant sauvegardera lui-même à la fin.
    """
    fresh = doc is None

    if fresh:
        doc = Document()

        # Styles de base — posés une seule fois, sur le document neuf : les
        # réappliquer à un document fourni écraserait les choix de l'appelant.
        normal = doc.styles["Normal"]
        normal.font.name = "Calibri"
        normal.font.size = Pt(10)
        normal.font.color.rgb = COLOR_TEXT

        for section in doc.sections:
            section.left_margin = Cm(1.8)
            section.right_margin = Cm(1.8)
            section.top_margin = Cm(1.8)
            section.bottom_margin = Cm(1.8)

    tree = _build_tree(tasks)

    scope_summary = []
    for space, folders in tree.items():
        for folder, lists in folders.items():
            for lst in lists:
                scope_summary.append(f"{space} › {folder + ' › ' if folder else ''}{lst}")

    # La page de garde n'a de sens que sur un document neuf : dans une
    # composition, c'est l'appelant qui la pose, une fois pour tout le rapport.
    if fresh:
        _add_cover(doc, title, scope_summary)

    first_space = True
    for space_name, folders in tree.items():
        if not first_space:
            doc.add_page_break()
        first_space = False

        h1 = doc.add_heading(space_name, level=1)
        for run in h1.runs:
            run.font.color.rgb = COLOR_BLUE

        for folder_name, lists in folders.items():
            if folder_name:
                h2 = doc.add_heading(folder_name, level=2)
                for run in h2.runs:
                    run.font.color.rgb = COLOR_BLUE_MID

            for list_name, list_tasks in lists.items():
                doc.add_heading(list_name, level=3)

                rows = _flatten_hierarchy(list_tasks)
                if not rows:
                    p = doc.add_paragraph("Aucune tâche dans cette liste.")
                    p.runs[0].font.italic = True
                    p.runs[0].font.color.rgb = COLOR_SLATE
                    continue

                _add_table(doc, rows)
                doc.add_paragraph()

    if save:
        doc.save(output_path)
    return output_path

# ══════════════════════════════════════════════════════════════════
# PLANNING EN TABLEAU DATÉ (chantier JOS-69)
# ══════════════════════════════════════════════════════════════════

def set_landscape(section):
    """
    Bascule une section en paysage.

    Un planning daté a autant de colonnes que la période a de semaines ou de
    mois : en portrait, elles seraient réduites à des traits. `python-docx`
    n'inverse pas les dimensions tout seul — changer `orientation` ne fait que
    déclarer l'intention, il faut permuter largeur et hauteur à la main.
    """
    from docx.enum.section import WD_ORIENT

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

    section.orientation = WD_ORIENT.LANDSCAPE
    section.left_margin = Cm(1.2)
    section.right_margin = Cm(1.2)


def generate_gantt_table(tasks: list, output_path: str, title: str = "Planning",
                         scale: str = "semaine", range_start=None, range_end=None,
                         doc=None, save: bool = True, show_tasks: bool = True):
    """
    Le planning en **tableau daté** : une ligne par élément, une colonne par
    unité de temps, les cellules de la période d'un élément teintées.

    C'est le rendu du bloc « Planning » en Word — le Gantt dessiné reste réservé
    au PowerPoint, qui a la surface pour ça. Les colonnes de gauche portent ce
    qu'un Gantt met dans son étiquette : le nom, l'état, les deux dates.

    Les colonnes de dates viennent de `planning_excel` : c'est le même découpage
    (`iter_columns`) et la même fenêtre (`compute_date_range`) que le tableur, il
    n'y a aucune raison d'en écrire une troisième version.
    """
    from generators.planning_excel import (
        compute_date_range, iter_columns, sort_tasks_hierarchical, DomainHeader,
        build_domain_color_map, get_task_colors,
    )
    from core.data_model import group_reunions as _group

    fresh = doc is None
    if fresh:
        doc = new_document(title, "")

    grouped = _group(tasks)
    ordered = sort_tasks_hierarchical(grouped)
    start_d, end_d = compute_date_range(grouped, scale, range_start=range_start, range_end=range_end)
    cols = iter_columns(start_d, end_d, scale)
    domain_map = build_domain_color_map(grouped)

    heading = doc.add_heading(title, level=1)
    for run in heading.runs:
        run.font.color.rgb = COLOR_BLUE

    table = doc.add_table(rows=1, cols=4 + len(cols))
    table.style = "Table Grid"
    table.alignment = WD_TABLE_ALIGNMENT.CENTER

    # En-tête : les quatre colonnes fixes, puis les dates.
    headers = ["Élément", "État", "Début", "Échéance"] + [_col_label(c, scale) for c in cols]
    for i, label in enumerate(headers):
        cell = table.rows[0].cells[i]
        cell.text = ""
        run = cell.paragraphs[0].add_run(label)
        run.font.bold = True
        run.font.size = Pt(6.5 if i >= 4 else 8)
        run.font.color.rgb = COLOR_WHITE
        _shade_cell(cell, BG_HEADER)
    _repeat_header(table.rows[0])

    for item in ordered:
        if isinstance(item, DomainHeader):
            continue

        is_chantier = getattr(item, "item_type", None) == TYPE_CHANTIER
        if not show_tasks and not is_chantier and getattr(item, "item_type", None) != TYPE_JALON:
            continue

        _add_gantt_row(table, item, cols, scale, domain_map, len(cols))

    _set_gantt_widths(table, len(cols))
    doc.add_paragraph()

    if save:
        doc.save(output_path)

    return output_path


def _col_label(col: dict, scale: str) -> str:
    """L'en-tête d'une colonne de dates, au plus court : la place est comptée."""
    d = col["date"]

    if scale == "jour":
        return str(d.day)
    if scale == "semaine":
        return "S{}".format(d.isocalendar()[1])

    return col.get("month_label") or str(d.month)


def _add_gantt_row(table, task, cols: list, scale: str, domain_map: dict, n_cols: int):
    """Une ligne : les quatre cellules fixes, puis la barre teintée."""
    from generators.planning_excel import get_task_colors

    row = table.add_row()
    bg_cell, bar_color, text_color = get_task_colors(task, domain_map)

    is_chantier = getattr(task, "item_type", None) == TYPE_CHANTIER
    occurrences = getattr(task, "occurrences", None)

    name_run = row.cells[0].paragraphs[0].add_run(task.name)
    name_run.font.size = Pt(8)
    name_run.font.bold = is_chantier

    for index, value in enumerate([
        _status_label(task) if not occurrences else "Réunions",
        _fmt_date(getattr(task, "start_date", None)),
        _fmt_date(getattr(task, "due_date", None)),
    ], start=1):
        run = row.cells[index].paragraphs[0].add_run(value)
        run.font.size = Pt(7.5)

    if is_chantier:
        for i in range(4):
            _shade_cell(row.cells[i], bg_cell)

    # La barre. Une réunion récurrente n'a pas de période : chacune de ses
    # occurrences teinte sa propre colonne.
    for occ in (occurrences or []):
        idx = _col_index(occ, cols, scale)
        if idx is not None:
            _shade_cell(row.cells[4 + idx], bar_color)
        continue

    if occurrences:
        return

    start = getattr(task, "start_date", None) or getattr(task, "due_date", None)
    end = getattr(task, "due_date", None) or getattr(task, "start_date", None)

    if start is None or end is None:
        return

    if end < start:
        start, end = end, start

    first = _col_index(start, cols, scale)
    last = _col_index(end, cols, scale)

    if first is None and last is None:
        return

    # Rognage aux bornes, comme dans le tableur : une barre qui déborde reste
    # visible sur la partie qui tombe dans la fenêtre.
    first = 0 if first is None else first
    last = n_cols - 1 if last is None else last

    for i in range(first, last + 1):
        _shade_cell(row.cells[4 + i], bar_color)


def _col_index(moment, cols: list, scale: str):
    """La colonne contenant cette date, ou `None` si elle est hors fenêtre."""
    from generators.planning_excel import date_to_col_idx

    idx = date_to_col_idx(moment, cols, 0)

    return idx


def _set_gantt_widths(table, n_cols: int):
    """
    Largeurs fixes : Word répartit sinon l'espace au prorata du contenu, et une
    colonne de date vide se réduit à un trait.
    """
    from docx.shared import Cm as _Cm

    fixed = [_Cm(6.5), _Cm(2.2), _Cm(1.8), _Cm(1.8)]
    date_w = _Cm(max(0.35, min(1.2, 14.0 / max(n_cols, 1))))

    for row in table.rows:
        for i, cell in enumerate(row.cells):
            cell.width = fixed[i] if i < 4 else date_w
