# src/pdftl/operations/update_bookmarks.py

"""Update PDF Table of Contents from a YAML or JSON file."""

import json
import logging

import pdftl.core.constants as c
from pdftl.core.registry import register_operation
from pdftl.core.types import OpResult
from pdftl.exceptions import MissingArgumentError, OperationError
from pdftl.info.toc import build_toc_tree
from pdftl.utils.dependencies import ensure_dependencies
from pdftl.utils.io_helpers import smart_open

logger = logging.getLogger(__name__)

_UPDATE_TOC_LONG_DESC = """
Replaces the document's Table of Contents (bookmarks) entirely using
a specified YAML or JSON file.

This operation consumes the structured format generated by the
`dump_bookmarks` command. It applies strict validation to prevent broken
PDFs: it validates page boundaries, catches typos in property names,
and ensures the hierarchical tree structure is sound.

### Important Rules & Behaviors
* **Full Replacement:** This command does not merge or append bookmarks. It completely
  overwrites the existing Table of Contents with the contents of your file.
* **Page Indexing:** Target `page` numbers must be 1-indexed (e.g., `page: 1`
  points to the very first physical page of the document).
* **Boundary Checking:** If a bookmark points to a page number that exceeds
  the total number of pages in the PDF, the operation will fail safely to
  prevent document corruption.
* **Strict Validation:** Unrecognized properties (such as a typoed `pagee` instead
  of `page`) will trigger an error rather than being silently ignored. This ensures
  your routing logic always behaves as expected.

### Clearing Bookmarks
If you want to completely remove the Table of Contents from a PDF, you
can provide a file containing just an empty list.
* **YAML file content:** `[]`
* **JSON file content:** `[]`

### Dependency note
Loading YAML files requires the `pyyaml` library. If it is not installed,
you can install it via `pip install pdftl[yaml-bookmarks]`, or simply use
`.json` files natively.
"""

_UPDATE_TOC_EXAMPLES = [
    {
        "desc": "Apply a YAML bookmarks file",
        "cmd": "in.pdf update_bookmarks bookmarks.yaml output modified.pdf",
    },
    {
        "desc": "Apply a JSON bookmarks file",
        "cmd": "in.pdf update_bookmarks bookmarks.json output modified.pdf",
    },
]


@register_operation(
    "update_bookmarks",
    tags=["info", "metadata", "bookmarks"],
    type="single input operation",
    desc="Replace PDF bookmarks from a YAML or JSON file",
    long_desc=_UPDATE_TOC_LONG_DESC,
    examples=_UPDATE_TOC_EXAMPLES,
    usage="<input> update_bookmarks <bookmarks_file> output <filename>",
    args=([c.INPUT_PDF, c.OPERATION_ARGS], {}),
)
def update_toc(pdf, op_args) -> OpResult:
    if not op_args:
        raise MissingArgumentError("update_bookmarks requires a <bookmarks_file> argument")

    file_path = op_args[0]

    with smart_open(file_path, mode="r") as f:
        try:
            if file_path.lower().endswith(".json"):
                toc_items = json.load(f)
            else:
                ensure_dependencies(
                    feature_name="YAML bookmarks loading",
                    dependencies={"yaml": "pyyaml"},
                    extra_tag="yaml",
                )
                import yaml

                toc_items = yaml.safe_load(f)

        except json.JSONDecodeError as exc:
            raise OperationError(f"Failed to parse JSON file: {exc}") from exc
        except Exception as exc:
            # Catching generic Exception here because we only conditionally import yaml
            # and yaml.YAMLError won't be available in scope if pyyaml isn't installed.
            if type(exc).__name__ in ("YAMLError", "ScannerError", "ParserError"):
                raise OperationError(f"Failed to parse YAML file: {exc}") from exc
            raise

    if not isinstance(toc_items, list):
        raise OperationError(
            "Invalid bookmarks format: Root element must be a list of items."
            " To delete all bookmarks, use []."
        )

    logger.debug("Got %s items for the new toc tree", len(toc_items))

    try:
        build_toc_tree(pdf, toc_items)
    except ValueError as exc:
        raise OperationError(exc) from exc

    return OpResult(success=True, pdf=pdf)
