"""PDF page extraction and assembly using pikepdf."""

import getpass
import warnings
from pathlib import Path
from typing import Union

import pikepdf

# pikepdf emits UserWarnings when XMP and DocumentInfo date fields are out of
# sync — common in PDFs created by third-party tools. These are harmless.
warnings.filterwarnings("ignore", category=UserWarning, module="pikepdf")


def parse_page_spec(
    spec: Union[str, int],
    page_count: int,
    filename: str = "",
) -> list[int]:
    """Parse a page specification into a list of 0-based page indices.

    Supports:
      all        -> every page
      5          -> page 5 only
      1-3        -> pages 1 through 3
      1,3,5      -> pages 1, 3, and 5
      1-3,5,7-9  -> mixed ranges and singles

    Page numbers in spec are 1-based (user-facing).
    """
    spec = str(spec).strip().lower()
    file_hint = f" in '{Path(filename).name}'" if filename else ""

    if spec == "all":
        return list(range(page_count))

    indices = []
    errors = []
    for part in spec.split(","):
        part = part.strip()
        if "-" in part:
            start_s, end_s = part.split("-", 1)
            start, end = int(start_s.strip()), int(end_s.strip())
            if start < 1 or end > page_count or start > end:
                errors.append(
                    f"  range '{start}-{end}' is invalid{file_hint} "
                    f"({page_count} pages total)"
                )
            else:
                indices.extend(range(start - 1, end))
        else:
            page = int(part)
            if page < 1 or page > page_count:
                errors.append(
                    f"  page {page} does not exist{file_hint} "
                    f"({page_count} pages total)"
                )
            else:
                indices.append(page - 1)

    if errors:
        raise ValueError("\n".join(errors))

    return indices


def get_pdf_info(path: str) -> dict:
    """Return page count and metadata for a PDF."""
    try:
        with pikepdf.open(path) as pdf:
            with pdf.open_metadata() as m:
                title = m.get("dc:title") or str(pdf.docinfo.get("/Title", ""))
                author = m.get("dc:creator") or str(pdf.docinfo.get("/Author", ""))
                # XMP fields can return lists (e.g. dc:creator is multi-value)
                if isinstance(title, list):
                    title = ", ".join(title)
                if isinstance(author, list):
                    author = ", ".join(author)
            return {
                "page_count": len(pdf.pages),
                "metadata": {"title": title, "author": author},
            }
    except pikepdf.PasswordError:
        raise RuntimeError(
            f"'{path}' is password-protected. PageFuse cannot open encrypted PDFs."
        )


def assemble(
    sources: list[dict],
    output_path: str,
    metadata: dict | None = None,
) -> None:
    """Assemble pages from multiple source PDFs into a single output PDF.

    Each source dict must have:
      - "pdf_path": str  — path to the (possibly converted) PDF
      - "pages":    str  — page specification (default "all")

    metadata (optional) may contain any of:
      - "title"   — defaults to the output filename stem
      - "author"  — override author field
      - "subject" — override subject field

    Explicit metadata takes priority over values copied from the first source.
    Annotations travel with their pages automatically.
    """
    output = pikepdf.Pdf.new()
    first_docinfo: dict[str, str] | None = None

    # --- validation pass: collect all page-spec errors before writing anything ---
    all_errors = []
    for source in sources:
        pdf_path = source["pdf_path"]
        page_spec = str(source.get("pages", "all"))
        display_name = source.get("original_path", pdf_path)
        try:
            with pikepdf.open(pdf_path) as src:
                parse_page_spec(page_spec, len(src.pages), filename=display_name)
        except pikepdf.PasswordError:
            all_errors.append(f"  '{display_name}' is password-protected")
        except ValueError as exc:
            all_errors.append(str(exc))

    if all_errors:
        raise ValueError("Page specification errors:\n" + "\n".join(all_errors))

    # --- assembly pass ---
    for source in sources:
        pdf_path = source["pdf_path"]
        page_spec = str(source.get("pages", "all"))

        try:
            src_cm = pikepdf.open(pdf_path)
        except pikepdf.PasswordError:
            raise RuntimeError(
                f"'{pdf_path}' is password-protected. PageFuse cannot open encrypted PDFs."
            )
        with src_cm as src:
            if first_docinfo is None:
                first_docinfo = {str(k): str(v) for k, v in src.docinfo.items()}
            indices = parse_page_spec(page_spec, len(src.pages), filename=pdf_path)
            for i in indices:
                output.pages.append(src.pages[i])

    # Start from first-source docinfo, then apply explicit overrides
    if first_docinfo:
        for k, v in first_docinfo.items():
            output.docinfo[k] = v

    meta = metadata or {}

    # Title: explicit > output filename stem
    output.docinfo["/Title"] = meta.get("title") or Path(output_path).stem

    # Author: explicit > first source > system username
    if not output.docinfo.get("/Author"):
        try:
            output.docinfo["/Author"] = getpass.getuser()
        except Exception:
            pass
    if meta.get("author"):
        output.docinfo["/Author"] = meta["author"]

    # Subject: explicit only — no default
    if meta.get("subject"):
        output.docinfo["/Subject"] = meta["subject"]

    output.docinfo["/Producer"] = "PageFuse"

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    output.save(output_path)
