#!/usr/bin/env python3
"""Faculty Office-artifact preservation helpers for .docx files.

Per CLAUDE.md Edit Discipline: when deriving a new artifact from a
faculty-authored Office file, COPY the original and modify minimally
(append or relabel). Never rebuild from scratch. This module provides the
helpers that enforce that rule for .docx files.

Functions
---------
copy_docx(src, dst)
    Copy a .docx file verbatim to a new path, preserving all content.

append_heading(doc_path, text, level=1)
    Append a paragraph styled as Heading <level> to an existing .docx.

append_table(doc_path, rows, *, header_row=True)
    Append a bordered table (list-of-rows) to the end of an existing .docx,
    leaving all existing content untouched.

Usage (CLI, optional)
---------------------
  python docx_append.py copy SRC.docx DST.docx
  python docx_append.py heading DST.docx "New Section" --level 2
  python docx_append.py table DST.docx row1_col1 row1_col2 -- row2_col1 row2_col2
"""

import argparse
import shutil
import sys
from pathlib import Path

try:
    from docx import Document
    from docx.shared import Pt
    from docx.oxml.ns import qn
    from docx.oxml import OxmlElement
    _DOCX_AVAILABLE = True
except ImportError:
    _DOCX_AVAILABLE = False


def _require_docx():
    if not _DOCX_AVAILABLE:
        raise ImportError(
            "python-docx is required. Install with: pip install python-docx==1.1.2"
        )


def copy_docx(src: str, dst: str) -> Path:
    """Copy a .docx file verbatim to a new path.

    The copy preserves every byte of the original — styles, tables, images,
    tracked changes, comments. Returns the destination Path.

    Args:
        src: Path to the source .docx file.
        dst: Path for the destination .docx file.

    Returns:
        Path object pointing to the copied file.

    Raises:
        FileNotFoundError: if src does not exist.
    """
    src_path = Path(src)
    if not src_path.is_file():
        raise FileNotFoundError(f"Source .docx not found: {src_path}")
    dst_path = Path(dst)
    dst_path.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(str(src_path), str(dst_path))
    return dst_path


def append_heading(doc_path: str, text: str, level: int = 1) -> None:
    """Append a heading paragraph to an existing .docx, leaving all prior content untouched.

    Uses the built-in Word heading style ('Heading 1', 'Heading 2', ...).
    If the style is not present in the document's style table, python-docx
    creates it automatically.

    Args:
        doc_path: Path to the .docx file to modify in-place.
        text: Heading text to append.
        level: Heading level (1-9). Default 1.

    Raises:
        ImportError: if python-docx is not installed.
        FileNotFoundError: if doc_path does not exist.
    """
    _require_docx()
    p = Path(doc_path)
    if not p.is_file():
        raise FileNotFoundError(f".docx not found: {p}")
    doc = Document(str(p))
    doc.add_heading(text, level=level)
    doc.save(str(p))


def _set_cell_border(cell):
    """Apply a thin black border to all sides of a table cell."""
    tc = cell._tc
    tcPr = tc.get_or_add_tcPr()
    tcBorders = OxmlElement("w:tcBorders")
    for side in ("top", "left", "bottom", "right"):
        border = OxmlElement(f"w:{side}")
        border.set(qn("w:val"), "single")
        border.set(qn("w:sz"), "4")
        border.set(qn("w:space"), "0")
        border.set(qn("w:color"), "000000")
        tcBorders.append(border)
    tcPr.append(tcBorders)


def append_table(
    doc_path: str,
    rows: list,
    *,
    header_row: bool = True,
) -> None:
    """Append a bordered table to an existing .docx, leaving all prior content untouched.

    Args:
        doc_path: Path to the .docx file to modify in-place.
        rows: List of rows; each row is a list of cell strings.
              All rows must have the same number of columns.
        header_row: If True, the first row is bold (header style). Default True.

    Raises:
        ImportError: if python-docx is not installed.
        FileNotFoundError: if doc_path does not exist.
        ValueError: if rows is empty or rows have inconsistent column counts.
    """
    _require_docx()
    p = Path(doc_path)
    if not p.is_file():
        raise FileNotFoundError(f".docx not found: {p}")
    if not rows:
        raise ValueError("rows must contain at least one row")
    col_counts = {len(r) for r in rows}
    if len(col_counts) > 1:
        raise ValueError(
            f"All rows must have the same column count; got counts: {sorted(col_counts)}"
        )

    doc = Document(str(p))
    num_cols = len(rows[0])
    table = doc.add_table(rows=len(rows), cols=num_cols)
    table.style = "Table Grid"

    for row_idx, row_data in enumerate(rows):
        row = table.rows[row_idx]
        for col_idx, cell_text in enumerate(row_data):
            cell = row.cells[col_idx]
            cell.text = str(cell_text)
            _set_cell_border(cell)
            if header_row and row_idx == 0:
                for paragraph in cell.paragraphs:
                    for run in paragraph.runs:
                        run.bold = True

    doc.save(str(p))


# ── CLI (optional) ─────────────────────────────────────────────────────────

def _build_parser():
    p = argparse.ArgumentParser(
        description="Faculty .docx preservation helpers — copy, append headings, append tables."
    )
    sub = p.add_subparsers(dest="command")

    cp = sub.add_parser("copy", help="Copy SRC.docx to DST.docx verbatim")
    cp.add_argument("src", help="Source .docx path")
    cp.add_argument("dst", help="Destination .docx path")

    hd = sub.add_parser("heading", help="Append a heading paragraph to DOCX")
    hd.add_argument("docx", help=".docx file path (modified in-place)")
    hd.add_argument("text", help="Heading text")
    hd.add_argument("--level", type=int, default=1, help="Heading level 1-9 (default 1)")

    tb = sub.add_parser("table", help="Append a table to DOCX from row arguments")
    tb.add_argument("docx", help=".docx file path (modified in-place)")
    tb.add_argument(
        "cells",
        nargs="+",
        help="Cell values in row-major order; use '--' to delimit rows",
    )
    tb.add_argument(
        "--cols",
        type=int,
        required=True,
        help="Number of columns per row",
    )
    tb.add_argument(
        "--no-header",
        action="store_true",
        help="Do not bold the first row as a header",
    )
    return p


def main(argv=None):
    parser = _build_parser()
    args = parser.parse_args(argv)

    if args.command == "copy":
        dst = copy_docx(args.src, args.dst)
        print(f"Copied to {dst}")

    elif args.command == "heading":
        append_heading(args.docx, args.text, level=args.level)
        print(f"Appended Heading {args.level}: {args.text!r} to {args.docx}")

    elif args.command == "table":
        cells = args.cells
        cols = args.cols
        if len(cells) % cols != 0:
            parser.error(
                f"{len(cells)} cell values cannot be evenly divided into {cols} columns"
            )
        rows = [cells[i: i + cols] for i in range(0, len(cells), cols)]
        append_table(args.docx, rows, header_row=not args.no_header)
        print(f"Appended {len(rows)}-row table to {args.docx}")

    else:
        parser.print_help()
        return 1

    return 0


if __name__ == "__main__":
    sys.exit(main())
