"""Zip Encrypt Utility."""

from __future__ import annotations

import argparse
import logging
import shutil
import subprocess  # noqa: S404
import sys
import zipfile
from functools import cached_property
from pathlib import Path
from time import perf_counter
from typing import ClassVar

from pytola_core.config import ConfigMixin

# Initialize logger
logger = logging.getLogger(__name__)


class ZipEncryptConfig(ConfigMixin):
    """Configuration for ZIP encryption utility."""

    SKIP_PREFIXES: ClassVar[tuple[str, ...]] = (
        ".",
        "__",
    )

    password: str = "DEFAULT_PASSWORD"  # noqa: S105
    max_name_len: int = 20
    max_file_count: int = 5
    max_workers: int = 10

    @cached_property
    def available_tools(self) -> list[str]:
        """Get list of available compression tools."""
        tools = []
        if shutil.which("7z"):
            tools.append("7z")
        if shutil.which("zip"):
            tools.append("zip")
        if shutil.which("rar"):
            tools.append("rar")
        return tools

    @cached_property
    def has_encryption_support(self) -> bool:
        """Check if any encryption-capable tools are available."""
        return bool(self.available_tools)


def _execute_command(cmd: list[str]) -> None:
    """Execute external command and handle results.

    Args:
        cmd: Command and arguments to execute

    Raises
    ------
        subprocess.CalledProcessError: If command execution fails
    """
    # Convert all arguments to strings
    cmd_str = [str(arg) for arg in cmd]
    logger.debug(f"Executing command: {' '.join(cmd_str)}")
    try:
        result = subprocess.run(  # noqa: S603
            cmd_str,
            capture_output=True,
            text=True,
            check=True,
            timeout=300,  # 5 minute timeout
            shell=False,
        )
        logger.debug(f"Command output: {result.stdout.strip()}")
        if result.stderr:
            logger.warning(f"Command stderr: {result.stderr.strip()}")
    except subprocess.TimeoutExpired:
        logger.exception(f"Command timed out: {' '.join(cmd_str)}")
        raise
    except subprocess.CalledProcessError as e:
        logger.exception(f"Command failed with exit code {e.returncode}: {e.cmd}")
        logger.exception(f"stderr: {e.stderr}")
        raise
    except FileNotFoundError:
        logger.exception(f"Command not found: {cmd_str[0]}")
        raise


# Initialize global configuration after all classes/functions are defined
conf = ZipEncryptConfig()


def encrypt_zip(
    root_path: str | Path | None = None,
    password: str | None = None,
    *,
    replace: bool = False,
) -> None:
    """Encrypt files and directories into password-protected ZIP archives.

    Args:
        root_path: Directory containing files to encrypt. Defaults to current directory.
        password: Encryption password. If not provided, uses configuration default.
        replace: Whether to replace existing ZIP files. Defaults to False.
    """
    # Use current directory if not specified
    root_path = Path.cwd() if root_path is None else Path(root_path)

    # Use configured password if not provided
    if password is None:
        password = conf.password

    # Validate inputs
    if not password:
        logger.error("Password cannot be empty")
        sys.exit(1)

    if not root_path.exists():
        logger.error(f"Directory does not exist: {root_path}")
        sys.exit(1)

    if not root_path.is_dir():
        logger.error(f"Path is not a directory: {root_path}")
        sys.exit(1)

    # Get and process files sequentially
    files = _get_valid_entries(root_path)
    if not files:
        logger.warning(f"No target files found in {root_path}")
        return

    logger.info(f"Starting encryption of {len(files)} files/directories...")

    # Process files one by one
    try:
        for file_path in files:
            _make_archive(file_path, password=password, replace=replace)
        logger.info("Encryption process completed successfully")
    except (OSError, RuntimeError):
        logger.exception("Encryption process failed")
        raise


def _create_encrypted_zip(filepath: Path, target_path: Path, password: str) -> None:
    """Create encrypted ZIP file using available tools.

    Args:
        filepath: Path to file/directory to encrypt
        target_path: Target ZIP file path
        password: Encryption password
    """
    if "7z" in conf.available_tools:
        logger.info("Using 7z command for encryption")
        cmds = ["7z", "a", "-p" + password, "-mem=AES256", str(target_path), str(filepath)]
    elif "zip" in conf.available_tools:
        logger.info("Using zip command for encryption")
        cmds = ["zip", "-r", "-P" + password, str(target_path), str(filepath)]
    elif "rar" in conf.available_tools:
        logger.info("Using rar command for encryption")
        cmds = ["rar", "a", "-p" + password, "-m5", str(target_path), str(filepath)]
    else:
        logger.warning("No encryption-capable tools found, using standard zipfile (no encryption)")
        _create_unencrypted_zip(filepath, target_path)
        return
    _execute_command(cmds)


def _create_unencrypted_zip(filepath: Path, target_path: Path) -> None:
    """Create unencrypted ZIP file as fallback option.

    Args:
        filepath: Path to file/directory to compress
        target_path: Target ZIP file path
    """
    with zipfile.ZipFile(target_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
        if filepath.is_file():
            # Use relative path, only include filename without full path
            zip_file.write(filepath, filepath.name)
        elif filepath.is_dir():
            # Use relative path to ensure ZIP contains only relative path structure
            for file in filepath.rglob("*"):
                if file.is_file():
                    arcname = str(file.relative_to(filepath))
                    zip_file.write(file, arcname)


def _get_valid_entries(dirpath: Path) -> list[Path]:
    """Get list of valid entries for processing.

    Args:
        dirpath: Directory path

    Returns
    -------
        List of valid entry paths
    """
    return [
        entry
        for entry in dirpath.iterdir()
        if entry.is_file() or (entry.is_dir() and not any(entry.name.startswith(x) for x in conf.SKIP_PREFIXES))
    ]


def _make_archive(filepath: Path, password: str, *, replace: bool = True) -> None:
    """Create archive for a single file or directory.

    Args:
        filepath: Path to file or directory to archive
        password: Encryption password
        replace: Whether to replace existing archives
    """
    target_path = filepath.parent / f"{filepath.stem}.zip"
    if target_path.exists():  # Avoid duplicate encryption
        if replace:
            logger.info(f"{target_path} already exists, overwriting.")
            target_path.unlink()
        else:
            logger.warning(f"{target_path} already exists, skipping.")
            return

    t0 = perf_counter()
    # Create encrypted ZIP file using password
    if filepath.is_file():
        logger.info(f"Encrypting file: {filepath.name} ...")
    elif filepath.is_dir():
        logger.info(f"Encrypting directory: {filepath.name} ...")
    else:
        logger.warning(f"{filepath} is not a file or directory.")
        return

    _create_encrypted_zip(filepath, target_path, password=password)
    logger.info(f"Encryption completed: {target_path} ({perf_counter() - t0:.2f}s)")


def parse_args() -> argparse.Namespace:
    """Parse command-line arguments for encryption tool."""
    parser = argparse.ArgumentParser(description="Encrypt files and directories into password-protected ZIP archives.")
    parser.add_argument("root_path", help="Directory containing files to encrypt")
    parser.add_argument("password", help="Encryption password")
    parser.add_argument("--replace", action="store_true", help="Whether to replace existing ZIP files")
    return parser.parse_args()


def main() -> None:
    """Encrypt files and directories into password-protected ZIP archives."""
    args = parse_args()

    if not args.password:
        logger.error("Password cannot be empty")
        return

    root_dir = Path(args.root_path)
    if not root_dir.exists():
        logger.error(f"Directory does not exist: {args.root_path}")
        return

    files = _get_valid_entries(root_dir)
    if not files:
        logger.warning(f"No target files found in {args.root_path}.")
        return

    logger.info(f"Starting encryption of {len(files)} files/directories...")
    try:
        for file_path in files:
            _make_archive(file_path, password=args.password, replace=args.replace)
    except (OSError, RuntimeError):
        logger.exception("Error occurred during execution")
        raise
