"""A class that performs HTTP-01 challenges for Apache"""
import errno
import logging
from typing import TYPE_CHECKING

from acme.challenges import KeyAuthorizationChallengeResponse
from acme import messages
from certbot import errors
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge
from certbot.compat import filesystem
from certbot.compat import os
from certbot.plugins import common
from certbot._internal.plugins.apache.obj import VirtualHost
from certbot._internal.plugins.apache.parser import get_aug_path

if TYPE_CHECKING:
    from certbot._internal.plugins.apache.configurator import ApacheConfigurator  # pragma: no cover

logger = logging.getLogger(__name__)


class ApacheHttp01(common.ChallengePerformer):
    """Class that performs HTTP-01 challenges within the Apache configurator."""

    CONFIG_TEMPLATE24_PRE = """\
        RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [END]
    """
    CONFIG_TEMPLATE24_POST = """\
        RewriteEngine on
        <Directory {0}>
            Require all granted
        </Directory>
        <Location /.well-known/acme-challenge>
            Require all granted
        </Location>
    """

    def __init__(self, configurator: "ApacheConfigurator") -> None:
        super().__init__(configurator)
        self.configurator: "ApacheConfigurator"
        self.challenge_conf_pre = os.path.join(
            self.configurator.conf("challenge-location"),
            "le_http_01_challenge_pre.conf")
        self.challenge_conf_post = os.path.join(
            self.configurator.conf("challenge-location"),
            "le_http_01_challenge_post.conf")
        self.challenge_dir = os.path.join(
            self.configurator.config.work_dir,
            "http_challenges")
        self.moded_vhosts: set[VirtualHost] = set()

    def perform(self) -> list[KeyAuthorizationChallengeResponse]:
        """Perform all HTTP-01 challenges."""
        if not self.achalls:
            return []
        if any(achall.identifier.typ == messages.IDENTIFIER_IP for achall in self.achalls):
            raise errors.ConfigurationError(
                "Apache authenticator not supported for IP address certificates")
        # Save any changes to the configuration as a precaution
        # About to make temporary changes to the config
        self.configurator.save("Changes before challenge setup", True)

        self.configurator.ensure_listen(str(self.configurator.config.http01_port))
        self.prepare_http01_modules()

        responses = self._set_up_challenges()

        self._mod_config()
        # Save reversible changes
        self.configurator.save("HTTP Challenge", True)

        return responses

    def prepare_http01_modules(self) -> None:
        """Make sure that we have the needed modules available for http01"""

        if self.configurator.conf("handle-modules"):
            needed_modules = ["rewrite", "authz_core"]
            for mod in needed_modules:
                if mod + "_module" not in self.configurator.parser.modules:
                    self.configurator.enable_mod(mod, temp=True)

    def _mod_config(self) -> None:
        selected_vhosts: list[VirtualHost] = []
        http_port = str(self.configurator.config.http01_port)

        # Search for VirtualHosts matching by name
        for chall in self.achalls:
            selected_vhosts += self._matching_vhosts(chall.identifier.value)

        # Ensure that we have one or more VirtualHosts that we can continue
        # with. (one that listens to port configured with --http-01-port)
        found = False
        for vhost in selected_vhosts:
            if any(a.is_wildcard() or a.get_port() == http_port for a in vhost.addrs):
                found = True

        # If there's at least one eligible VirtualHost, also add all unnamed VirtualHosts
        # because they might match at runtime (#8890)
        if found:
            selected_vhosts += self._unnamed_vhosts()
        # Otherwise, add every Virtualhost which listens on the right port
        else:
            selected_vhosts += self._relevant_vhosts()

        # Add the challenge configuration
        for vh in selected_vhosts:
            self._set_up_include_directives(vh)

        self.configurator.reverter.register_file_creation(
            True, self.challenge_conf_pre)
        self.configurator.reverter.register_file_creation(
            True, self.challenge_conf_post)

        config_text_pre = self.CONFIG_TEMPLATE24_PRE.format(self.challenge_dir)
        config_text_post = self.CONFIG_TEMPLATE24_POST.format(self.challenge_dir)

        logger.debug("writing a pre config file with text:\n %s", config_text_pre)
        with open(self.challenge_conf_pre, "w") as new_conf:
            new_conf.write(config_text_pre)
        logger.debug("writing a post config file with text:\n %s", config_text_post)
        with open(self.challenge_conf_post, "w") as new_conf:
            new_conf.write(config_text_post)

    def _matching_vhosts(self, domain: str) -> list[VirtualHost]:
        """Return all VirtualHost objects that have the requested domain name or
        a wildcard name that would match the domain in ServerName or ServerAlias
        directive.
        """
        matching_vhosts = []
        for vhost in self.configurator.vhosts:
            if self.configurator.domain_in_names(vhost.get_names(), domain):
                # domain_in_names also matches the exact names, so no need
                # to check "domain in vhost.get_names()" explicitly here
                matching_vhosts.append(vhost)

        return matching_vhosts

    def _relevant_vhosts(self) -> list[VirtualHost]:
        http01_port = str(self.configurator.config.http01_port)
        relevant_vhosts: list[VirtualHost] = []
        for vhost in self.configurator.vhosts:
            if any(a.is_wildcard() or a.get_port() == http01_port for a in vhost.addrs):
                if not vhost.ssl:
                    relevant_vhosts.append(vhost)
        if not relevant_vhosts:
            raise errors.PluginError(
                "Unable to find a virtual host listening on port {0} which is"
                " currently needed for Certbot to prove to the CA that you"
                " control your domain. Please add a virtual host for port"
                " {0}.".format(http01_port))

        return relevant_vhosts

    def _unnamed_vhosts(self) -> list[VirtualHost]:
        """Return all VirtualHost objects with no ServerName"""
        return [vh for vh in self.configurator.vhosts if vh.name is None]

    def _set_up_challenges(self) -> list[KeyAuthorizationChallengeResponse]:
        if not os.path.isdir(self.challenge_dir):
            with filesystem.temp_umask(0o022):
                try:
                    filesystem.makedirs(self.challenge_dir, 0o755)
                except OSError as exception:
                    if exception.errno not in (errno.EEXIST, errno.EISDIR):
                        raise errors.PluginError(
                            "Couldn't create root for http-01 challenge")

        responses = []
        for achall in self.achalls:
            responses.append(self._set_up_challenge(achall))

        return responses

    def _set_up_challenge(self, achall: KeyAuthorizationAnnotatedChallenge
                          ) -> KeyAuthorizationChallengeResponse:
        response: KeyAuthorizationChallengeResponse
        response, validation = achall.response_and_validation()

        name: str = os.path.join(self.challenge_dir, achall.chall.encode("token"))

        self.configurator.reverter.register_file_creation(True, name)
        with open(name, 'wb') as f:
            f.write(validation.encode())
        filesystem.chmod(name, 0o644)

        return response

    def _set_up_include_directives(self, vhost: VirtualHost) -> None:
        """Includes override configuration to the beginning and to the end of
        VirtualHost. Note that this include isn't added to Augeas search tree"""

        if vhost not in self.moded_vhosts:
            logger.debug(
                "Adding a temporary challenge validation Include for name: %s in: %s",
                vhost.name, vhost.filep)
            self.configurator.parser.add_dir_beginning(
                vhost.path, "Include", self.challenge_conf_pre)
            self.configurator.parser.add_dir(
                vhost.path, "Include", self.challenge_conf_post)

            if not vhost.enabled:
                self.configurator.parser.add_dir(
                    get_aug_path(self.configurator.parser.loc["default"]),
                    "Include", vhost.filep)

            self.moded_vhosts.add(vhost)
