"""
Module dedicated to document signature generation.

The contents of this module are internal to fpdf2, and not part of the public API.
They may change at any time without prior warning or any deprecation period,
in non-backward-compatible ways.
"""

import hashlib
from datetime import timezone
from unittest.mock import patch

from .errors import FPDFException
from .syntax import build_obj_dict, Name
from .syntax import create_dictionary_string as pdf_dict
from .util import buffer_subst


class Signature:
    def __init__(self, contact_info=None, location=None, m=None, reason=None):
        self.type = Name("Sig")
        self.filter = Name("Adobe.PPKLite")
        self.sub_filter = Name("adbe.pkcs7.detached")
        self.contact_info = contact_info
        "Information provided by the signer to enable a recipient to contact the signer to verify the signature"
        self.location = location
        "The CPU host name or physical location of the signing"
        self.m = m
        "The time of signing"
        self.reason = reason
        "The reason for the signing"
        self.byte_range = _SIGNATURE_BYTERANGE_PLACEHOLDER
        self.contents = "<" + _SIGNATURE_CONTENTS_PLACEHOLDER + ">"

    def serialize(self, _security_handler=None, _obj_id=None):
        obj_dict = build_obj_dict(
            {key: getattr(self, key) for key in dir(self)},
            _security_handler=_security_handler,
            _obj_id=_obj_id,
        )
        return pdf_dict(obj_dict)


def sign_content(signer, buffer, key, cert, extra_certs, hashalgo, sign_time):
    """
    Perform PDF signing based on the content of the buffer, performing substitutions on it.
    The signing operation does not alter the buffer size
    """
    # We start by substituting the ByteRange,
    # that defines which part of the document content the signature is based on.
    # This is basically ALL the content EXCEPT the signature content itself.
    sig_placeholder = _SIGNATURE_CONTENTS_PLACEHOLDER.encode("latin1")
    start_index = buffer.find(sig_placeholder)
    end_index = start_index + len(sig_placeholder)
    content_range = (0, start_index - 1, end_index + 1, len(buffer) - end_index - 1)
    # pylint: disable=consider-using-f-string
    buffer = buffer_subst(
        buffer,
        _SIGNATURE_BYTERANGE_PLACEHOLDER,
        "[%010d %010d %010d %010d]" % content_range,
    )

    # We compute the ByteRange hash, of everything before & after the placeholder:
    content_hash = hashlib.new(hashalgo)
    content_hash.update(buffer[: content_range[1]])  # before
    content_hash.update(buffer[content_range[2] :])  # after

    # This monkey-patching is needed, at the time of endesive v2.0.9,
    # to get control over signed_time, initialized by endesive.signer.sign() to be datetime.now():
    class mock_datetime:
        @staticmethod
        def now(tz):  # pylint: disable=unused-argument
            return sign_time.astimezone(timezone.utc)

    sign = patch("endesive.signer.datetime", mock_datetime)(signer.sign)

    contents = sign(
        datau=None,
        key=key,
        cert=cert,
        othercerts=extra_certs,
        hashalgo=hashalgo,
        attrs=True,
        signed_value=content_hash.digest(),
    )
    contents = _pkcs11_aligned(contents).encode("latin1")
    # Sanity check, otherwise we will break the xref table:
    if len(sig_placeholder) != len(contents):
        raise FPDFException(
            "Generated signature has an invalid size and would corrupt the PDF "
            "cross-reference table."
        )
    return buffer.replace(sig_placeholder, contents, 1)


def _pkcs11_aligned(data):
    data = "".join(f"{i:02x}" for i in data)
    return data + "0" * (0x4000 - len(data))


_SIGNATURE_BYTERANGE_PLACEHOLDER = "[0000000000 0000000000 0000000000 0000000000]"
_SIGNATURE_CONTENTS_PLACEHOLDER = _pkcs11_aligned((0,))
