#!/usr/bin/env python3
"""Generate B&W normalization step diagrams for PYTANIE 3.

Each diagram shows database tables at a specific normalization stage.
Designed for A4 laser printer output (300 DPI, black & white).
"""

from __future__ import annotations

import logging

import matplotlib as mpl

mpl.use("Agg")
from pathlib import Path
from typing import TYPE_CHECKING

import matplotlib.patches as mpatches
import matplotlib.pyplot as plt

if TYPE_CHECKING:
    from matplotlib.axes import Axes
    from matplotlib.figure import Figure

logger = logging.getLogger(__name__)

OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)

# Common settings
DPI = 300
FONT_SIZE = 8
HEADER_COLOR = "#D0D0D0"
CELL_COLOR = "#FFFFFF"
HIGHLIGHT_COLOR = "#F0D0D0"  # light red-ish gray for violations
FIXED_COLOR = "#D0F0D0"  # light green-ish gray for fixed
FD_ARROW_COLOR = "#444444"


def _compute_col_widths(
    headers: list[str],
    rows: list[list[str]],
) -> list[float]:
    """Auto-calculate column widths based on content."""
    col_widths: list[float] = []
    for c in range(len(headers)):
        max_len = len(headers[c])
        for r in rows:
            if c < len(r):
                max_len = max(max_len, len(str(r[c])))
        col_widths.append(max(max_len * 0.08 + 0.1, 0.5))
    return col_widths


def draw_table(
    ax: Axes,
    x: float,
    y: float,
    title: str,
    headers: list[str],
    rows: list[list[str]],
    *,
    col_widths: list[float] | None = None,
    highlight_cols: set[int] | None = None,
    highlight_rows: set[int] | None = None,
    highlight_cells: set[tuple[int, int]] | None = None,
    strikethrough_cells: set[tuple[int, int]] | None = None,
    title_fontsize: int = 9,
) -> tuple[float, float]:
    """Draw a single table on the axes at position (x, y).

    Args:
        ax: matplotlib axes
        x: left position of the table
        y: top position of the table
        title: table title string
        headers: list of column header strings
        rows: list of lists (row data)
        col_widths: list of column widths (in inches-ish units)
        highlight_cols: set of column indices to highlight
        highlight_rows: set of row indices to highlight
        highlight_cells: set of (row, col) to highlight
        strikethrough_cells: set of (row, col) to draw strikethrough
        title_fontsize: font size for table title

    Returns:
        (width, height) of the drawn table
    """
    n_rows = len(rows)

    if col_widths is None:
        col_widths = _compute_col_widths(headers, rows)

    row_height = 0.22
    total_width = sum(col_widths)
    total_height = (n_rows + 1) * row_height  # +1 for header

    # Title
    ax.text(
        x + total_width / 2,
        y + 0.18,
        title,
        fontsize=title_fontsize,
        fontweight="bold",
        ha="center",
        va="bottom",
        family="monospace",
    )

    y_start = y

    # Draw header row
    cx = x
    for _c, (hdr, w) in enumerate(zip(headers, col_widths, strict=False)):
        color = HEADER_COLOR
        rect = mpatches.FancyBboxPatch(
            (cx, y_start),
            w,
            -row_height,
            boxstyle="square,pad=0",
            facecolor=color,
            edgecolor="black",
            linewidth=0.5,
        )
        ax.add_patch(rect)
        ax.text(
            cx + w / 2,
            y_start - row_height / 2,
            hdr,
            fontsize=FONT_SIZE,
            fontweight="bold",
            ha="center",
            va="center",
            family="monospace",
        )
        cx += w

    # Draw data rows
    for r_idx, row in enumerate(rows):
        cy = y_start - (r_idx + 1) * row_height
        cx = x
        for c_idx, (val, w) in enumerate(zip(row, col_widths, strict=False)):
            color = CELL_COLOR
            if highlight_cols and c_idx in highlight_cols:
                color = HIGHLIGHT_COLOR
            if highlight_rows and r_idx in highlight_rows:
                color = HIGHLIGHT_COLOR
            if highlight_cells and (r_idx, c_idx) in highlight_cells:
                color = HIGHLIGHT_COLOR

            rect = mpatches.FancyBboxPatch(
                (cx, cy),
                w,
                -row_height,
                boxstyle="square,pad=0",
                facecolor=color,
                edgecolor="black",
                linewidth=0.5,
            )
            ax.add_patch(rect)

            text_color = "black"
            ax.text(
                cx + w / 2,
                cy - row_height / 2,
                str(val),
                fontsize=FONT_SIZE,
                ha="center",
                va="center",
                family="monospace",
                color=text_color,
            )

            if strikethrough_cells and (r_idx, c_idx) in strikethrough_cells:
                ax.plot(
                    [cx + 0.03, cx + w - 0.03],
                    [cy - row_height / 2, cy - row_height / 2],
                    color="black",
                    linewidth=1.0,
                )

            cx += w

    return total_width, total_height + 0.25  # extra for title


def create_figure(
    width_inches: float = 11.69,
    height_inches: float = 8.27,
) -> tuple[Figure, Axes]:
    """Create A4 landscape figure."""
    fig, ax = plt.subplots(1, 1, figsize=(width_inches, height_inches), dpi=DPI)
    ax.set_xlim(0, width_inches)
    ax.set_ylim(0, height_inches)
    ax.axis("off")
    ax.set_aspect("equal")
    return fig, ax


def add_arrow(
    ax: Axes,
    x1: float,
    y1: float,
    x2: float,
    y2: float,
    label: str = "",
    *,
    color: str = "black",
) -> None:
    """Draw an arrow with optional label."""
    ax.annotate(
        "",
        xy=(x2, y2),
        xytext=(x1, y1),
        arrowprops={"arrowstyle": "->", "color": color, "lw": 1.5},
    )
    if label:
        mx, my = (x1 + x2) / 2, (y1 + y2) / 2
        ax.text(
            mx,
            my + 0.12,
            label,
            fontsize=7,
            ha="center",
            va="bottom",
            family="monospace",
            color=color,
        )


def add_label(
    ax: Axes,
    x: float,
    y: float,
    text: str,
    *,
    fontsize: int = 8,
    color: str = "black",
    ha: str = "left",
    style: str = "normal",
) -> None:
    """Add a text label."""
    ax.text(
        x,
        y,
        text,
        fontsize=fontsize,
        ha=ha,
        va="center",
        family="monospace",
        color=color,
        style=style,
    )


# ============================================================
# DIAGRAM 1: 0NF Table
# ============================================================
def draw_0nf() -> None:
    """Draw 0nf."""
    fig, ax = create_figure(11.69, 5.5)

    headers = [
        "StID",
        "Imie",
        "Telefony",
        "KursID",
        "NazwaKursu",
        "Prowadzacy",
        "WydzialID",
        "NazwaWydzialu",
    ]
    rows = [
        [
            "1",
            "Anna",
            "111-222, 333-444",
            "K10",
            "Bazy danych",
            "Kowalski",
            "W4",
            "EiTI",
        ],
        ["1", "Anna", "111-222, 333-444", "K20", "Algorytmy", "Nowak", "W4", "EiTI"],
        ["2", "Jan", "555-666", "K10", "Bazy danych", "Kowalski", "W4", "EiTI"],
        ["3", "Ewa", "777-888", "K30", "Optyka", "Wisniewski", "W2", "Fizyka"],
    ]
    col_widths = [0.5, 0.55, 1.55, 0.65, 1.1, 1.05, 0.85, 1.2]

    # Highlight the non-atomic column
    draw_table(
        ax,
        0.8,
        4.5,
        "0NF: Rejestr (forma nienormalna)",
        headers,
        rows,
        col_widths,
        highlight_cols={2},  # Telefony column
        title_fontsize=11,
    )

    # Annotations
    add_label(
        ax,
        0.8,
        1.9,
        'PROBLEM: Kolumna "Telefony" zawiera LISTY wartosci (nieatomowe).',
        fontsize=9,
        color="black",
    )
    add_label(
        ax,
        0.8,
        1.55,
        'Redundancja: "Anna", "W4", "EiTI", "Bazy danych" powtorzone wielokrotnie.',
        fontsize=9,
        color="black",
    )
    add_label(
        ax,
        0.8,
        1.2,
        (
            "Zaleznosci funkcyjne:  StID -> Imie, WydzialID"
            "    |    WydzialID -> NazwaWydzialu"
        ),
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax,
        0.8,
        0.9,
        (
            "  KursID -> NazwaKursu    |    (StID,KursID)"
            " -> Prowadzacy    |    Prowadzacy -> KursID"
        ),
        fontsize=8,
        color="#333333",
    )

    fig.savefig(
        str(Path(OUTPUT_DIR) / "nf_0nf_table.png"),
        bbox_inches="tight",
        facecolor="white",
        pad_inches=0.2,
    )
    plt.close(fig)
    logger.info("Generated: nf_0nf_table.png")


# ============================================================
# DIAGRAM 2: 1NF — atomic values
# ============================================================
def draw_1nf() -> None:
    """Draw 1nf."""
    fig, ax = create_figure(11.69, 6.0)

    # Main table after removing Telefony
    headers1 = [
        "StID*",
        "Imie",
        "KursID*",
        "NazwaKursu",
        "Prowadzacy",
        "WydzialID",
        "NazwaWydzialu",
    ]
    rows1 = [
        ["1", "Anna", "K10", "Bazy danych", "Kowalski", "W4", "EiTI"],
        ["1", "Anna", "K20", "Algorytmy", "Nowak", "W4", "EiTI"],
        ["2", "Jan", "K10", "Bazy danych", "Kowalski", "W4", "EiTI"],
        ["3", "Ewa", "K30", "Optyka", "Wisniewski", "W2", "Fizyka"],
    ]
    cw1 = [0.55, 0.55, 0.7, 1.1, 1.05, 0.85, 1.2]

    draw_table(
        ax,
        0.5,
        5.2,
        "1NF: Rejestr  (klucz: StID, KursID)",
        headers1,
        rows1,
        cw1,
        title_fontsize=10,
    )

    # Telefony table
    headers2 = ["StID*", "Telefon*"]
    rows2 = [
        ["1", "111-222"],
        ["1", "333-444"],
        ["2", "555-666"],
        ["3", "777-888"],
    ]
    cw2 = [0.55, 0.85]

    draw_table(
        ax,
        7.5,
        5.2,
        "Telefony  (klucz: StID, Telefon)",
        headers2,
        rows2,
        cw2,
        title_fontsize=10,
    )

    # Arrow
    add_arrow(ax, 6.6, 4.3, 7.4, 4.3, "wydzielono", "#333333")

    # Annotations
    add_label(
        ax,
        0.5,
        2.6,
        'KROK: Nieatomowa kolumna "Telefony" wydzielona do osobnej tabeli.',
        fontsize=9,
    )
    add_label(
        ax,
        0.5,
        2.25,
        "Kazda komorka zawiera JEDNA wartosc. Klucz glowny wyznaczony.",
        fontsize=9,
    )
    add_label(
        ax,
        0.5,
        1.85,
        "PROBLEM 2NF: NazwaKursu zalezy TYLKO od KursID (czesc klucza).",
        fontsize=9,
        color="black",
    )
    add_label(
        ax,
        0.5,
        1.5,
        (
            "             Imie, WydzialID, NazwaWydzialu"
            " zaleza TYLKO od StID (czesc klucza)."
        ),
        fontsize=9,
        color="black",
    )
    add_label(
        ax,
        0.5,
        1.15,
        "  --> Czesciowe zaleznosci od klucza zlozonego = NARUSZENIE 2NF.",
        fontsize=9,
        color="black",
    )

    fig.savefig(
        str(Path(OUTPUT_DIR) / "nf_1nf_tables.png"),
        bbox_inches="tight",
        facecolor="white",
        pad_inches=0.2,
    )
    plt.close(fig)
    logger.info("Generated: nf_1nf_tables.png")


# ============================================================
# DIAGRAM 3: 2NF — no partial dependencies
# ============================================================
def draw_2nf() -> None:
    """Draw 2nf."""
    fig, ax = create_figure(11.69, 6.5)

    # Studenci
    h1 = ["StID*", "Imie", "WydzialID", "NazwaWydzialu"]
    r1 = [
        ["1", "Anna", "W4", "EiTI"],
        ["2", "Jan", "W4", "EiTI"],
        ["3", "Ewa", "W2", "Fizyka"],
    ]
    cw1 = [0.55, 0.55, 0.85, 1.2]
    draw_table(
        ax,
        0.3,
        5.8,
        "Studenci (kl: StID)",
        h1,
        r1,
        cw1,
        highlight_cols={2, 3},
        title_fontsize=9,
    )

    # Kursy
    h2 = ["KursID*", "NazwaKursu"]
    r2 = [["K10", "Bazy danych"], ["K20", "Algorytmy"], ["K30", "Optyka"]]
    cw2 = [0.7, 1.1]
    draw_table(ax, 4.0, 5.8, "Kursy (kl: KursID)", h2, r2, cw2, title_fontsize=9)

    # Zapisy
    h3 = ["StID*", "KursID*", "Prowadzacy"]
    r3 = [
        ["1", "K10", "Kowalski"],
        ["1", "K20", "Nowak"],
        ["2", "K10", "Kowalski"],
        ["3", "K30", "Wisniewski"],
    ]
    cw3 = [0.55, 0.7, 1.05]
    draw_table(ax, 6.8, 5.8, "Zapisy (kl: StID, KursID)", h3, r3, cw3, title_fontsize=9)

    # Telefony
    h4 = ["StID*", "Telefon*"]
    r4 = [["1", "111-222"], ["1", "333-444"], ["2", "555-666"], ["3", "777-888"]]
    cw4 = [0.55, 0.85]
    draw_table(ax, 9.5, 5.8, "Telefony", h4, r4, cw4, title_fontsize=9)

    # Annotations
    add_label(
        ax,
        0.3,
        3.3,
        (
            "KROK: Rozbito czesc. zaleznosci"
            " — atrybuty zalezne od czesci klucza wydzielone."
        ),
        fontsize=9,
    )
    add_label(
        ax,
        0.3,
        2.95,
        "  StID -> Imie, WydzialID, NazwaWydzialu  ==>  tabela Studenci",
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax,
        0.3,
        2.65,
        "  KursID -> NazwaKursu                    ==>  tabela Kursy",
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax,
        0.3,
        2.3,
        'PROBLEM 3NF w "Studenci": StID -> WydzialID -> NazwaWydzialu',
        fontsize=9,
        color="black",
    )
    add_label(
        ax,
        0.3,
        1.95,
        "  NazwaWydzialu zalezy od WydzialID (nie-klucz), nie bezposrednio od StID.",
        fontsize=9,
        color="black",
    )
    add_label(
        ax,
        0.3,
        1.6,
        "  --> Zaleznosc PRZECHODNIA = NARUSZENIE 3NF.",
        fontsize=9,
        color="black",
    )

    fig.savefig(
        str(Path(OUTPUT_DIR) / "nf_2nf_tables.png"),
        bbox_inches="tight",
        facecolor="white",
        pad_inches=0.2,
    )
    plt.close(fig)
    logger.info("Generated: nf_2nf_tables.png")


# ============================================================
# DIAGRAM 4: 3NF — no transitive dependencies
# ============================================================
def draw_3nf() -> None:
    """Draw 3nf."""
    fig, ax = create_figure(11.69, 6.5)

    # Student table after removing transitive dependency
    h1 = ["StID*", "Imie", "WydzialID"]
    r1 = [["1", "Anna", "W4"], ["2", "Jan", "W4"], ["3", "Ewa", "W2"]]
    cw1 = [0.55, 0.55, 0.85]
    draw_table(ax, 0.3, 5.8, "Studenci (kl: StID)", h1, r1, cw1, title_fontsize=9)

    # Wydzialy (new!)
    h2 = ["WydzialID*", "NazwaWydzialu"]
    r2 = [["W4", "EiTI"], ["W2", "Fizyka"]]
    cw2 = [0.85, 1.2]
    draw_table(ax, 2.6, 5.8, "Wydzialy (kl: WydzialID)", h2, r2, cw2, title_fontsize=9)

    # Kursy
    h3 = ["KursID*", "NazwaKursu"]
    r3 = [["K10", "Bazy danych"], ["K20", "Algorytmy"], ["K30", "Optyka"]]
    cw3 = [0.7, 1.1]
    draw_table(ax, 5.2, 5.8, "Kursy (kl: KursID)", h3, r3, cw3, title_fontsize=9)

    # Zapisy (highlight BCNF violation)
    h4 = ["StID*", "KursID*", "Prowadzacy"]
    r4 = [
        ["1", "K10", "Kowalski"],
        ["1", "K20", "Nowak"],
        ["2", "K10", "Kowalski"],
        ["3", "K30", "Wisniewski"],
    ]
    cw4 = [0.55, 0.7, 1.05]
    draw_table(
        ax,
        7.8,
        5.8,
        "Zapisy (kl: StID, KursID)",
        h4,
        r4,
        cw4,
        highlight_cols={1, 2},
        title_fontsize=9,
    )

    # Annotations
    add_label(
        ax,
        0.3,
        3.3,
        "KROK: Rozdzielono Studenci -> Studenci + Wydzialy (usun. zal. przechodnia).",
        fontsize=9,
    )
    add_label(
        ax,
        0.3,
        2.95,
        "  StID -> WydzialID -> NazwaWydzialu"
        "  rozbito: NazwaWydzialu w osobnej tabeli.",
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax,
        0.3,
        2.55,
        'PROBLEM BCNF w "Zapisy":  FD: Prowadzacy -> KursID (1 prowadzacy = 1 kurs)',
        fontsize=9,
        color="black",
    )
    add_label(
        ax,
        0.3,
        2.2,
        "  Prowadzacy NIE jest nadkluczem tabeli Zapisy -> NARUSZENIE BCNF.",
        fontsize=9,
        color="black",
    )
    add_label(
        ax,
        0.3,
        1.85,
        "  3NF OK, bo KursID jest atrybutem pierwszym (prime) -> wyjatek 3NF.",
        fontsize=9,
        color="#333333",
    )
    add_label(
        ax,
        0.3,
        1.5,
        "  BCNF nie ma takiego wyjatku"
        " -> kazda nietrywialna FD wymaga nadklucza po lewej.",
        fontsize=9,
        color="#333333",
    )

    fig.savefig(
        str(Path(OUTPUT_DIR) / "nf_3nf_tables.png"),
        bbox_inches="tight",
        facecolor="white",
        pad_inches=0.2,
    )
    plt.close(fig)
    logger.info("Generated: nf_3nf_tables.png")


# ============================================================
# DIAGRAM 5: BCNF — every determinant is a superkey
# ============================================================
def draw_bcnf() -> None:
    """Draw bcnf."""
    fig, ax = create_figure(11.69, 7.5)

    # Studenci
    h1 = ["StID*", "Imie", "WydzialID"]
    r1 = [["1", "Anna", "W4"], ["2", "Jan", "W4"], ["3", "Ewa", "W2"]]
    cw1 = [0.55, 0.55, 0.85]
    draw_table(ax, 0.3, 6.8, "Studenci", h1, r1, cw1, title_fontsize=9)

    # Wydzialy
    h2 = ["WydzialID*", "NazwaWydz."]
    r2 = [["W4", "EiTI"], ["W2", "Fizyka"]]
    cw2 = [0.85, 1.0]
    draw_table(ax, 2.5, 6.8, "Wydzialy", h2, r2, cw2, title_fontsize=9)

    # Kursy
    h3 = ["KursID*", "NazwaKursu"]
    r3 = [["K10", "Bazy danych"], ["K20", "Algorytmy"], ["K30", "Optyka"]]
    cw3 = [0.7, 1.1]
    draw_table(ax, 4.8, 6.8, "Kursy", h3, r3, cw3, title_fontsize=9)

    # ProwadzacyKurs (NEW - from BCNF decomposition)
    h4 = ["Prowadzacy*", "KursID"]
    r4 = [["Kowalski", "K10"], ["Nowak", "K20"], ["Wisniewski", "K30"]]
    cw4 = [1.05, 0.7]
    draw_table(
        ax, 7.2, 6.8, "ProwadzacyKurs (kl: Prow.)", h4, r4, cw4, title_fontsize=9
    )

    # New student-advisor junction table
    h5 = ["StID*", "Prowadzacy*"]
    r5 = [["1", "Kowalski"], ["1", "Nowak"], ["2", "Kowalski"], ["3", "Wisniewski"]]
    cw5 = [0.55, 1.05]
    draw_table(ax, 9.5, 6.8, "StudentProw. (kl: oba)", h5, r5, cw5, title_fontsize=9)

    # Telefony
    h6 = ["StID*", "Telefon*"]
    r6 = [["1", "111-222"], ["1", "333-444"], ["2", "555-666"], ["3", "777-888"]]
    cw6 = [0.55, 0.85]
    draw_table(ax, 0.3, 4.6, "Telefony", h6, r6, cw6, title_fontsize=9)

    # Annotations
    add_label(
        ax, 0.3, 2.9, "KROK: Zapisy(StID, KursID, Prowadzacy) rozbite na:", fontsize=9
    )
    add_label(
        ax,
        0.3,
        2.55,
        "  ProwadzacyKurs(Prowadzacy, KursID)"
        "  — FD: Prowadzacy -> KursID, klucz: Prowadzacy",
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax,
        0.3,
        2.25,
        "  StudentProwadzacy(StID, Prowadzacy)  — ktory student u ktorego prowadzacego",
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax,
        0.3,
        1.85,
        "Teraz KAZDA nietrywialna FD ma nadklucz po lewej stronie -> BCNF spelnione.",
        fontsize=9,
    )
    add_label(
        ax,
        0.3,
        1.45,
        "Rekonstrukcja: StudentProw. JOIN ProwadzacyKurs"
        " ON Prowadzacy -> odtworzenie Zapisy.",
        fontsize=8,
        color="#333333",
    )

    fig.savefig(
        str(Path(OUTPUT_DIR) / "nf_bcnf_tables.png"),
        bbox_inches="tight",
        facecolor="white",
        pad_inches=0.2,
    )
    plt.close(fig)
    logger.info("Generated: nf_bcnf_tables.png")


# ============================================================
# DIAGRAM 6: 4NF example — multi-valued dependencies
# ============================================================
def draw_4nf() -> None:
    """Draw 4nf."""
    fig, ax = create_figure(11.69, 7.5)

    # Before: table with MVD violation
    h_before = ["StID*", "Hobby*", "Umiejetnosc*"]
    r_before = [
        ["1", "Szachy", "Python"],
        ["1", "Szachy", "SQL"],
        ["1", "Bieganie", "Python"],
        ["1", "Bieganie", "SQL"],
        ["2", "Plywanie", "Java"],
    ]
    cw_before = [0.55, 0.9, 1.0]
    draw_table(
        ax,
        0.5,
        6.8,
        "PRZED: StudentAktywnosci (klucz: StID, Hobby, Umiejetnosc)",
        h_before,
        r_before,
        cw_before,
        highlight_cols={1, 2},
        title_fontsize=10,
    )

    # Arrows
    add_label(ax, 3.5, 6.3, "StID  ->>  Hobby", fontsize=9, color="black")
    add_label(ax, 3.5, 6.0, "StID  ->>  Umiejetnosc", fontsize=9, color="black")
    add_label(ax, 3.5, 5.6, "NIEZALEZNE MVD w jednej tabeli", fontsize=9, color="black")
    add_label(
        ax,
        3.5,
        5.2,
        "= iloczyn kartezjanski = NARUSZENIE 4NF",
        fontsize=9,
        color="black",
    )

    # After: two tables
    add_arrow(ax, 3.0, 4.2, 3.0, 3.7, "", "#333333")
    add_label(ax, 3.2, 3.95, "dekompozycja", fontsize=8, color="#333333")

    h_hobby = ["StID*", "Hobby*"]
    r_hobby = [["1", "Szachy"], ["1", "Bieganie"], ["2", "Plywanie"]]
    cw_hobby = [0.55, 0.9]
    draw_table(
        ax, 0.5, 3.5, "PO: StudentHobby", h_hobby, r_hobby, cw_hobby, title_fontsize=10
    )

    h_skill = ["StID*", "Umiejetnosc*"]
    r_skill = [["1", "Python"], ["1", "SQL"], ["2", "Java"]]
    cw_skill = [0.55, 1.0]
    draw_table(
        ax,
        3.5,
        3.5,
        "PO: StudentUmiejetnosc",
        h_skill,
        r_skill,
        cw_skill,
        title_fontsize=10,
    )

    # Summary on the right side
    add_label(ax, 6.5, 6.5, "4NF: BCNF + brak nietrywialnych MVD", fontsize=10)
    add_label(
        ax, 6.5, 6.1, "MVD X ->> Y: jeden X = ZBIOR Y-ow,", fontsize=8, color="#333333"
    )
    add_label(
        ax, 6.5, 5.8, "niezaleznie od reszty kolumn.", fontsize=8, color="#333333"
    )
    add_label(
        ax, 6.5, 5.35, "Naruszenie: Student 1 ma 2 hobby i 2 umiejetnosci", fontsize=8
    )
    add_label(
        ax, 6.5, 5.05, "  -> 2 x 2 = 4 wiersze (iloczyn kartezjanski!)", fontsize=8
    )
    add_label(
        ax, 6.5, 4.65, "Naprawa: rozdziel niezalezne MVD do osobnych tabel.", fontsize=8
    )
    add_label(
        ax,
        6.5,
        4.25,
        "Po dekompozycji: 3 + 3 = 6 wierszy zamiast 5 z ilocz.",
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax, 6.5, 3.85, "  (ale BEZ sztucznych kombinacji!)", fontsize=8, color="#333333"
    )

    # Key insight box
    rect = mpatches.FancyBboxPatch(
        (6.3, 2.5),
        5.0,
        1.0,
        boxstyle="round,pad=0.1",
        facecolor="#F0F0F0",
        edgecolor="black",
        linewidth=1.0,
    )
    ax.add_patch(rect)
    add_label(ax, 6.5, 3.2, "ROZNICA 4NF vs BCNF:", fontsize=9)
    add_label(
        ax,
        6.5,
        2.85,
        "BCNF dotyczy FD (X -> Y, jedna wartosc)",
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax,
        6.5,
        2.55,
        "4NF dotyczy MVD (X ->> Y, zbior wartosci)",
        fontsize=8,
        color="#333333",
    )

    fig.savefig(
        str(Path(OUTPUT_DIR) / "nf_4nf_example.png"),
        bbox_inches="tight",
        facecolor="white",
        pad_inches=0.2,
    )
    plt.close(fig)
    logger.info("Generated: nf_4nf_example.png")


# ============================================================
# DIAGRAM 7: 5NF example — join dependencies
# ============================================================
def draw_5nf() -> None:
    """Draw 5nf."""
    fig, ax = create_figure(11.69, 8.5)

    # Before: ternary table
    h_before = ["Dostawca*", "Czesc*", "Projekt*"]
    r_before = [
        ["Alfa", "Sruba", "Most"],
        ["Alfa", "Sruba", "Wiezowiec"],
        ["Alfa", "Nakretka", "Most"],
        ["Beta", "Sruba", "Wiezowiec"],
        ["Beta", "Nakretka", "Wiezowiec"],
    ]
    cw_before = [0.9, 0.9, 1.0]
    draw_table(
        ax,
        0.5,
        7.8,
        "PRZED: Dostawy (klucz: Dostawca, Czesc, Projekt)",
        h_before,
        r_before,
        cw_before,
        title_fontsize=10,
    )

    add_label(ax, 3.8, 7.3, "Tabela w 4NF (brak nietrywialnych MVD),", fontsize=8)
    add_label(
        ax, 3.8, 7.0, "ale NIE w 5NF jesli zachodzi regula cykliczna:", fontsize=8
    )
    add_label(
        ax, 3.8, 6.55, "Jesli Dostawca dostarcza Czesc", fontsize=8, color="#333333"
    )
    add_label(
        ax, 3.8, 6.25, "  I Dostawca dostarcza do Projektu", fontsize=8, color="#333333"
    )
    add_label(
        ax, 3.8, 5.95, "  I Czesc jest uzywana w Projekcie", fontsize=8, color="#333333"
    )
    add_label(
        ax,
        3.8,
        5.65,
        "  ==> Dostawca dostarcza te Czesc do tego Projektu.",
        fontsize=8,
        color="black",
    )

    # Arrow down
    add_arrow(ax, 1.8, 5.1, 1.8, 4.6, "dekompozycja 5NF", "#333333")

    # After: three binary tables
    h1 = ["Dostawca*", "Czesc*"]
    r1 = [
        ["Alfa", "Sruba"],
        ["Alfa", "Nakretka"],
        ["Beta", "Sruba"],
        ["Beta", "Nakretka"],
    ]
    cw1 = [0.9, 0.9]
    draw_table(ax, 0.3, 4.3, "DostawcaCzesc", h1, r1, cw1, title_fontsize=9)

    h2 = ["Dostawca*", "Projekt*"]
    r2 = [["Alfa", "Most"], ["Alfa", "Wiezowiec"], ["Beta", "Wiezowiec"]]
    cw2 = [0.9, 1.0]
    draw_table(ax, 3.0, 4.3, "DostawcaProjekt", h2, r2, cw2, title_fontsize=9)

    h3 = ["Czesc*", "Projekt*"]
    r3 = [
        ["Sruba", "Most"],
        ["Sruba", "Wiezowiec"],
        ["Nakretka", "Most"],
        ["Nakretka", "Wiezowiec"],
    ]
    cw3 = [0.9, 1.0]
    draw_table(ax, 5.7, 4.3, "CzescProjekt", h3, r3, cw3, title_fontsize=9)

    # Join reconstruction note
    rect = mpatches.FancyBboxPatch(
        (8.3, 3.5),
        3.0,
        4.0,
        boxstyle="round,pad=0.1",
        facecolor="#F0F0F0",
        edgecolor="black",
        linewidth=1.0,
    )
    ax.add_patch(rect)

    add_label(ax, 8.5, 7.2, "5NF (PJNF):", fontsize=10)
    add_label(ax, 8.5, 6.8, "Project-Join NF", fontsize=8, color="#333333")
    add_label(ax, 8.5, 6.35, "Kazda zaleznosc", fontsize=8)
    add_label(ax, 8.5, 6.05, "zlaczenia (JD)", fontsize=8)
    add_label(ax, 8.5, 5.75, "implikowana przez", fontsize=8)
    add_label(ax, 8.5, 5.45, "klucze kandydujace.", fontsize=8)
    add_label(ax, 8.5, 4.9, "Rekonstrukcja:", fontsize=9)
    add_label(ax, 8.5, 4.55, "DC JOIN DP JOIN CP", fontsize=8, color="#333333")
    add_label(ax, 8.5, 4.2, "= oryginalna tabela", fontsize=8, color="#333333")
    add_label(ax, 8.5, 3.75, "(bezstratnie!)", fontsize=8, color="#333333")

    # Verification example at the bottom
    add_label(
        ax,
        0.3,
        2.0,
        "Weryfikacja: Alfa dostarcza Nakretke?"
        " Alfa -> Wiezowiec? Nakretka -> Wiezowiec?",
        fontsize=8,
    )
    add_label(
        ax,
        0.3,
        1.65,
        "  TAK, TAK, TAK --> wg reguly cyklicznej:"
        " Alfa dostarcza Nakretke do Wiezowca.",
        fontsize=8,
        color="#333333",
    )
    add_label(
        ax,
        0.3,
        1.25,
        "Ale: Alfa dostarcza Nakretke? TAK. Alfa -> Most? TAK. Nakretka -> Most? TAK.",
        fontsize=8,
    )
    add_label(
        ax,
        0.3,
        0.9,
        "  --> Alfa dostarcza Nakretke do Mostu."
        " (Tego wiersza NIE MA w oryginale -- BLAD!)",
        fontsize=8,
        color="black",
    )
    add_label(
        ax,
        0.3,
        0.5,
        "  Dekompozycja 5NF jest poprawna TYLKO"
        " jesli regula cykliczna rzeczywiscie zachodzi!",
        fontsize=8,
        color="black",
    )

    fig.savefig(
        str(Path(OUTPUT_DIR) / "nf_5nf_example.png"),
        bbox_inches="tight",
        facecolor="white",
        pad_inches=0.2,
    )
    plt.close(fig)
    logger.info("Generated: nf_5nf_example.png")


# ============================================================
# DIAGRAM 8: Full normalization summary flowchart
# ============================================================
def draw_summary_flow() -> None:
    """Draw summary flow."""
    fig, ax = create_figure(11.69, 6.0)

    # Boxes for each NF
    box_y = 4.5
    box_h = 1.8
    box_w = 1.4
    gap = 0.25

    nf_data = [
        ("0NF", "Nienormalna", "Listy w\nkomorkach,\nbrak klucza"),
        ("1NF", "Atomowosc", "Kazda komorka\n= 1 wartosc,\njest klucz"),
        ("2NF", "Pelny klucz", "Brak czesciowej\nzaleznosci od\nklucza zlozonego"),
        ("3NF", "Tylko klucz", "Brak zaleznosci\nprzechodniej\nA->B->C"),
        ("BCNF", "Nadklucz", "Lewa strona\nkazdej FD\n= nadklucz"),
        ("4NF", "Brak MVD", "Brak nietryw.\nwielowart.\nzaleznosci"),
        ("5NF", "Brak JD", "Kazda zal.\nzlaczenia\nimpl. kluczem"),
    ]

    for i, (name, subtitle, desc) in enumerate(nf_data):
        x = 0.3 + i * (box_w + gap)

        # Main box
        rect = mpatches.FancyBboxPatch(
            (x, box_y - box_h),
            box_w,
            box_h,
            boxstyle="round,pad=0.05",
            facecolor="#F5F5F5" if i == 0 else "#FFFFFF",
            edgecolor="black",
            linewidth=1.2,
        )
        ax.add_patch(rect)

        # NF name
        ax.text(
            x + box_w / 2,
            box_y - 0.15,
            name,
            fontsize=12,
            fontweight="bold",
            ha="center",
            va="top",
            family="monospace",
        )

        # Subtitle
        ax.text(
            x + box_w / 2,
            box_y - 0.45,
            subtitle,
            fontsize=7,
            ha="center",
            va="top",
            family="monospace",
            color="#333333",
        )

        # Description
        ax.text(
            x + box_w / 2,
            box_y - 0.75,
            desc,
            fontsize=6.5,
            ha="center",
            va="top",
            family="monospace",
            color="#555555",
            linespacing=1.3,
        )

        # Arrow to next
        if i < len(nf_data) - 1:
            ax.annotate(
                "",
                xy=(x + box_w + 0.02, box_y - box_h / 2),
                xytext=(x + box_w + gap - 0.02, box_y - box_h / 2),
                arrowprops={"arrowstyle": "<-", "color": "black", "lw": 1.5},
            )

    # Mnemonic quote at the bottom
    ax.text(
        5.85,
        2.2,
        '"Klucz, caly klucz i tylko klucz -- tak mi dopomoz Codd"',
        fontsize=11,
        ha="center",
        va="center",
        family="monospace",
        style="italic",
    )
    ax.text(
        5.85,
        1.8,
        "1NF: klucz istnieje  |  2NF: caly klucz  |  3NF: tylko klucz",
        fontsize=9,
        ha="center",
        va="center",
        family="monospace",
        color="#333333",
    )
    ax.text(
        5.85,
        1.4,
        "BCNF: kazdy determinant = nadklucz  |  4NF: +brak MVD  |  5NF: +brak JD",
        fontsize=9,
        ha="center",
        va="center",
        family="monospace",
        color="#333333",
    )

    # Hierarchy
    ax.text(
        5.85,
        0.8,
        "5NF  (zawiera sie w)  4NF  (zaw.)  BCNF"
        "  (zaw.)  3NF  (zaw.)  2NF  (zaw.)  1NF",
        fontsize=8,
        ha="center",
        va="center",
        family="monospace",
        color="#555555",
    )

    fig.savefig(
        str(Path(OUTPUT_DIR) / "nf_summary_flow.png"),
        bbox_inches="tight",
        facecolor="white",
        pad_inches=0.2,
    )
    plt.close(fig)
    logger.info("Generated: nf_summary_flow.png")


# ============================================================
# Main
# ============================================================
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    logger.info("Generating normalization diagrams...")
    draw_0nf()
    draw_1nf()
    draw_2nf()
    draw_3nf()
    draw_bcnf()
    draw_4nf()
    draw_5nf()
    draw_summary_flow()
    logger.info("Done! All diagrams saved to %s", OUTPUT_DIR)
