from __future__ import annotations

import html
import re
from datetime import date
from pathlib import Path

from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
    BaseDocTemplate,
    CondPageBreak,
    Frame,
    PageBreak,
    PageTemplate,
    Paragraph,
    Spacer,
    Table,
    TableStyle,
)
from reportlab.platypus.tableofcontents import TableOfContents


ROOT_DIR = Path(__file__).resolve().parents[2]
SOURCE_MD = ROOT_DIR / "docs" / "USER_GUIDE.md"
OUTPUT_PDF = ROOT_DIR / "docs" / "POS_System_User_Guide.pdf"

DOCUMENT_TITLE = "POS System User Guide"
DOCUMENT_SUBTITLE = "Client User Guide for Business Owners and Store Employees"
DOCUMENT_VERSION = "1.0"


def register_fonts() -> dict[str, str]:
    font_sets = [
        {
            "regular": Path("C:/Windows/Fonts/arial.ttf"),
            "bold": Path("C:/Windows/Fonts/arialbd.ttf"),
            "italic": Path("C:/Windows/Fonts/ariali.ttf"),
            "bold_italic": Path("C:/Windows/Fonts/arialbi.ttf"),
        },
        {
            "regular": Path("C:/Windows/Fonts/calibri.ttf"),
            "bold": Path("C:/Windows/Fonts/calibrib.ttf"),
            "italic": Path("C:/Windows/Fonts/calibrii.ttf"),
            "bold_italic": Path("C:/Windows/Fonts/calibriz.ttf"),
        },
    ]

    for font_set in font_sets:
        if all(path.exists() for path in font_set.values()):
            pdfmetrics.registerFont(TTFont("GuideRegular", str(font_set["regular"])))
            pdfmetrics.registerFont(TTFont("GuideBold", str(font_set["bold"])))
            pdfmetrics.registerFont(TTFont("GuideItalic", str(font_set["italic"])))
            pdfmetrics.registerFont(TTFont("GuideBoldItalic", str(font_set["bold_italic"])))
            pdfmetrics.registerFontFamily(
                "Guide",
                normal="GuideRegular",
                bold="GuideBold",
                italic="GuideItalic",
                boldItalic="GuideBoldItalic",
            )
            return {
                "regular": "GuideRegular",
                "bold": "GuideBold",
                "italic": "GuideItalic",
                "bold_italic": "GuideBoldItalic",
                "family": "Guide",
                "mono": "Courier",
            }

    return {
        "regular": "Helvetica",
        "bold": "Helvetica-Bold",
        "italic": "Helvetica-Oblique",
        "bold_italic": "Helvetica-BoldOblique",
        "family": "Helvetica",
        "mono": "Courier",
    }


FONTS = register_fonts()


def style_sheet() -> dict[str, ParagraphStyle]:
    base = getSampleStyleSheet()
    styles: dict[str, ParagraphStyle] = {}

    styles["CoverTitle"] = ParagraphStyle(
        "CoverTitle",
        parent=base["Title"],
        fontName=FONTS["bold"],
        fontSize=30,
        leading=36,
        alignment=TA_CENTER,
        textColor=colors.HexColor("#111827"),
        spaceAfter=18,
    )
    styles["CoverSubtitle"] = ParagraphStyle(
        "CoverSubtitle",
        parent=base["Normal"],
        fontName=FONTS["regular"],
        fontSize=13,
        leading=18,
        alignment=TA_CENTER,
        textColor=colors.HexColor("#4b5563"),
        spaceAfter=8,
    )
    styles["CoverMeta"] = ParagraphStyle(
        "CoverMeta",
        parent=base["Normal"],
        fontName=FONTS["regular"],
        fontSize=10,
        leading=14,
        alignment=TA_CENTER,
        textColor=colors.HexColor("#6b7280"),
    )
    styles["TitleHeading"] = ParagraphStyle(
        "TitleHeading",
        parent=base["Heading1"],
        fontName=FONTS["bold"],
        fontSize=20,
        leading=25,
        textColor=colors.HexColor("#111827"),
        spaceBefore=0,
        spaceAfter=14,
    )
    styles["SectionHeading"] = ParagraphStyle(
        "SectionHeading",
        parent=base["Heading2"],
        fontName=FONTS["bold"],
        fontSize=16,
        leading=21,
        textColor=colors.HexColor("#1f4e79"),
        spaceBefore=0,
        spaceAfter=10,
        keepWithNext=True,
    )
    styles["SubHeading"] = ParagraphStyle(
        "SubHeading",
        parent=base["Heading3"],
        fontName=FONTS["bold"],
        fontSize=12.5,
        leading=16,
        textColor=colors.HexColor("#111827"),
        spaceBefore=10,
        spaceAfter=6,
        keepWithNext=True,
    )
    styles["Body"] = ParagraphStyle(
        "Body",
        parent=base["BodyText"],
        fontName=FONTS["regular"],
        fontSize=9.7,
        leading=14,
        textColor=colors.HexColor("#1f2937"),
        spaceAfter=6,
    )
    styles["Label"] = ParagraphStyle(
        "Label",
        parent=styles["Body"],
        fontName=FONTS["bold"],
        textColor=colors.HexColor("#374151"),
        spaceBefore=4,
        spaceAfter=4,
        keepWithNext=True,
    )
    styles["Bullet"] = ParagraphStyle(
        "Bullet",
        parent=styles["Body"],
        leftIndent=18,
        firstLineIndent=0,
        bulletIndent=4,
        bulletFontName=FONTS["regular"],
        bulletFontSize=8.5,
        spaceAfter=3,
    )
    styles["Numbered"] = ParagraphStyle(
        "Numbered",
        parent=styles["Body"],
        leftIndent=20,
        firstLineIndent=0,
        bulletIndent=0,
        bulletFontName=FONTS["regular"],
        bulletFontSize=8.5,
        spaceAfter=3,
    )
    styles["CodeBlock"] = ParagraphStyle(
        "CodeBlock",
        parent=styles["Body"],
        fontName=FONTS["mono"],
        fontSize=8,
        leading=10,
        backColor=colors.HexColor("#f3f4f6"),
        borderColor=colors.HexColor("#e5e7eb"),
        borderWidth=0.5,
        borderPadding=6,
        leftIndent=6,
        rightIndent=6,
        spaceBefore=4,
        spaceAfter=8,
    )
    styles["TocTitle"] = ParagraphStyle(
        "TocTitle",
        parent=styles["TitleHeading"],
        alignment=TA_LEFT,
        spaceAfter=18,
    )
    styles["TocLevel0"] = ParagraphStyle(
        "TocLevel0",
        parent=styles["Body"],
        fontName=FONTS["regular"],
        fontSize=9.4,
        leading=13,
        leftIndent=0,
        firstLineIndent=0,
        spaceBefore=1,
        spaceAfter=1,
    )
    return styles


STYLES = style_sheet()


class GuideDocTemplate(BaseDocTemplate):
    def __init__(self, filename: str, **kwargs):
        super().__init__(filename, **kwargs)
        self._bookmark_counter = 0

    def afterFlowable(self, flowable):
        if not isinstance(flowable, Paragraph):
            return

        level = getattr(flowable, "_toc_level", None)
        if level is None:
            return

        text = flowable.getPlainText()
        key = getattr(flowable, "_bookmark_name", None)
        if key:
            self.canv.bookmarkPage(key)
            self.canv.addOutlineEntry(text, key, level=level, closed=False)
        self.notify("TOCEntry", (level, text, self.page, key))


def draw_page(canvas, doc):
    page_width, page_height = doc.pagesize

    if doc.page > 1:
        canvas.saveState()
        canvas.setStrokeColor(colors.HexColor("#d1d5db"))
        canvas.setLineWidth(0.5)
        canvas.line(doc.leftMargin, page_height - 0.52 * inch, page_width - doc.rightMargin, page_height - 0.52 * inch)
        canvas.setFont(FONTS["regular"], 8)
        canvas.setFillColor(colors.HexColor("#6b7280"))
        canvas.drawString(doc.leftMargin, page_height - 0.38 * inch, DOCUMENT_TITLE)
        canvas.drawRightString(page_width - doc.rightMargin, 0.42 * inch, f"Page {doc.page}")
        canvas.restoreState()


def format_inline(text: str) -> str:
    escaped = html.escape(text, quote=False)

    def replace_code(match: re.Match[str]) -> str:
        content = match.group(1)
        return f'<font name="{FONTS["mono"]}">{content}</font>'

    escaped = re.sub(r"`([^`]+)`", replace_code, escaped)
    escaped = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", escaped)
    return escaped


def make_paragraph(text: str, style: ParagraphStyle) -> Paragraph:
    return Paragraph(format_inline(text), style)


def make_heading(text: str, style: ParagraphStyle, toc_level: int | None = None) -> Paragraph:
    heading = make_paragraph(text, style)
    if toc_level is not None:
        safe = re.sub(r"[^A-Za-z0-9]+", "_", text).strip("_")[:70] or "section"
        heading._toc_level = toc_level
        heading._bookmark_name = f"toc_{toc_level}_{safe}_{abs(hash(text))}"
    return heading


def flush_list(story: list, list_items: list[str], list_type: str) -> None:
    if not list_items:
        return

    style = STYLES["Bullet"] if list_type == "bullet" else STYLES["Numbered"]
    for index, item in enumerate(list_items, start=1):
        prefix = "- " if list_type == "bullet" else f"{index}. "
        story.append(make_paragraph(f"{prefix}{item}", style))
    story.append(Spacer(1, 3))
    list_items.clear()


def markdown_to_story(markdown_text: str) -> list:
    story: list = []
    list_items: list[str] = []
    list_type = ""
    code_lines: list[str] = []
    in_code = False
    first_h2 = True

    for raw_line in markdown_text.splitlines():
        line = raw_line.rstrip()

        if line.strip().startswith("```"):
            if in_code:
                story.append(Paragraph("<br/>".join(html.escape(value) for value in code_lines), STYLES["CodeBlock"]))
                code_lines = []
                in_code = False
            else:
                flush_list(story, list_items, list_type)
                list_type = ""
                in_code = True
            continue

        if in_code:
            code_lines.append(line)
            continue

        if not line.strip():
            flush_list(story, list_items, list_type)
            list_type = ""
            continue

        h1 = re.match(r"^#\s+(.+)$", line)
        h2 = re.match(r"^##\s+(.+)$", line)
        h3 = re.match(r"^###\s+(.+)$", line)
        bullet = re.match(r"^-\s+(.+)$", line)
        numbered = re.match(r"^\d+\.\s+(.+)$", line)

        if h1:
            flush_list(story, list_items, list_type)
            list_type = ""
            story.append(make_heading(h1.group(1), STYLES["TitleHeading"]))
            continue

        if h2:
            flush_list(story, list_items, list_type)
            list_type = ""
            if not first_h2:
                story.append(CondPageBreak(2.2 * inch))
                story.append(Spacer(1, 8))
            first_h2 = False
            story.append(make_heading(h2.group(1), STYLES["SectionHeading"], toc_level=0))
            continue

        if h3:
            flush_list(story, list_items, list_type)
            list_type = ""
            story.append(make_heading(h3.group(1), STYLES["SubHeading"]))
            continue

        if bullet:
            if list_type not in ("", "bullet"):
                flush_list(story, list_items, list_type)
            list_type = "bullet"
            list_items.append(bullet.group(1))
            continue

        if numbered:
            if list_type not in ("", "number"):
                flush_list(story, list_items, list_type)
            list_type = "number"
            list_items.append(numbered.group(1))
            continue

        flush_list(story, list_items, list_type)
        list_type = ""

        if line.endswith(":") and len(line) <= 90:
            story.append(make_paragraph(line, STYLES["Label"]))
        else:
            story.append(make_paragraph(line, STYLES["Body"]))

    flush_list(story, list_items, list_type)
    if in_code and code_lines:
        story.append(Paragraph("<br/>".join(html.escape(value) for value in code_lines), STYLES["CodeBlock"]))

    return story


def cover_story() -> list:
    generated = date.today().strftime("%B %d, %Y").replace(" 0", " ")
    meta_data = [
        ["Document Version", DOCUMENT_VERSION],
        ["Generated", generated],
        ["Source", "docs/USER_GUIDE.md"],
    ]
    table = Table(meta_data, colWidths=[1.6 * inch, 2.8 * inch], hAlign="CENTER")
    table.setStyle(
        TableStyle(
            [
                ("FONTNAME", (0, 0), (-1, -1), FONTS["regular"]),
                ("FONTSIZE", (0, 0), (-1, -1), 9),
                ("TEXTCOLOR", (0, 0), (-1, -1), colors.HexColor("#4b5563")),
                ("ALIGN", (0, 0), (0, -1), "RIGHT"),
                ("ALIGN", (1, 0), (1, -1), "LEFT"),
                ("LINEABOVE", (0, 0), (-1, 0), 0.5, colors.HexColor("#d1d5db")),
                ("LINEBELOW", (0, -1), (-1, -1), 0.5, colors.HexColor("#d1d5db")),
                ("TOPPADDING", (0, 0), (-1, -1), 7),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 7),
            ]
        )
    )

    return [
        Spacer(1, 1.35 * inch),
        Paragraph(DOCUMENT_TITLE, STYLES["CoverTitle"]),
        Paragraph(DOCUMENT_SUBTITLE, STYLES["CoverSubtitle"]),
        Spacer(1, 0.45 * inch),
        table,
        Spacer(1, 0.8 * inch),
        Paragraph("Prepared for daily store operations, branch management, and staff training.", STYLES["CoverMeta"]),
        PageBreak(),
    ]


def toc_story() -> list:
    toc = TableOfContents()
    toc.levelStyles = [STYLES["TocLevel0"]]
    return [
        Paragraph("Table of Contents", STYLES["TocTitle"]),
        toc,
        PageBreak(),
    ]


def build_pdf() -> None:
    if not SOURCE_MD.exists():
        raise FileNotFoundError(f"Source guide not found: {SOURCE_MD}")

    markdown_text = SOURCE_MD.read_text(encoding="utf-8")
    OUTPUT_PDF.parent.mkdir(parents=True, exist_ok=True)

    doc = GuideDocTemplate(
        str(OUTPUT_PDF),
        pagesize=letter,
        leftMargin=0.68 * inch,
        rightMargin=0.68 * inch,
        topMargin=0.78 * inch,
        bottomMargin=0.65 * inch,
        title=DOCUMENT_TITLE,
        author="gjbulan/pos_system",
        subject="Client user guide for POS System",
    )
    frame = Frame(
        doc.leftMargin,
        doc.bottomMargin,
        doc.width,
        doc.height,
        id="normal",
    )
    doc.addPageTemplates([PageTemplate(id="Guide", frames=[frame], onPage=draw_page)])

    story = cover_story() + toc_story() + markdown_to_story(markdown_text)
    doc.multiBuild(story)


if __name__ == "__main__":
    build_pdf()
    print(f"Generated {OUTPUT_PDF}")
