import functools
import logging
import sys
from typing import Any, Callable, TypeVar, cast

import click

F = TypeVar("F", bound=Callable[..., Any])

log = logging.getLogger(__name__)


class DocumentFilenameException(Exception):
    """Exception for document-related filename errors."""


class AddedDuplicateDocumentException(DocumentFilenameException):
    """Exception for a document is added twice."""

    def __init__(self) -> None:
        super().__init__("A document was added twice")


class InputFileNotFoundException(DocumentFilenameException):
    """Exception for when an input file does not exist."""

    def __init__(self) -> None:
        super().__init__("Input file not found: make sure you typed it correctly.")


class InputFileNotReadableException(DocumentFilenameException):
    """Exception for when an input file exists but is not readable."""

    def __init__(self) -> None:
        super().__init__("You don't have permission to open the input file.")


class NonPDFOutputFileException(DocumentFilenameException):
    """Exception for when the output file is not a PDF."""

    def __init__(self) -> None:
        super().__init__("Safe PDF filename must end in '.pdf'")


class IllegalOutputFilenameException(DocumentFilenameException):
    """Exception for when the output file contains illegal characters."""

    def __init__(self, char: str) -> None:
        super().__init__(f"Illegal character: {char}")


class UnwriteableOutputDirException(DocumentFilenameException):
    """Exception for when the output file is not writeable."""

    def __init__(self) -> None:
        super().__init__("Safe PDF filename is not writable")


class NotSetInputFilenameException(DocumentFilenameException):
    """Exception for when the output filename is set before having an
    associated input file."""

    def __init__(self) -> None:
        super().__init__("Input filename has not been set yet.")


class NotSetOutputFilenameException(DocumentFilenameException):
    """Exception for when the output filename is read before it has been set."""

    def __init__(self) -> None:
        super().__init__("Output filename has not been set yet.")


class NonExistantOutputDirException(DocumentFilenameException):
    """Exception for when the output dir does not exist."""

    def __init__(self) -> None:
        super().__init__("Output directory does not exist")


class OutputDirIsNotDirException(DocumentFilenameException):
    """Exception for when the specified output dir is not actually a dir."""

    def __init__(self) -> None:
        super().__init__("Specified output directory is actually not a directory")


class UnwriteableArchiveDirException(DocumentFilenameException):
    """Exception for when the archive directory cannot be created."""

    def __init__(self) -> None:
        super().__init__(
            "Archive directory for storing unsafe documents cannot be created."
        )


class SuffixNotApplicableException(DocumentFilenameException):
    """Exception for when the suffix cannot be applied to the output filename."""

    def __init__(self) -> None:
        super().__init__("Cannot set a suffix after setting an output filename")


def handle_document_errors(func: F) -> F:
    """Decorator to log document-related errors and exit gracefully."""

    @functools.wraps(func)
    def wrapper(*args, **kwargs):  # type: ignore
        try:
            return func(*args, **kwargs)
        except DocumentFilenameException as e:
            if getattr(sys, "dangerzone_dev", False):
                # Show the full traceback only on dev environments.
                msg = "An exception occured while validating a document"
                log.exception(msg)
            click.echo(str(e))
            sys.exit(1)

    return cast(F, wrapper)


#### Container-related errors


class ImageNotPresentException(Exception):
    pass


class ImageInstallationException(Exception):
    pass


class NoContainerTechException(Exception):
    def __init__(self, container_tech: str) -> None:
        super().__init__(f"{container_tech} is not installed")


class NotAvailableContainerTechException(Exception):
    def __init__(self, container_tech: str, error: str) -> None:
        self.error = error
        self.container_tech = container_tech
        super().__init__(f"{container_tech} is not available")
