# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# src/pdftl/operations/update_info.py

"""Update PDF metadata from a dump (as generated by dump_data)"""

import pdftl.core.constants as c
from pdftl.core.core_types import OpResult
from pdftl.core.registry import register_operation
from pdftl.exceptions import (
    InvalidArgumentError,
    MissingArgumentError,
    OperationError,
    PdftlConfigError,
    UserCommandLineError,
)

# from pdftl.operations.types.info_types import InfoSpec
from pdftl.info.info_types import PdfInfo
from pdftl.info.set_info import set_metadata_in_pdf
from pdftl.operations.parsers.update_info_parser import update_info_parser
from pdftl.utils.arg_helpers import resolve_operation_spec
from pdftl.utils.string_utils import recursive_decode, xml_decode_for_info
from pdftl.utils.user_input import filename_completer

_UPDATE_INFO_UTF8_LONG_DESC = """

Updates the document-level metadata and structural
information of the input PDF by reading from a specified
metadata file (or stdin).

This operation is the direct counterpart to
`dump_data_utf8`. It is identical to `update_info`, except
it assumes the input file contains raw UTF-8 strings that
have **not** been XML-escaped.

Use this operation if your metadata source file was created
by `dump_data_utf8` or if you have manually created a
metadata file that contains raw, unescaped UTF-8 characters.

For a complete description of the input format this command
expects, see the help for `dump_data`.

"""

_META_TXT_TEST_SETUP = {"copy_assets": {"meta.txt": "meta.txt"}}

_UPDATE_INFO_UTF8_EXAMPLES = [
    {
        "cmd": "in.pdf update_info_utf8 meta.txt output out.pdf",
        "desc": "Update utf-8 metadata according to meta.txt",
        "test_setup": _META_TXT_TEST_SETUP,
    },
]


_UPDATE_INFO_LONG_DESC = """

Updates the document-level metadata and structural
information of the input PDF by reading from a specified
`<metadata>` file (or stdin, if `<metadata>` is '-').
Alternatively, instructions can be specified in a structured
JSON file, loaded by setting `<metadata>` to
`@instructions.json`.

This operation consumes the plain-text format generated by
the `dump_data` command. It correctly parses the XML-escaped
strings created by that command.

This operation can read a full `dump_data` file in pdftk
'stanza' format, and will apply the following sections to
the output PDF:

* **Info:** Updates the Document Info dictionary (Title,
  Author, Subject, Keywords, CreationDate, ModDate, and
  custom keys).

* **Bookmarks:** Deletes all existing bookmarks and replaces
  them with the `Bookmark` stanzas from the input file.
  Target page numbers are 1-indexed.

* **PageMedia:** Applies page-level settings (Rotation,
  CropBox, TrimBox) as defined in `PageMedia` stanzas.
  Page numbers are 1-indexed.

* **PageLabels:** Replaces the document's page labelling
  scheme (e.g., "i, ii, iii") using the `PageLabel` stanzas
  from the input file. Indices are 1-indexed.

* **PdfID:** Updates the `PdfID0` top-level field (the
  creation ID).
  * You may provide a 32-character hex string to set a
      PdfID0. For example, `PdfID0: 1234abcd1234abcd1234abcd1234abcd`
  * **Fresh Bake:** If you set `PdfID0` to the string
      `RESET` (case-insensitive), the tool will remove
      the existing ID array. The underlying library will
      generate a brand new, matching ID pair (ID0=ID1)
      when saving.
  * `PdfID1` (the modification ID) is **not** updatable;
      it is automatically managed by the underlying PDF
      library to ensure specification compliance.

    Providing a `PdfID1` value in the input will result
    in a warning. It will otherwise be ignored.

For a complete description of the stanza-based input format
this command expects, see the help for `dump_data`.

The format of the JSON for `@instructions.json` is hopefully
intuitive. Key values for `update_info` should be
xml-escaped. (For `update_info_utf8` they should not be.)
Here is an example for `update_info`:

```
{
  "Info": {
    "Title": "Annual Financial Report &amp; Accounts 2025",
    "Author": "Acme Corp",
    "Subject": "Financial Data",
    "Keywords": "finance, report, 2025",
    "CustomKey": "Internal-ID-99"
  },
  "PdfID0": "38a4d9afd883e2750c63c2473ffd21e1",
  "BookmarkList": [
    {
      "Title": "Executive Summary",
      "Level": 1,
      "PageNumber": 1
    },
    {
      "Title": "Financial Statements",
      "Level": 1,
      "PageNumber": 5,
      "Children": [
        {
          "Title": "Income Statement",
          "Level": 2,
          "PageNumber": 6
        },
        {
          "Title": "Balance Sheet",
          "Level": 2,
          "PageNumber": 8
        }
      ]
    }
  ],
  "PageMediaList": [
    {
      "PageNumber": 1,
      "Rotation": 90
    }
  ],
  "PageLabelList": [
    {
      "Index": 1,
      "Style": "LowercaseRoman",
      "Start": 1
    },
    {
      "Index": 5,
      "Style": "DecimalArabic",
      "Start": 1
    }
  ]
}
```

Where it makes sense, you can omit keys in order to perform
a partial update.

"""

_UPDATE_INFO_EXAMPLES = [
    {
        "cmd": "in.pdf update_info meta.txt output out.pdf",
        "desc": "Update metadata according to meta.txt",
        "test_setup": _META_TXT_TEST_SETUP,
    },
]


@register_operation(
    "update_info_utf8",
    tags=["info", "metadata"],
    type="single input operation",
    desc="Update PDF metadata from `dump_data_utf8` instructions",
    long_desc=_UPDATE_INFO_UTF8_LONG_DESC,
    usage="<input> update_info_utf8 <metadata> output <filename>",
    examples=_UPDATE_INFO_UTF8_EXAMPLES,
    args=([c.INPUT_PDF, c.OPERATION_ARGS, c.GET_INPUT], {}, {"xml_strings": False}),
)
@register_operation(
    "update_info",
    tags=["info", "metadata"],
    type="single input operation",
    desc="Update PDF metadata from `dump_data` instructions",
    long_desc=_UPDATE_INFO_LONG_DESC,
    usage="<input> update_info <metadata> output <filename>",
    examples=_UPDATE_INFO_EXAMPLES,
    args=([c.INPUT_PDF, c.OPERATION_ARGS, c.GET_INPUT], {}, {"xml_strings": True}),
)
def update_info(pdf, op_args, get_input, xml_strings=True) -> OpResult:
    """CLI Adapter for `update_info`: parses string arguments into a spec, then runs logic.

    Expects `op_args` to have valid operation arguments (see
    pdftl help update_info).

    `xml_strings` controls whether to XML-decode string fields in the
    input.

    """
    if len(op_args) > 1:
        raise InvalidArgumentError(
            f"Unexpected argument(s) for update_info: {' '.join(op_args[1:])}"
        )
    if not op_args:
        raise MissingArgumentError("update_info requires a <metadata> argument")
    meta_filename = op_args[0]
    if meta_filename == "PROMPT":
        meta_filename = get_input(
            "Enter the filename of a metadata file in dump_data format: ",
            completer=filename_completer,
        )
    if meta_filename == "-":  # open stdout by passing None
        meta_filename = None
    try:
        # 1. Resolve arguments.
        # model_class=None ensures that if an @file is used, we get the raw dict
        # instead of an auto-converted PdfInfo object.
        result: PdfInfo | dict = resolve_operation_spec(
            [meta_filename],
            update_info_parser,
            model_class=None,
            data={"xml_strings": xml_strings},
        )

        # 2. Normalize to PdfInfo
        if isinstance(result, PdfInfo):
            # Path A: parser was used (legacy text file). It's already decoded and ready.
            spec = result

        elif isinstance(result, dict):
            # Path B: @file was used. We have a raw dict.
            # If we are in legacy mode, we must decode the XML entities in the JSON strings.
            if xml_strings:
                result = recursive_decode(result, xml_decode_for_info)

            spec = PdfInfo.from_dict(result)

        else:
            # internal error, want full traceback
            raise TypeError(f"Unexpected result type: {type(result)}")

        return execute_update_info(pdf, spec)

    except ValueError as exc:
        raise OperationError(exc) from exc
    except (OSError, PdftlConfigError) as exc:
        raise UserCommandLineError(exc) from exc


def execute_update_info(pdf, spec):
    """Update PDF info."""
    set_metadata_in_pdf(pdf, spec)
    return OpResult(success=True, pdf=pdf)
