#!/usr/bin/env python3
"""Generator driver: DBML -> Parser -> Helpers -> Jinja2 -> Python SQLite code.

Usage:
    python generate.py

Reads:  data/library.dbml
        templates/sqlite_library.py.jinja2
Writes: generated/library.py
"""

from __future__ import annotations

import re
from pathlib import Path

from jinja2 import Environment, FileSystemLoader, StrictUndefined

from helpers import build_query_context, build_seed_context, build_table_context

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------

PROJECT_DIR = Path(__file__).resolve().parent
DATA_FILE = PROJECT_DIR / "data" / "library.dbml"
TEMPLATE_DIR = PROJECT_DIR / "templates"
TEMPLATE_NAME = "sqlite_library.py.jinja2"
OUTPUT_DIR = PROJECT_DIR / "generated"
OUTPUT_FILE = OUTPUT_DIR / "library.py"


# ===================================================================
# Stage 1 — Parser: read DBML, produce in-memory representation
# ===================================================================


def parse_dbml(text: str) -> dict:
    """Parse a DBML string into tables, refs, and note blocks.

    Returns
    -------
    dict with keys:
        tables : list[dict]   — each has name, columns
        refs   : list[dict]   — each has from_table, from_column, to_table, to_column
        seeds  : list[dict]   — each has table, columns, rows
        queries: list[dict]   — each has name, sql
    """
    tables = _parse_tables(text)
    refs = _parse_refs(text, tables)
    seeds, queries = _parse_notes(text)
    return {
        "tables": tables,
        "refs": refs,
        "seeds": seeds,
        "queries": queries,
    }


# ---------------------------------------------------------------------------
# Table parsing
# ---------------------------------------------------------------------------

# Matches: Table name { ... }
_TABLE_RE = re.compile(
    r"Table\s+(\w+)\s*\{(.*?)\}", re.DOTALL
)

# Matches a column line:  name type [settings]
_COLUMN_RE = re.compile(
    r"^\s+(\w+)\s+(\w+)\s*(?:\[([^\]]*)\])?\s*$", re.MULTILINE
)


def _parse_tables(text: str) -> list[dict]:
    tables = []
    for m in _TABLE_RE.finditer(text):
        table_name = m.group(1)
        body = m.group(2)
        columns = []
        for cm in _COLUMN_RE.finditer(body):
            col_name = cm.group(1)
            col_type = cm.group(2)
            settings = cm.group(3) or ""
            columns.append(
                {
                    "name": col_name,
                    "type": col_type,
                    "is_pk": "pk" in settings,
                    "is_autoincrement": "increment" in settings,
                    "is_not_null": "not null" in settings,
                    "is_nullable": "null" in settings and "not null" not in settings,
                    "ref_inline": _parse_inline_ref(settings),
                }
            )
        tables.append({"name": table_name, "columns": columns})
    return tables


def _parse_inline_ref(settings: str) -> dict | None:
    """Parse an inline ref like ``ref: > authors.id`` from column settings."""
    m = re.search(r"ref:\s*>\s*(\w+)\.(\w+)", settings)
    if m:
        return {"to_table": m.group(1), "to_column": m.group(2)}
    return None


# ---------------------------------------------------------------------------
# Reference parsing  (both inline refs and standalone Ref: lines)
# ---------------------------------------------------------------------------

_STANDALONE_REF_RE = re.compile(
    r"Ref:\s*(\w+)\.(\w+)\s*>\s*(\w+)\.(\w+)"
)


def _parse_refs(text: str, tables: list[dict]) -> list[dict]:
    refs: list[dict] = []

    # Collect inline refs already parsed per-column.
    for table in tables:
        for col in table["columns"]:
            if col["ref_inline"]:
                refs.append(
                    {
                        "from_table": table["name"],
                        "from_column": col["name"],
                        "to_table": col["ref_inline"]["to_table"],
                        "to_column": col["ref_inline"]["to_column"],
                    }
                )

    # Collect standalone Ref: lines.
    for m in _STANDALONE_REF_RE.finditer(text):
        refs.append(
            {
                "from_table": m.group(1),
                "from_column": m.group(2),
                "to_table": m.group(3),
                "to_column": m.group(4),
            }
        )

    return refs


# ---------------------------------------------------------------------------
# Note block parsing  (seed data + queries)
# ---------------------------------------------------------------------------

_NOTE_RE = re.compile(r"Note\s*\{\s*'''(.*?)'''", re.DOTALL)

_SEED_RE = re.compile(
    r"seed:(\w+)\s*=\s*\(([^)]+)\)\s*VALUES\s*(.*?)(?=\n\s*(?:seed:|query:|\Z))",
    re.DOTALL,
)

_QUERY_RE = re.compile(
    r"query:(\w+)\s*=\s*(.*?)(?=\n\s*(?:seed:|query:|\Z))",
    re.DOTALL,
)

_ROW_RE = re.compile(r"\(([^)]+)\)")


def _parse_notes(text: str) -> tuple[list[dict], list[dict]]:
    seeds: list[dict] = []
    queries: list[dict] = []

    for note_match in _NOTE_RE.finditer(text):
        body = note_match.group(1)

        # --- seeds ---
        for sm in _SEED_RE.finditer(body):
            table = sm.group(1)
            columns = [c.strip() for c in sm.group(2).split(",")]
            rows_text = sm.group(3)
            rows = []
            for rm in _ROW_RE.finditer(rows_text):
                row_values = rm.group(1)
                rows.append(_split_row_values(row_values))
            seeds.append({"table": table, "columns": columns, "rows": rows})

        # --- queries ---
        for qm in _QUERY_RE.finditer(body):
            name = qm.group(1)
            sql = " ".join(qm.group(2).strip().split())
            queries.append({"name": name, "sql": sql})

    return seeds, queries


def _split_row_values(raw: str) -> list[str]:
    """Split a row like ``'hello', 42, NULL`` into individual value strings.

    Preserves string quoting and NULL as-is so the template can emit them
    directly as Python literals.
    """
    values: list[str] = []
    current = ""
    in_string = False
    for ch in raw:
        if ch == "'" and not in_string:
            in_string = True
            current += ch
        elif ch == "'" and in_string:
            in_string = False
            current += ch
        elif ch == "," and not in_string:
            values.append(current.strip())
            current = ""
        else:
            current += ch
    if current.strip():
        values.append(current.strip())

    # Convert NULL -> None for Python, keep strings and ints as-is.
    result = []
    for v in values:
        if v.upper() == "NULL":
            result.append("None")
        else:
            result.append(v)
    return result


# ===================================================================
# Stage 2 — Helpers: reshape parsed data for template consumption
# ===================================================================
# (Imported from helpers.py — see build_table_context, etc.)


# ===================================================================
# Stage 3 — Template rendering + output
# ===================================================================


def render(parsed: dict) -> str:
    """Render the Jinja2 template with parsed + reshaped data."""
    env = Environment(
        loader=FileSystemLoader(str(TEMPLATE_DIR)),
        undefined=StrictUndefined,
        trim_blocks=True,
        lstrip_blocks=True,
        keep_trailing_newline=True,
    )

    # Custom filter: replace any string with '?' for SQL placeholders.
    env.filters["regex_replace"] = lambda s, _pattern, _repl: "?"

    template = env.get_template(TEMPLATE_NAME)

    # Build template contexts via helpers.
    table_contexts = [
        build_table_context(t, parsed["refs"]) for t in parsed["tables"]
    ]
    seed_contexts = build_seed_context(parsed["seeds"])
    query_contexts = build_query_context(parsed["queries"])

    return template.render(
        source_file=DATA_FILE.name,
        tables=table_contexts,
        seeds=seed_contexts,
        queries=query_contexts,
    )


# ===================================================================
# Main — driver script
# ===================================================================


def main() -> None:
    print(f"Reading data model:  {DATA_FILE}")
    dbml_text = DATA_FILE.read_text()

    print("Parsing DBML...")
    parsed = parse_dbml(dbml_text)
    print(
        f"  Found {len(parsed['tables'])} tables, "
        f"{len(parsed['refs'])} refs, "
        f"{len(parsed['seeds'])} seed blocks, "
        f"{len(parsed['queries'])} queries."
    )

    print("Rendering template...")
    output = render(parsed)

    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    OUTPUT_FILE.write_text(output)
    print(f"Generated: {OUTPUT_FILE}")


if __name__ == "__main__":
    main()
