import os
import threading
import time
from subprocess import PIPE, Popen
from uuid import uuid4

from . import debug, info


def join_with_script_dir(path: str) -> str:
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), path)


class CertificateCreator:
    cakey = join_with_script_dir("ca.key")
    cacert = join_with_script_dir("ca.crt")
    certkey = join_with_script_dir("cert.key")
    certdir = join_with_script_dir("certs/")

    lock = threading.Lock()

    def validate(self) -> None:
        # Verify the CertificateAuthority files exist
        if not os.path.isfile(CertificateCreator.cakey):
            raise Exception(f"Cannot find {CertificateCreator.cakey}")
        if not os.path.isfile(CertificateCreator.cacert):
            raise Exception(f"Cannot find {CertificateCreator.cacert}")
        if not os.path.isfile(CertificateCreator.certkey):
            raise Exception(f"Cannot find {CertificateCreator.certkey}")
        if not os.path.isdir(CertificateCreator.certdir):
            raise Exception(f"Cannot find {CertificateCreator.certdir}")
        # Verify the `certs` dir is reachable
        try:
            test_file_location = f"{CertificateCreator.certdir}/{uuid4()}.txt"
            debug(
                f"Writing test file to {test_file_location} to verify the directory is writable..."
            )
            with open(test_file_location, "w") as file:
                file.write("test")
            os.remove(test_file_location)
        except Exception:
            info("Failed to write test file")
            info(
                f"The directory {CertificateCreator.certdir} does not seem to be writable"
            )
            raise

    def create(self, path: str) -> str:
        """
        Create an SSL certificate for the supplied hostname.
        This method will return a path to the certificate.
        """
        full_name = path.split(":")[0]

        with CertificateCreator.lock:
            # We don't want to create certificates for every possible endpoint
            # Especially with randomly named S3-buckets

            # We can create certificates that match wildcards to reduce the total number
            # For example:
            # Hostname:      somebucket.s3.amazonaws.com
            # Certificate:   *.s3.amazonaws.com
            #
            # All requests that match this wildcard certificate will reuse it

            wildcard_name = f"*.{'.'.join(full_name.split('.')[1:])}"
            # In Linux we could use asterisk as part of the filename, but not on Windows
            # So we need to pick a different (unique) filename that works everywhere
            wildcard_filename = f"_.{'.'.join(full_name.split('.')[1:])}"
            server_csr = f"{self.certdir.rstrip('/')}/{wildcard_filename}.csr"

            # Verify if the certificate already exists
            certpath = f"{self.certdir.rstrip('/')}/{wildcard_filename}.crt"
            if not os.path.isfile(certpath):
                # Create a Config-file that contains the wildcard-name
                with open(f"{self.certdir.rstrip('/')}/req.conf.tmpl") as f:
                    config_template = f.read()
                config_template = config_template.replace("{{full_name}}", full_name)
                config_template = config_template.replace(
                    "{{wildcard_name}}", wildcard_name
                )
                config_template_name = (
                    f"{self.certdir.rstrip('/')}/{wildcard_filename}.conf"
                )
                with open(config_template_name, "w") as f:
                    f.write(config_template)

                # Create a Certificate Signing Request
                #
                subject = f"/CN={full_name}"[0:64]
                commands = [
                    "openssl",
                    "req",
                    "-new",
                    "-key",
                    self.certkey,
                    "-out",
                    server_csr,
                ]
                commands.extend(["-subj", subject, "-config", config_template_name])

                p1 = Popen(commands)
                p1.communicate()
                debug(f"Created CSR in {server_csr}")

                # Create the actual certificate used by the requests
                p2 = Popen(
                    [
                        "openssl",
                        "x509",
                        "-req",
                        "-in",
                        server_csr,
                        "-days",
                        "3650",
                        "-CA",
                        self.cacert,
                        "-CAkey",
                        self.cakey,
                        "-set_serial",
                        f"{int(time.time() * 1000)}",
                        "-out",
                        certpath,
                        "-extensions",
                        "req_ext",
                        "-extfile",
                        config_template_name,
                    ],
                    stderr=PIPE,
                )
                p2.communicate()
                debug(f"Created certificate for {path} called {certpath}")
                os.remove(server_csr)
                os.remove(config_template_name)
                debug(f"Removed intermediate certificates for {certpath}")
        return certpath
