"""
PDF security operations: certificates, digital signatures, and encryption.

This module provides Pythonic wrappers around the MicroPDF security FFI functions
for managing PDF document security including digital signatures, certificate
management, and encryption/decryption.

Example usage:
    from micropdf.security import Certificate, encrypt_pdf, decrypt_pdf, sign_pdf

    # Encrypt a PDF
    encrypt_pdf("input.pdf", "encrypted.pdf", user_password="secret")

    # Sign a PDF
    cert = Certificate.from_pkcs12("cert.p12", "password")
    sign_pdf("input.pdf", "signed.pdf", cert, field_name="Signature1")
    cert.drop()

    # Check if encrypted
    if is_encrypted("document.pdf"):
        decrypt_pdf("document.pdf", "decrypted.pdf", "password")
"""

from dataclasses import dataclass
from enum import IntEnum
from pathlib import Path
from typing import Optional, Union

from .ffi_native import ffi, lib, is_native_available
from .errors import MicroPDFError, ErrorCode


class EncryptionAlgorithm(IntEnum):
    """Encryption algorithm options."""
    RC4_40 = 0
    RC4_128 = 1
    AES_128 = 2
    AES_256 = 3


class PDFPermission(IntEnum):
    """PDF permission flags."""
    PRINT = 1 << 2
    MODIFY = 1 << 3
    COPY = 1 << 4
    ANNOTATE = 1 << 5
    FILL_FORMS = 1 << 8
    ACCESSIBILITY = 1 << 9
    ASSEMBLE = 1 << 10
    PRINT_HIGH_QUALITY = 1 << 11


@dataclass
class SignatureVerification:
    """Result of verifying a PDF signature."""
    valid: bool
    certificate_valid: bool
    document_modified: bool
    has_timestamp: bool
    signer_name: Optional[str]
    signing_time: int
    error_message: Optional[str]


def _check_native() -> None:
    """Check if native library is available."""
    if not is_native_available():
        raise MicroPDFError(
            ErrorCode.SYSTEM,
            "Native library not available. Set MICROPDF_LIB_PATH or install micropdf with native bindings."
        )


def _check_result(result: int, operation: str) -> None:
    """Check FFI result code and raise on error."""
    if result < 0:
        raise MicroPDFError(ErrorCode.GENERIC, f"{operation}: error code {result}")


def _to_path_bytes(path: Union[str, Path]) -> bytes:
    """Convert path to UTF-8 encoded bytes."""
    return str(path).encode('utf-8')


class Certificate:
    """A digital certificate for signing PDFs."""

    __slots__ = ('_handle',)

    def __init__(self, handle: int) -> None:
        self._handle = handle

    @classmethod
    def from_pkcs12(cls, path: Union[str, Path], password: str) -> 'Certificate':
        """
        Load a PKCS#12 (.p12/.pfx) certificate.

        Args:
            path: Path to the PKCS#12 file.
            password: Password for the certificate.

        Returns:
            Certificate instance.

        Raises:
            MicroPDFError: If loading fails.
        """
        _check_native()
        path_bytes = _to_path_bytes(path)
        pwd_bytes = password.encode('utf-8')
        handle = lib.mp_certificate_load_pkcs12(path_bytes, pwd_bytes)
        if handle == 0:
            raise MicroPDFError(ErrorCode.GENERIC, "Failed to load PKCS#12 certificate")
        return cls(handle)

    @classmethod
    def from_pem(
        cls,
        cert_path: Union[str, Path],
        key_path: Union[str, Path],
        key_password: Optional[str] = None,
    ) -> 'Certificate':
        """
        Load a PEM certificate with separate key file.

        Args:
            cert_path: Path to the PEM certificate file.
            key_path: Path to the private key file.
            key_password: Optional password for the private key.

        Returns:
            Certificate instance.

        Raises:
            MicroPDFError: If loading fails.
        """
        _check_native()
        cert_bytes = _to_path_bytes(cert_path)
        key_bytes = _to_path_bytes(key_path)
        pwd_bytes = key_password.encode('utf-8') if key_password else ffi.NULL
        handle = lib.mp_certificate_load_pem(cert_bytes, key_bytes, pwd_bytes)
        if handle == 0:
            raise MicroPDFError(ErrorCode.GENERIC, "Failed to load PEM certificate")
        return cls(handle)

    @property
    def subject(self) -> Optional[str]:
        """Get the certificate subject distinguished name."""
        _check_native()
        ptr = lib.mp_certificate_get_subject(self._handle)
        if ptr == ffi.NULL:
            return None
        return ffi.string(ptr).decode('utf-8')

    @property
    def issuer(self) -> Optional[str]:
        """Get the certificate issuer distinguished name."""
        _check_native()
        ptr = lib.mp_certificate_get_issuer(self._handle)
        if ptr == ffi.NULL:
            return None
        return ffi.string(ptr).decode('utf-8')

    @property
    def is_valid(self) -> bool:
        """Check if the certificate is currently valid."""
        _check_native()
        return lib.mp_certificate_is_valid(self._handle) == 1

    def drop(self) -> None:
        """Release the certificate resources."""
        if getattr(self, '_handle', 0) != 0:
            lib.mp_certificate_drop(self._handle)
            self._handle = 0

    def __del__(self) -> None:
        try:
            self.drop()
        except Exception:
            pass

    def __enter__(self) -> 'Certificate':
        return self

    def __exit__(self, *args: object) -> None:
        self.drop()


class EncryptionOptions:
    """Builder for PDF encryption options."""

    __slots__ = ('_handle',)

    def __init__(self) -> None:
        _check_native()
        self._handle = lib.mp_encryption_options_new()
        if self._handle == 0:
            raise MicroPDFError(ErrorCode.GENERIC, "Failed to create encryption options")

    def set_user_password(self, password: str) -> 'EncryptionOptions':
        """Set the user password (required to open the document)."""
        _check_result(
            lib.mp_encryption_set_user_password(self._handle, password.encode('utf-8')),
            "set_user_password"
        )
        return self

    def set_owner_password(self, password: str) -> 'EncryptionOptions':
        """Set the owner password (required for full access)."""
        _check_result(
            lib.mp_encryption_set_owner_password(self._handle, password.encode('utf-8')),
            "set_owner_password"
        )
        return self

    def set_algorithm(self, algorithm: EncryptionAlgorithm) -> 'EncryptionOptions':
        """Set the encryption algorithm."""
        _check_result(
            lib.mp_encryption_set_algorithm(self._handle, int(algorithm)),
            "set_algorithm"
        )
        return self

    def set_permissions(self, permissions: int) -> 'EncryptionOptions':
        """
        Set document permissions as a bitmask of PDFPermission values.

        Args:
            permissions: Bitmask of PDFPermission flags.
        """
        _check_result(
            lib.mp_encryption_set_permissions(self._handle, permissions),
            "set_permissions"
        )
        return self

    def drop(self) -> None:
        """Release the encryption options resources."""
        if getattr(self, '_handle', 0) != 0:
            lib.mp_encryption_options_drop(self._handle)
            self._handle = 0

    def __del__(self) -> None:
        try:
            self.drop()
        except Exception:
            pass

    def __enter__(self) -> 'EncryptionOptions':
        return self

    def __exit__(self, *args: object) -> None:
        self.drop()


def sign_pdf(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    certificate: Certificate,
    field_name: str = "Signature1",
    page: int = 0,
    x: float = 0.0,
    y: float = 0.0,
    width: float = 200.0,
    height: float = 50.0,
    reason: Optional[str] = None,
    location: Optional[str] = None,
) -> None:
    """
    Add a visible digital signature to a PDF.

    Args:
        input_path: Path to the input PDF.
        output_path: Path for the signed output PDF.
        certificate: Certificate to use for signing.
        field_name: Name of the signature field.
        page: Zero-based page number for the signature.
        x: X coordinate of signature rectangle.
        y: Y coordinate of signature rectangle.
        width: Width of signature rectangle.
        height: Height of signature rectangle.
        reason: Reason for signing.
        location: Location of signing.

    Raises:
        MicroPDFError: If signing fails.
    """
    _check_native()
    result = lib.mp_signature_create(
        _to_path_bytes(input_path),
        _to_path_bytes(output_path),
        certificate._handle,
        field_name.encode('utf-8'),
        page,
        x, y, width, height,
        reason.encode('utf-8') if reason else ffi.NULL,
        location.encode('utf-8') if location else ffi.NULL,
    )
    _check_result(result, "sign_pdf")


def sign_pdf_invisible(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    certificate: Certificate,
    field_name: str = "Signature1",
    reason: Optional[str] = None,
    location: Optional[str] = None,
) -> None:
    """
    Add an invisible digital signature to a PDF.

    Args:
        input_path: Path to the input PDF.
        output_path: Path for the signed output PDF.
        certificate: Certificate to use for signing.
        field_name: Name of the signature field.
        reason: Reason for signing.
        location: Location of signing.

    Raises:
        MicroPDFError: If signing fails.
    """
    _check_native()
    result = lib.mp_signature_create_invisible(
        _to_path_bytes(input_path),
        _to_path_bytes(output_path),
        certificate._handle,
        field_name.encode('utf-8'),
        reason.encode('utf-8') if reason else ffi.NULL,
        location.encode('utf-8') if location else ffi.NULL,
    )
    _check_result(result, "sign_pdf_invisible")


def verify_signature(
    pdf_path: Union[str, Path],
    field_name: str,
) -> SignatureVerification:
    """
    Verify a digital signature in a PDF.

    Args:
        pdf_path: Path to the signed PDF.
        field_name: Name of the signature field to verify.

    Returns:
        SignatureVerification with the verification results.

    Raises:
        MicroPDFError: If verification fails.
    """
    _check_native()
    result_struct = ffi.new("SignatureVerifyResult*")
    result = lib.mp_signature_verify(
        _to_path_bytes(pdf_path),
        field_name.encode('utf-8'),
        result_struct,
    )
    _check_result(result, "verify_signature")

    try:
        signer = None
        if result_struct.signer_name != ffi.NULL:
            signer = ffi.string(result_struct.signer_name).decode('utf-8')

        error_msg = None
        if result_struct.error_message != ffi.NULL:
            error_msg = ffi.string(result_struct.error_message).decode('utf-8')

        return SignatureVerification(
            valid=bool(result_struct.valid),
            certificate_valid=bool(result_struct.certificate_valid),
            document_modified=bool(result_struct.document_modified),
            has_timestamp=bool(result_struct.has_timestamp),
            signer_name=signer,
            signing_time=result_struct.signing_time,
            error_message=error_msg,
        )
    finally:
        lib.mp_signature_verify_result_free(result_struct)


def signature_count(pdf_path: Union[str, Path]) -> int:
    """
    Count the number of signatures in a PDF.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        Number of signatures.

    Raises:
        MicroPDFError: If the operation fails.
    """
    _check_native()
    result = lib.mp_signature_count(_to_path_bytes(pdf_path))
    if result < 0:
        _check_result(result, "signature_count")
    return result


def encrypt_pdf(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    options: Optional[EncryptionOptions] = None,
    user_password: Optional[str] = None,
    owner_password: Optional[str] = None,
    algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_256,
) -> None:
    """
    Encrypt a PDF file.

    Can be called with an EncryptionOptions object for full control, or with
    simple password arguments for convenience.

    Args:
        input_path: Path to the input PDF.
        output_path: Path for the encrypted output PDF.
        options: Pre-configured EncryptionOptions (takes precedence).
        user_password: User password (used if options is None).
        owner_password: Owner password (used if options is None).
        algorithm: Encryption algorithm (used if options is None).

    Raises:
        MicroPDFError: If encryption fails.
    """
    _check_native()

    if options is not None:
        result = lib.mp_encrypt_pdf(
            _to_path_bytes(input_path),
            _to_path_bytes(output_path),
            options._handle,
        )
        _check_result(result, "encrypt_pdf")
    else:
        opts = EncryptionOptions()
        try:
            if user_password:
                opts.set_user_password(user_password)
            if owner_password:
                opts.set_owner_password(owner_password)
            opts.set_algorithm(algorithm)
            result = lib.mp_encrypt_pdf(
                _to_path_bytes(input_path),
                _to_path_bytes(output_path),
                opts._handle,
            )
            _check_result(result, "encrypt_pdf")
        finally:
            opts.drop()


def decrypt_pdf(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    password: str,
) -> None:
    """
    Decrypt a PDF file.

    Args:
        input_path: Path to the encrypted PDF.
        output_path: Path for the decrypted output PDF.
        password: Password to decrypt the PDF.

    Raises:
        MicroPDFError: If decryption fails.
    """
    _check_native()
    result = lib.mp_decrypt_pdf(
        _to_path_bytes(input_path),
        _to_path_bytes(output_path),
        password.encode('utf-8'),
    )
    _check_result(result, "decrypt_pdf")


def is_encrypted(pdf_path: Union[str, Path]) -> bool:
    """
    Check if a PDF file is encrypted.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        True if the PDF is encrypted, False otherwise.

    Raises:
        MicroPDFError: If the check fails.
    """
    _check_native()
    result = lib.mp_is_encrypted(_to_path_bytes(pdf_path))
    if result < 0:
        _check_result(result, "is_encrypted")
    return result == 1


__all__ = [
    "EncryptionAlgorithm",
    "PDFPermission",
    "SignatureVerification",
    "Certificate",
    "EncryptionOptions",
    "sign_pdf",
    "sign_pdf_invisible",
    "verify_signature",
    "signature_count",
    "encrypt_pdf",
    "decrypt_pdf",
    "is_encrypted",
]
