#!/usr/bin/env python3
import os
import re
import csv
import io
import pandas as pd
from reportlab.lib import colors
from reportlab.lib.pagesizes import inch
from reportlab.platypus import Table, TableStyle, SimpleDocTemplate, Paragraph
from reportlab.lib.styles import ParagraphStyle
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
from reportlab.lib.enums import TA_LEFT, TA_CENTER
from xml.sax.saxutils import escape as xml_escape

# === USER CONFIGURATION === #
CSV_FILENAME       = "MCRO_language_search-terms-per-doc.csv"
OUTPUT_FILENAME    = "MCRO_language_search-terms-per-doc.pdf"
PDF_DATA_FILENAME  = "pdfData.csv"

TEST_MODE          = False
TEST_ROWS          = 2000

# page margins, in inches
PAGE_MARGIN_LEFT   = 0.5
PAGE_MARGIN_RIGHT  = 0.5
PAGE_MARGIN_TOP    = 0.75
PAGE_MARGIN_BOTTOM = 0.75
TOP_MARGIN_FIRST_PAGE_OFFSET = 0.0

DEFAULT_ROW_HEIGHT = 0.26 * inch

USE_MANUAL_WIDTHS       = True
MANUAL_WIDTHS_INCHES    = [9.75629921259843,1.54370078740157,1.15748031496063,3.95629921259842]
HORIZONTAL_CENTER_FLAGS = [0,1,1,0]

TEXT_INDENT_POINTS      = 4
COL_MULT                = 0.9

# ─── LEGACY HYPERLINK RULES (DISABLED) ───────────────────────────────────────
# We keep these here for reference, but they are not used anymore. All
# hyperlink behavior now comes from pdfData.csv row-3 rules.
HYPERLINK_ON    = False   # wrap existing MnCourtFraud.com URLs
LINK_MCRO_ON    = False   # wrap cells starting with "MCRO_27-"
LINK_CR_ON      = False   # wrap cells starting with "27-CR-"
LINK_PAD_TOP    = 2.5     # 3.75    # pts to shrink clickable rect top (kept for spacing)
LINK_PAD_BOTTOM = 2.5     # 1.7     # pts to shrink clickable rect bottom (kept for spacing)

# === FONT SETTINGS === #
USE_LIBERATION = True
FONT_REGULAR   = "Times-Roman"
FONT_BOLD      = "Times-Bold"
FONT_SIZE      = 12

try:
    if USE_LIBERATION:
        pdfmetrics.registerFont(TTFont(
            "LiberationSerif",
            "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf"
        ))
        pdfmetrics.registerFont(TTFont(
            "LiberationSerif-Bold",
            "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf"
        ))
        FONT_REGULAR = "LiberationSerif"
        FONT_BOLD    = "LiberationSerif-Bold"
except Exception:
    print("⚠️ Liberation Serif not found. Using Times.")


def make_style(is_header: bool = False, align: str = "LEFT") -> ParagraphStyle:
    """Create a ParagraphStyle for header/body cells."""
    return ParagraphStyle(
        name="Header" if is_header else "Cell",
        fontName=FONT_BOLD if is_header else FONT_REGULAR,
        fontSize=FONT_SIZE,
        leading=FONT_SIZE + 2,
        spaceBefore=0,
        spaceAfter=0,
        alignment=TA_CENTER if align == "CENTER" else TA_LEFT,
        leftIndent=0 if align == "CENTER" else TEXT_INDENT_POINTS,
    )


# ─── pdfData.csv PARSING HELPERS ─────────────────────────────────────────────

def parse_manual_widths(width_strs, num_cols):
    """Convert list of width strings to floats (in inches), up to num_cols.

    Stops at the first empty value. Handles occasional '...' truncations by
    cutting everything from the first '...' onward.
    """
    widths = []
    for raw in width_strs:
        if len(widths) >= num_cols:
            break
        s = (raw or "").strip()
        if not s:
            break
        # Handle things like '2.1437007...4724409449'
        s_clean = re.split(r"\.\.\.+", s)[0]
        try:
            widths.append(float(s_clean))
        except ValueError:
            print(f"⚠️ Could not parse manual width '{raw}' as float – ignoring the rest for this row.")
            break
    return widths


def parse_center_flags(center_strs, num_cols):
    """Parse row-2 '0/1' center flags into a list[int] of length num_cols."""
    flags = []
    for idx in range(num_cols):
        v = ""
        if idx < len(center_strs):
            v = (center_strs[idx] or "").strip()
        flags.append(1 if v == "1" else 0)
    return flags


def _parse_hyperlink_spec_cell(raw):
    """Parse a single row-3 cell like:
         '1,1, "https://mncourtfraud.com/file/" + VAL + ".pdf"'
    into a dict: {'color': bool, 'underline': bool, 'expr': str}
    or None if it can't be parsed.
    """
    s = (raw or "").strip()
    if not s:
        return None

    # Normalise smart quotes to plain double quotes.
    s = s.replace("“", '"').replace("”", '"')

    # Use CSV parsing so the expression after the second comma can contain commas.
    try:
        parts = next(csv.reader(io.StringIO(s)))
    except Exception:
        parts = [p.strip() for p in s.split(",", 2)]

    if len(parts) < 3:
        return None

    color_flag = parts[0].strip()
    underline_flag = parts[1].strip()
    expr = parts[2].strip()

    return {
        "color": (color_flag == "1"),
        "underline": (underline_flag == "1"),
        "expr": expr,
    }


def parse_hyperlink_specs(link_strs, num_cols):
    """Parse row-3 hyperlink cells into a list[spec or None] of length num_cols."""
    specs = [None] * num_cols
    for idx in range(num_cols):
        raw = ""
        if idx < len(link_strs):
            raw = (link_strs[idx] or "").strip()
        if not raw:
            continue
        spec = _parse_hyperlink_spec_cell(raw)
        if spec:
            specs[idx] = spec
    return specs


def build_link_url(cell_value, expr):
    """Build a URL from the row-3 expression and the cell's text value.

    Supports simple mini-expressions like:
        https://mncourtfraud.com/file/ + VAL + ".pdf"
        "https://mncourtfraud.com/file/" + VAL + ".json"
        VAL + ".json"
        https://mncourtfraud.com/file/ + VAL

    We split on '+' and treat tokens as either:
      - VAL          => replaced by the cell value
      - "literal"    => literal string (quotes stripped)
      - other text   => literal prefix/suffix
    """
    expr = (expr or "").replace("“", '"').replace("”", '"').strip()
    if not expr:
        return cell_value

    parts = [p.strip() for p in expr.split('+')]

    # If it's a concatenation expression or mentions VAL, evaluate it.
    if len(parts) > 1 or "VAL" in expr:
        out = ""
        for p in parts:
            if not p:
                continue
            if p == "VAL":
                out += cell_value
            else:
                m = re.match(r'^"(.*)"$', p)
                if m:
                    # "literal" => literal
                    out += m.group(1)
                else:
                    # Bare text (e.g. https://mncourtfraud.com/file/)
                    out += p
        return out

    # If no '+' and no VAL, but it is a single quoted literal, treat as prefix.
    m = re.match(r'^"(.*)"$', expr)
    if m:
        return m.group(1) + cell_value

    # Fallback: replace VAL if user wrote something weird.
    return expr.replace("VAL", cell_value)


def load_jobs_from_pdfdata(pdf_data_path):
    """Read pdfData.csv and return a list of job dicts.

    Each job dict has:
        - csv_filename
        - manual_width_strs
        - center_flag_strs
        - hyperlink_strs
    """
    jobs = []
    with open(pdf_data_path, newline="", encoding="utf-8") as f:
        reader = csv.reader(f)
        rows = list(reader)

    # Expect groups of 3 rows per CSV.
    for i in range(0, len(rows), 3):
        if i + 1 >= len(rows):
            break
        header = rows[i]
        centers = rows[i + 1] if i + 1 < len(rows) else []
        links = rows[i + 2] if i + 2 < len(rows) else []

        if not header or not (header[0] or "").strip():
            # Skip triplets without a filename.
            continue

        csv_filename = header[0].strip()
        manual_width_strs = header[1:]
        center_flag_strs = centers[1:] if centers else []
        hyperlink_strs = links[1:] if links else []

        jobs.append({
            "csv_filename": csv_filename,
            "manual_width_strs": manual_width_strs,
            "center_flag_strs": center_flag_strs,
            "hyperlink_strs": hyperlink_strs,
        })

    return jobs


# ─── MAIN CONVERSION LOGIC ───────────────────────────────────────────────────

def convert_csv_to_pdf(csv_path, output_path,
                       manual_width_strs,
                       center_flag_strs,
                       hyperlink_strs):
    """Convert one CSV file to a PDF using the supplied layout rules."""
    # Read everything as text; don't try to be clever about types.
    df = pd.read_csv(
        csv_path,
        dtype=str,
        low_memory=False,
        keep_default_na=False,
        na_filter=False,
    )

    if TEST_MODE:
        df = df.head(TEST_ROWS)

    num_cols = len(df.columns)

    # Interpret pdfData settings for this CSV.
    manual_widths = parse_manual_widths(manual_width_strs, num_cols)
    center_flags = parse_center_flags(center_flag_strs, num_cols)
    hyperlink_specs = parse_hyperlink_specs(hyperlink_strs, num_cols)

    # Prepare table data: header row
    data = []
    header_row = [
        Paragraph(xml_escape(str(col)), make_style(is_header=True, align="CENTER"))
        for col in df.columns
    ]
    data.append(header_row)

    # Body rows
    for _, row in df.iterrows():
        cells = []
        for idx, val in enumerate(row):
            # Treat everything as "just text"; empty or whitespace-only becomes blank.
            text_raw = "" if val is None else str(val)
            if text_raw is None:
                text_raw = ""
            if text_raw.strip() == "":
                text_html = ""
            else:
                spec = hyperlink_specs[idx] if idx < len(hyperlink_specs) else None
                if spec:
                    url = build_link_url(text_raw, spec.get("expr", ""))
                    inner = xml_escape(text_raw)
                    if spec.get("underline", False):
                        inner = f"<u>{inner}</u>"
                    if spec.get("color", False):
                        inner = f"<font color='#000080'>{inner}</font>"
                    text_html = f"<a href='{xml_escape(url)}'>{inner}</a>"
                else:
                    text_html = xml_escape(text_raw)

            align = "CENTER" if center_flags[idx] else "LEFT"
            cells.append(Paragraph(text_html, make_style(False, align)))
        data.append(cells)

    # Column widths
    if USE_MANUAL_WIDTHS and manual_widths and len(manual_widths) == num_cols:
        col_widths = [w * COL_MULT * inch for w in manual_widths]
    else:
        col_widths = [1.5 * inch] * num_cols

    # Page size
    table_width = sum(col_widths)
    page_width  = table_width + (PAGE_MARGIN_LEFT + PAGE_MARGIN_RIGHT) * inch
    page_height = 11 * inch + TOP_MARGIN_FIRST_PAGE_OFFSET * inch
    pagesize    = (page_width, page_height)
    row_heights = [DEFAULT_ROW_HEIGHT] * len(data)

    # Create & style table
    table = Table(data, colWidths=col_widths, repeatRows=1)
#                  rowHeights=row_heights, repeatRows=1)
    style = TableStyle()

    # Header style
    style.add("BACKGROUND",    (0, 0), (-1, 0), colors.HexColor("#eeeeee"))
    style.add("TEXTCOLOR",     (0, 0), (-1, 0), colors.black)
    style.add("ALIGN",         (0, 0), (-1, 0), "CENTER")
    style.add("VALIGN",        (0, 0), (-1, 0), "MIDDLE")
    style.add("FONTNAME",      (0, 0), (-1, 0), FONT_BOLD)
    style.add("FONTSIZE",      (0, 0), (-1, 0), FONT_SIZE)
    style.add("BOTTOMPADDING", (0, 0), (-1, 0), 6)
    style.add("TOPPADDING",    (0, 0), (-1, 0), 6)
    style.add("GRID",          (0, 0), (-1, 0), 1, colors.black)

    # Body style
    for col in range(num_cols):
        align = "CENTER" if center_flags[col] else "LEFT"
        style.add("ALIGN",         (col, 1), (col, -1), align)
        style.add("VALIGN",        (col, 1), (col, -1), "MIDDLE")
        style.add("GRID",          (col, 1), (col, -1), 0.75, colors.grey)
        style.add("TOPPADDING",    (col, 1), (col, -1), LINK_PAD_TOP)
        style.add("BOTTOMPADDING", (col, 1), (col, -1), LINK_PAD_BOTTOM)

    table.setStyle(style)

    # Build PDF
    doc = SimpleDocTemplate(
        output_path,
        pagesize=pagesize,
        leftMargin   = PAGE_MARGIN_LEFT * inch,
        rightMargin  = PAGE_MARGIN_RIGHT * inch,
        topMargin    = PAGE_MARGIN_TOP * inch + TOP_MARGIN_FIRST_PAGE_OFFSET * inch,
        bottomMargin = PAGE_MARGIN_BOTTOM * inch,
    )
    doc.build([table])
    print(f"   ✅ Created {output_path}")


def main():
    base_dir = os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals() else os.getcwd()
    pdf_data_path = os.path.join(base_dir, PDF_DATA_FILENAME)

    # Decide whether to use bulk pdfData.csv or single-file config.
    if os.path.exists(pdf_data_path):
        print(f"🔎 Using pdfData configuration from {pdf_data_path}")
        jobs = load_jobs_from_pdfdata(pdf_data_path)
    else:
        print("⚠️ pdfData.csv not found – falling back to single-file mode.")
        jobs = [{
            "csv_filename": CSV_FILENAME,
            "manual_width_strs": [str(w) for w in MANUAL_WIDTHS_INCHES],
            "center_flag_strs": [str(f) for f in HORIZONTAL_CENTER_FLAGS],
            "hyperlink_strs": [],
        }]

    if not jobs:
        print("⚠️ No jobs found in pdfData.csv; nothing to do.")
        return

    for job in jobs:
        csv_filename = job["csv_filename"]
        csv_path = os.path.join(base_dir, csv_filename)
        if not os.path.exists(csv_path):
            print(f"⚠️ CSV not found, skipping: {csv_path}")
            continue

        output_name = os.path.splitext(csv_filename)[0] + ".pdf"
        output_path = os.path.join(base_dir, output_name)

        print(f"▶️ Converting {csv_filename} → {output_name}")
        convert_csv_to_pdf(
            csv_path,
            output_path,
            job.get("manual_width_strs", []),
            job.get("center_flag_strs", []),
            job.get("hyperlink_strs", []),
        )

    print("✅ All conversions complete.")


if __name__ == "__main__":
    main()
