#!/usr/bin/env python3
"""Convert DokuWiki pages to nicely-formatted PDFs (reportlab)."""
import re
import sys
from reportlab.lib import colors
from reportlab.lib.colors import HexColor
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    PageBreak, Preformatted, KeepTogether, HRFlowable
)
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY

ACCENT = HexColor("#00a3c4")
ACCENT_LIGHT = HexColor("#e0f4f8")
DARK = HexColor("#1a1a2e")
GRAY = HexColor("#666666")
CODE_BG = HexColor("#f4f4f4")
INFO_BG = HexColor("#e8f4fd")
IMPORTANT_BG = HexColor("#fef3cd")
TIP_BG = HexColor("#d4edda")


def build_styles():
    styles = getSampleStyleSheet()
    styles.add(ParagraphStyle("H1c", parent=styles["Heading1"],
        fontSize=20, textColor=ACCENT, spaceAfter=14, spaceBefore=10,
        fontName="Helvetica-Bold"))
    styles.add(ParagraphStyle("H2c", parent=styles["Heading2"],
        fontSize=15, textColor=ACCENT, spaceAfter=10, spaceBefore=14,
        fontName="Helvetica-Bold", borderPadding=(0, 0, 4, 0),
        borderColor=ACCENT, borderWidth=0))
    styles.add(ParagraphStyle("H3c", parent=styles["Heading3"],
        fontSize=12.5, textColor=DARK, spaceAfter=8, spaceBefore=10,
        fontName="Helvetica-Bold"))
    styles.add(ParagraphStyle("H4c", parent=styles["Heading4"],
        fontSize=11, textColor=DARK, spaceAfter=6, spaceBefore=8,
        fontName="Helvetica-Bold"))
    styles.add(ParagraphStyle("Bodyc", parent=styles["BodyText"],
        fontSize=9.5, leading=13, spaceAfter=6, alignment=TA_JUSTIFY))
    styles.add(ParagraphStyle("Bulletc", parent=styles["BodyText"],
        fontSize=9.5, leading=13, leftIndent=16, bulletIndent=4, spaceAfter=3))
    styles.add(ParagraphStyle("ItalicFooter", parent=styles["BodyText"],
        fontSize=9, textColor=GRAY, alignment=TA_CENTER, spaceAfter=4))
    return styles


def fmt_inline(text):
    """Convert DokuWiki inline markup to reportlab mini-HTML."""
    # escape &, <, >
    text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
    # bold **x**
    text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
    # inline code ''x''
    text = re.sub(r"''(.+?)''", r'<font face="Courier" color="#b22222">\1</font>', text)
    # italic //x// (avoid URLs)
    text = re.sub(r"(?<!:)//(.+?)//", r"<i>\1</i>", text)
    # doku links [[target|label]] -> label
    text = re.sub(r"\[\[[^\]|]+\|([^\]]+)\]\]", r"\1", text)
    text = re.sub(r"\[\[([^\]]+)\]\]", r"\1", text)
    # line break \\
    text = text.replace("\\\\", "<br/>")
    return text


def parse_wiki(raw):
    """Return list of (kind, payload) blocks."""
    lines = raw.splitlines()
    blocks = []
    i = 0
    while i < len(lines):
        ln = lines[i]
        stripped = ln.strip()

        # Separator
        if stripped == "----":
            blocks.append(("hr", None))
            i += 1
            continue

        # Headings
        m = re.match(r"^(=+)\s*(.+?)\s*=+\s*$", stripped)
        if m:
            level = 7 - len(m.group(1))  # ======(6) -> h1
            level = max(1, min(4, level))
            blocks.append((f"h{level}", m.group(2)))
            i += 1
            continue

        # Code block
        if stripped.startswith("<code"):
            i += 1
            code_lines = []
            while i < len(lines) and "</code>" not in lines[i]:
                code_lines.append(lines[i])
                i += 1
            i += 1  # skip </code>
            blocks.append(("code", "\n".join(code_lines)))
            continue

        # WRAP box
        m = re.match(r"^<WRAP\s+([^>]*)>\s*$", stripped)
        if m:
            kind = "info"
            attrs = m.group(1).lower()
            if "important" in attrs:
                kind = "important"
            elif "tip" in attrs:
                kind = "tip"
            elif "info" in attrs:
                kind = "info"
            i += 1
            body_lines = []
            while i < len(lines) and "</WRAP>" not in lines[i]:
                body_lines.append(lines[i])
                i += 1
            i += 1
            blocks.append(("wrap", (kind, "\n".join(body_lines))))
            continue

        # Table (starts with ^ or |)
        if stripped.startswith("^") or stripped.startswith("|"):
            tbl_lines = []
            while i < len(lines) and (lines[i].strip().startswith("^") or lines[i].strip().startswith("|")):
                tbl_lines.append(lines[i].strip())
                i += 1
            blocks.append(("table", tbl_lines))
            continue

        # Bullet list (item or sub-item)
        if re.match(r"^\s*\*\s+", ln):
            items = []
            while i < len(lines) and re.match(r"^\s*\*\s+", lines[i]):
                m2 = re.match(r"^(\s*)\*\s+(.+)$", lines[i])
                indent = len(m2.group(1))
                items.append((indent, m2.group(2)))
                i += 1
            blocks.append(("bullets", items))
            continue

        # Ordered list (DokuWiki uses "  - item")
        if re.match(r"^\s+-\s+", ln):
            items = []
            while i < len(lines) and re.match(r"^\s+-\s+", lines[i]):
                m2 = re.match(r"^(\s*)-\s+(.+)$", lines[i])
                indent = len(m2.group(1))
                items.append((indent, m2.group(2)))
                i += 1
            blocks.append(("ordered", items))
            continue

        # Italic footer //...//
        if stripped.startswith("//") and stripped.endswith("//") and len(stripped) > 4:
            blocks.append(("italic", stripped.strip("/").strip()))
            i += 1
            continue

        # Blank line -> spacer
        if not stripped:
            blocks.append(("space", None))
            i += 1
            continue

        # Regular paragraph (gather until blank or special)
        para_lines = [ln]
        i += 1
        while i < len(lines):
            nxt = lines[i]
            s = nxt.strip()
            if not s or s.startswith("=") or s.startswith("^") or s.startswith("|") \
               or s.startswith("<") or s == "----" or re.match(r"^\s*\*\s+", nxt) \
               or re.match(r"^\s+-\s+", nxt):
                break
            para_lines.append(nxt)
            i += 1
        blocks.append(("para", " ".join(l.strip() for l in para_lines)))
    return blocks


def parse_table_row(line):
    """Parse a DokuWiki table row. Returns (is_header_cells_list, cells)."""
    # Row like: ^ h1 ^ h2 ^  or  | c1 | c2 |
    # A row may mix ^ and | (cell-level header)
    sep_chars = "^|"
    cells = []
    is_header = []
    # find separators
    indices = [idx for idx, c in enumerate(line) if c in sep_chars]
    for a, b in zip(indices[:-1], indices[1:]):
        cell = line[a+1:b].strip()
        header = line[a] == "^"
        cells.append(cell)
        is_header.append(header)
    return is_header, cells


def build_table(tbl_lines, styles):
    data = []
    header_flags = []
    for ln in tbl_lines:
        is_h, cells = parse_table_row(ln)
        if not cells:
            continue
        data.append(cells)
        header_flags.append(is_h)
    if not data:
        return None

    # Wrap each cell in a Paragraph for wrapping
    wrapped = []
    cell_style = ParagraphStyle("cell", fontSize=8.5, leading=11)
    head_style = ParagraphStyle("head", fontSize=9, leading=11,
                                 fontName="Helvetica-Bold", textColor=colors.white)
    for row_idx, row in enumerate(data):
        new_row = []
        for col_idx, cell in enumerate(row):
            is_h = header_flags[row_idx][col_idx] if col_idx < len(header_flags[row_idx]) else False
            style = head_style if is_h else cell_style
            new_row.append(Paragraph(fmt_inline(cell), style))
        wrapped.append(new_row)

    ncols = max(len(r) for r in wrapped)
    # pad
    for r in wrapped:
        while len(r) < ncols:
            r.append(Paragraph("", cell_style))

    # column widths: spread evenly on 17cm available
    total_w = 17 * cm
    col_w = total_w / ncols
    t = Table(wrapped, colWidths=[col_w] * ncols, repeatRows=1)

    ts = TableStyle([
        ("GRID", (0, 0), (-1, -1), 0.3, colors.lightgrey),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("LEFTPADDING", (0, 0), (-1, -1), 4),
        ("RIGHTPADDING", (0, 0), (-1, -1), 4),
        ("TOPPADDING", (0, 0), (-1, -1), 3),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, HexColor("#f9f9f9")]),
    ])
    # header row style (if row 0 is all headers)
    if all(header_flags[0]):
        ts.add("BACKGROUND", (0, 0), (-1, 0), ACCENT)
    # color header cells individually otherwise
    for ri, flags in enumerate(header_flags):
        for ci, is_h in enumerate(flags):
            if is_h and ri > 0:
                ts.add("BACKGROUND", (ci, ri), (ci, ri), ACCENT_LIGHT)
    t.setStyle(ts)
    return t


def build_wrap(kind, text, styles):
    bg = {"info": INFO_BG, "important": IMPORTANT_BG, "tip": TIP_BG}.get(kind, INFO_BG)
    border = {"info": ACCENT, "important": HexColor("#e0a020"), "tip": HexColor("#28a745")}.get(kind, ACCENT)
    label = {"info": "Info", "important": "Important", "tip": "Astuce"}.get(kind, "Note")
    # Body can contain inline markup — render lines joined
    text_fmt = fmt_inline(text.strip()).replace("\n", "<br/>")
    inner = f'<b><font color="{border.hexval()}">{label}</font></b><br/>{text_fmt}'
    p = Paragraph(inner, ParagraphStyle("wrap", fontSize=9.5, leading=13,
                                         textColor=DARK, spaceAfter=4))
    tbl = Table([[p]], colWidths=[17 * cm])
    tbl.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), bg),
        ("BOX", (0, 0), (-1, -1), 1, border),
        ("LEFTPADDING", (0, 0), (-1, -1), 10),
        ("RIGHTPADDING", (0, 0), (-1, -1), 10),
        ("TOPPADDING", (0, 0), (-1, -1), 8),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
        ("LINEBEFORE", (0, 0), (0, -1), 4, border),
    ]))
    return tbl


def render_blocks(blocks, styles, title):
    story = [
        Paragraph(title, styles["H1c"]),
        HRFlowable(width="100%", thickness=2, color=ACCENT, spaceBefore=2, spaceAfter=10),
    ]
    for kind, payload in blocks:
        if kind == "h1":
            story.append(Paragraph(fmt_inline(payload), styles["H1c"]))
        elif kind == "h2":
            story.append(Paragraph(fmt_inline(payload), styles["H2c"]))
            story.append(HRFlowable(width="40%", thickness=1, color=ACCENT,
                                     spaceBefore=0, spaceAfter=6))
        elif kind == "h3":
            story.append(Paragraph(fmt_inline(payload), styles["H3c"]))
        elif kind == "h4":
            story.append(Paragraph(fmt_inline(payload), styles["H4c"]))
        elif kind == "hr":
            story.append(HRFlowable(width="100%", thickness=0.5,
                                     color=colors.lightgrey, spaceBefore=6, spaceAfter=6))
        elif kind == "para":
            story.append(Paragraph(fmt_inline(payload), styles["Bodyc"]))
        elif kind == "italic":
            story.append(Paragraph(f"<i>{fmt_inline(payload)}</i>", styles["ItalicFooter"]))
        elif kind == "code":
            story.append(Preformatted(payload, ParagraphStyle(
                "code", fontName="Courier", fontSize=8, leading=10,
                backColor=CODE_BG, borderColor=colors.lightgrey, borderWidth=0.5,
                borderPadding=6, spaceAfter=8, spaceBefore=4, leftIndent=4)))
        elif kind == "wrap":
            w_kind, w_body = payload
            story.append(build_wrap(w_kind, w_body, styles))
            story.append(Spacer(1, 6))
        elif kind == "table":
            t = build_table(payload, styles)
            if t:
                story.append(t)
                story.append(Spacer(1, 8))
        elif kind == "bullets":
            for indent, text in payload:
                lvl = indent // 2
                story.append(Paragraph(fmt_inline(text), ParagraphStyle(
                    "bl", fontSize=9.5, leading=13,
                    leftIndent=16 + lvl * 12, bulletIndent=4 + lvl * 12, spaceAfter=2),
                    bulletText="•"))
        elif kind == "ordered":
            for n, (indent, text) in enumerate(payload, 1):
                lvl = max(0, (indent - 2) // 2)
                story.append(Paragraph(fmt_inline(text), ParagraphStyle(
                    "ol", fontSize=9.5, leading=13,
                    leftIndent=18 + lvl * 12, bulletIndent=4 + lvl * 12, spaceAfter=2),
                    bulletText=f"{n}."))
        elif kind == "space":
            story.append(Spacer(1, 4))
    return story


def add_page_footer(canvas, doc):
    canvas.saveState()
    canvas.setFont("Helvetica", 8)
    canvas.setFillColor(GRAY)
    canvas.drawString(1.5 * cm, 1 * cm,
                       "SANEF DSI / Sécurité Opérationnelle — Plan d'action Qualys V3")
    canvas.drawRightString(A4[0] - 1.5 * cm, 1 * cm, f"Page {canvas.getPageNumber()}")
    canvas.restoreState()


def generate(src_path, dst_path, title):
    with open(src_path, "r", encoding="utf-8") as f:
        raw = f.read()
    blocks = parse_wiki(raw)
    styles = build_styles()
    story = render_blocks(blocks, styles, title)

    doc = SimpleDocTemplate(dst_path, pagesize=A4,
                             leftMargin=1.5 * cm, rightMargin=1.5 * cm,
                             topMargin=1.8 * cm, bottomMargin=1.8 * cm,
                             title=title)
    doc.build(story, onFirstPage=add_page_footer, onLaterPages=add_page_footer)
    print(f"Generated: {dst_path}")


if __name__ == "__main__":
    # args: src title dst
    src, title, dst = sys.argv[1], sys.argv[2], sys.argv[3]
    generate(src, dst, title)
