"""Function: Encrypt/decrypt all PDF files in the current directory.

This module provides functionality to encrypt and decrypt PDF files
using strong encryption algorithms. It supports batch processing
of multiple files with configurable passwords and security settings.
"""

from __future__ import annotations

import argparse
import contextlib
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import partial
from pathlib import Path
from typing import Any, Callable, Final

import pypdf
from pytola_core.config import ConfigMixin

# Constants

CWD: Final[Path] = Path.cwd()

logger = logging.getLogger(__name__)

__version__ = "1.0.0"
__build__ = "20260204"


class PDFCryptConfig(ConfigMixin):
    """Configuration for PDF encryption/decryption operations."""

    MAX_WORKERS: int = 4
    MIN_PASSWORD_LENGTH: int = 6
    DEFAULT_PASSWORD: str = ""
    OUTPUT_SUFFIX_ENCRYPTED: str = ".enc.pdf"
    OUTPUT_SUFFIX_DECRYPTED: str = ".dec.pdf"


# Global configuration instance
conf = PDFCryptConfig()


def is_encrypted(filepath: Path) -> bool:
    """Check if a PDF file is encrypted.

    Args:
        filepath: Path to the PDF file

    Returns
    -------
        bool: True if file is encrypted, False otherwise
    """
    try:
        return pypdf.PdfReader(filepath).is_encrypted
    except (OSError, ValueError, RuntimeError):
        logger.exception(f"Failed to check encryption status for {filepath}")
        return False


def _validate_password_strength(password: str) -> None:
    """Validate password strength and log warnings if needed.

    Args:
        password: Password to validate
    """
    if len(password) < conf.MIN_PASSWORD_LENGTH:
        logger.warning(
            f"Password length less than {conf.MIN_PASSWORD_LENGTH} characters, recommend using a stronger password",
        )


def _setup_pdf_writer(reader: pypdf.PdfReader) -> pypdf.PdfWriter:
    """Create and configure PDF writer with pages from reader.

    Args:
        reader: Source PDF reader

    Returns
    -------
        Configured PDF writer
    """
    writer = pypdf.PdfWriter()
    for page in reader.pages:
        writer.add_page(page)
    return writer


def _handle_output_file_conflict(filepath: Path) -> None:
    """Log warning if output file already exists.

    Args:
        filepath: Target file path
    """
    if filepath.exists():
        logger.warning(f"Target file already exists, will overwrite: {filepath}")


def _cleanup_resources(reader: pypdf.PdfReader | None = None, writer: pypdf.PdfWriter | None = None) -> None:
    """Clean up PDF resources properly.

    Args:
        reader: PDF reader to close
        writer: PDF writer to close
    """
    if reader:
        with contextlib.suppress(Exception):
            reader.stream.close()
    # pypdf.PdfWriter has no explicit close method
    if writer:
        with contextlib.suppress(Exception):
            writer.close()


def encrypt_pdf(
    filepath: Path,
    password: str,
) -> tuple[Path, Path | None]:
    """Encrypt a single PDF file.

    Args:
        filepath: Path to the PDF file to encrypt
        password: Encryption password

    Returns
    -------
        Tuple containing original file path and encrypted file path (if successful)
    """
    # Validate password strength
    _validate_password_strength(password)

    reader = None
    writer = None
    enc_pdf_file = filepath.with_suffix(conf.OUTPUT_SUFFIX_ENCRYPTED)

    try:
        # Use context manager to ensure resource cleanup
        with filepath.open("rb") as input_file:
            reader = pypdf.PdfReader(input_file)
            writer = _setup_pdf_writer(reader)

            writer.encrypt(
                user_password=password,
                owner_password=password,
                use_128bit=True,
            )

            # Check if target file already exists
            _handle_output_file_conflict(enc_pdf_file)

            with enc_pdf_file.open("wb") as output_file:
                writer.write(output_file)
    except OSError:
        logger.exception(f"Failed to write encrypted file [{enc_pdf_file.name}]")
        return filepath, None
    except (ValueError, RuntimeError):
        logger.exception(f"Error encrypting file [{filepath.name}]")
        return filepath, None
    else:
        logger.info(f"Successfully encrypted file: {enc_pdf_file}")
        return filepath, enc_pdf_file
    finally:
        # Explicitly clean up resources
        _cleanup_resources(reader, writer)


def _attempt_pdf_decryption(
    reader: pypdf.PdfReader,
    password: str,
    filepath: Path,
) -> bool:
    """Attempt to decrypt PDF file and log results.

    Args:
        reader: PDF reader instance
        password: Decryption password
        filepath: Path to the PDF file

    Returns
    -------
        True if decryption successful, False otherwise
    """
    if reader.decrypt(password):
        logger.info(f"Successfully decrypted file [{filepath.name}]!")
        return True
    logger.error(f"Failed to decrypt file [{filepath.name}], incorrect password.")
    return False


def _copy_pdf_pages(reader: pypdf.PdfReader) -> pypdf.PdfWriter:
    """Copy all pages from reader to a new writer.

    Args:
        reader: Source PDF reader

    Returns
    -------
        PDF writer with copied pages
    """
    writer = pypdf.PdfWriter()
    for page_num in range(len(reader.pages)):
        page = reader.pages[page_num]
        writer.add_page(page)
    return writer


def decrypt_pdf(
    filepath: Path,
    password: str,
) -> tuple[Path, Path | None]:
    """Decrypt a PDF file.

    Args:
        filepath: Path to the PDF file to decrypt
        password: Decryption password

    Returns
    -------
        Tuple containing original file path and decrypted file path (if successful)
    """
    reader = None
    writer = None

    try:
        # Open the input PDF file
        with filepath.open("rb") as f:
            reader = pypdf.PdfReader(f)

            # Try to decrypt the file
            if not _attempt_pdf_decryption(reader, password, filepath):
                return filepath, None

            # Create a new PdfWriter object and copy pages
            writer = _copy_pdf_pages(reader)

            # Write the decrypted PDF to output file
            outfile = filepath.with_suffix(conf.OUTPUT_SUFFIX_DECRYPTED)

            # Check if target file already exists
            _handle_output_file_conflict(outfile)

            with outfile.open("wb") as output_file:
                writer.write(output_file)
                logger.info(f"Wrote decrypted file to [{outfile}]")
                return filepath, outfile

    except OSError:
        logger.exception("Failed to write decrypted file")
        return filepath, None
    except (ValueError, RuntimeError):
        logger.exception(f"Error decrypting file [{filepath.name}]")
        return filepath, None
    finally:
        # Explicitly clean up resources
        _cleanup_resources(reader, writer)


def list_pdf() -> tuple[list[Path], list[Path]]:
    """List PDF files in current directory.

    Returns
    -------
        Tuple containing lists of unencrypted and encrypted PDF files
    """
    un_encrypted = [_ for _ in CWD.rglob("*.pdf") if not is_encrypted(_)]
    encrypted = [_ for _ in CWD.rglob("*.pdf") if is_encrypted(_)]

    logger.info(f"Encrypted files: [green bold]{encrypted}")
    logger.info(f"Unencrypted files: [green bold]{un_encrypted}")
    return un_encrypted, encrypted


def run_batch(func: Callable[[Path], Any], files: list[Path]) -> None:
    """Run function on multiple files concurrently.

    Args:
        func: Function to apply to each file
        files: List of files to process
    """
    if not files:
        logger.info("No files to process")
        return

    logger.info(f"Processing {len(files)} files...")

    with ThreadPoolExecutor(max_workers=conf.MAX_WORKERS) as executor:
        # Submit all tasks
        future_to_file = {executor.submit(func, file): file for file in files}

        # Process completed tasks
        for future in as_completed(future_to_file):
            file = future_to_file[future]
            try:
                result = future.result()
                if result[1]:  # If processing was successful
                    logger.info(f"Completed: {file.name}")
                else:
                    logger.error(f"Failed: {file.name}")
            except (OSError, RuntimeError, ValueError):
                logger.exception(f"Error processing {file.name}")


def decrypt(password: str) -> None:
    """Execute decryption operation.

    Args:
        password: Decryption password
    """
    _, encrypted_files = list_pdf()
    if not encrypted_files:
        logger.error(f"No encrypted PDF files found in directory: {CWD}")
        return

    dec_func = partial(decrypt_pdf, password=password)
    run_batch(dec_func, encrypted_files)


def encrypt(password: str) -> None:
    """Execute encryption operation.

    Args:
        password: Encryption password
    """
    unencrypted_files, _ = list_pdf()
    if not unencrypted_files:
        logger.error(f"No unencrypted PDF files found in directory: {CWD}")
        return

    enc_func = partial(encrypt_pdf, password=password)
    run_batch(enc_func, unencrypted_files)


def parse_args() -> argparse.Namespace:
    """Parse command line arguments.

    Returns
    -------
        Parsed arguments namespace
    """
    parser = argparse.ArgumentParser(
        description="Encrypt/decrypt PDF files in current directory",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  pdfcrypt encrypt mypassword           # Encrypt all unencrypted PDFs
  pdfcrypt decrypt mypassword           # Decrypt all encrypted PDFs
  pdfcrypt --help                       # Show this help message
        """,
    )

    parser.add_argument(
        "action",
        choices=["encrypt", "decrypt"],
        help="Action to perform: encrypt or decrypt PDF files",
    )

    parser.add_argument("password", help="Password for encryption/decryption")

    parser.add_argument(
        "--workers",
        type=int,
        default=4,
        help="Number of concurrent workers (default: 4)",
    )

    parser.add_argument(
        "-v",
        "--version",
        action="version",
        version=f"%(prog)s {__version__} (build {__build__})",
    )

    return parser.parse_args()


def main() -> None:
    """Run encryption/decryption operations based on command line arguments."""
    args = parse_args()

    # Update configuration
    conf.MAX_WORKERS = args.workers
    conf.DEFAULT_PASSWORD = args.password

    # Execute requested action
    if args.action == "encrypt":
        encrypt(args.password)
    elif args.action == "decrypt":
        decrypt(args.password)
    else:
        logger.error(f"Unknown action: {args.action}")


if __name__ == "__main__":
    main()
