from __future__ import annotations

import logging
import re
from pathlib import (
    Path,
)
from typing import (
    Any,
)

import requests
from exasol.slc import api as exaslct_api
from exasol.slc.api.get_language_definition_builder import (
    get_language_definition_builder,
)
from exasol.slc.models.compression_strategy import CompressionStrategy
from exasol_integration_test_docker_environment.lib.docker import (
    ContextDockerClient,
)

from exasol.nb_connector.ai_lab_config import AILabConfig as CKey
from exasol.nb_connector.secret_store import Secrets
from exasol.nb_connector.slc import constants
from exasol.nb_connector.slc.git_access import GitAccess
from exasol.nb_connector.slc.package_file_editor import append_packages
from exasol.nb_connector.slc.package_types import (
    CondaPackageDefinition,
    PipPackageDefinition,
)
from exasol.nb_connector.slc.slc_compression_strategy import SlcCompressionStrategy
from exasol.nb_connector.slc.slc_flavor import (
    SlcError,
    SlcFlavor,
)
from exasol.nb_connector.slc.workspace import (
    Workspace,
    current_directory,
)
from exasol.nb_connector.utils import optional_str_to_bool

NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$", flags=re.IGNORECASE)


def _verify_name(slc_name: str) -> None:
    if not NAME_PATTERN.match(slc_name):
        raise SlcError(
            f'SLC name "{slc_name}" doesn\'t match'
            f' regular expression "{NAME_PATTERN}".'
        )


class ScriptLanguageContainer:
    """
    Support building different flavors of Exasol Script Language
    Containers (SLCs) using the SLCT.

    Parameter ``name`` serves as base of the language alias and a key for the related flavor stored in the
    Secure Configuration Storage (SCS / secrets / conf).  The flavor is used
    as a template for building the SLC.

    If the flavor is missing in the SCS or the SLC Git repository has not been
    checked out (i.e. cloned) into the checkout_dir, then the constructor will
    raise an SlcError.

    Additionally, the caller needs to ensure, that a flavor with this name is
    contained in the SLC release specified in variable
    constants.SLC_RELEASE_TAG.
    """

    def __init__(
        self,
        secrets: Secrets,
        name: str,
    ):
        self.secrets = secrets
        self.name = name
        _verify_name(name)
        self.flavor = SlcFlavor(name).verify(secrets)
        self.compression_strategy = SlcCompressionStrategy(name).verify(secrets)
        self.workspace = Workspace.for_slc(name, secrets)
        if not self.checkout_dir.is_dir():
            raise SlcError(
                f"SLC Git repository not checked out to {self.checkout_dir}."
            )
        if not self.flavor_path.is_dir():
            raise SlcError(
                f"Given flavor {self.flavor} not found in version {constants.SLC_RELEASE_TAG} of "
                "Script-Languages-Release. "
                "Check out available flavors at https://github.com/exasol/script-languages-release/releases/tag/{constants.SLC_RELEASE_TAG}."
            )

    @classmethod
    def create(
        cls,
        secrets: Secrets,
        name: str,
        flavor: str,
        compression_strategy: CompressionStrategy = CompressionStrategy.GZIP,
    ) -> ScriptLanguageContainer:
        _verify_name(name)
        slc_flavor = SlcFlavor(name)
        if slc_flavor.exists(secrets):
            raise SlcError(
                "Secure Configuration Storage already contains a"
                f" flavor for SLC name {name}."
            )
        slc_compression_strategy = SlcCompressionStrategy(slc_name=name)
        if slc_compression_strategy.exists(secrets):
            raise SlcError(
                "Secure Configuration Storage already contains a"
                f" compression strategy for SLC name {name}."
            )
        slc_flavor.save(secrets, flavor)
        slc_compression_strategy.save(secrets, compression_strategy)
        workspace = Workspace.for_slc(name, secrets)
        workspace.clone_slc_repo()
        return cls(secrets=secrets, name=name)

    @classmethod
    def create_or_open(
        cls,
        secrets: Secrets,
        name: str,
        flavor: str,
        compression_strategy: CompressionStrategy = CompressionStrategy.GZIP,
    ) -> ScriptLanguageContainer:
        _verify_name(name)
        slc_flavor = SlcFlavor(name)
        if slc_flavor.exists(secrets):
            logging.info(
                "Secure Configuration Storage already contains a"
                " flavor for SLC name %s.",
                name,
            )
        else:
            slc_flavor.save(secrets, flavor)

        slc_compression_strategy = SlcCompressionStrategy(slc_name=name)
        if slc_compression_strategy.exists(secrets):
            logging.info(
                "Secure Configuration Storage already contains a"
                f" compression strategy for SLC name {name}."
            )
        else:
            slc_compression_strategy.save(secrets, compression_strategy)
        workspace = Workspace.for_slc(name, secrets)
        workspace.clone_slc_repo()
        return cls(secrets=secrets, name=name)

    @property
    def language_alias(self) -> str:
        """
        Is case-insensitive.
        """
        return f"custom_slc_{self.name}"

    @property
    def checkout_dir(self) -> Path:
        return self.workspace.git_clone_path

    @property
    def _flavor_path_rel(self) -> str:
        return str(self.flavor_path.relative_to(self.checkout_dir))

    @property
    def flavor_path(self) -> Path:
        return self.checkout_dir / constants.FLAVORS_PATH_IN_SLC_REPO / self.flavor

    @property
    def custom_packages_dir(self):
        """
        Returns the path to the custom packages directory of the flavor
        """
        return self.flavor_path / "flavor_customization" / "packages"

    @property
    def custom_pip_file(self) -> Path:
        """
        Returns the path to the custom pip packages file of the flavor
        """
        return self.custom_packages_dir / "python3_pip_packages"

    @property
    def custom_conda_file(self) -> Path:
        """
        Returns the path to the custom conda packages file of the flavor
        """
        return self.custom_packages_dir / "conda_packages"

    def restore_custom_pip_file(self):
        """
        Restores the custom pip packages file from Git. All changes will be overwritten.
        """
        GitAccess.checkout_file(
            self.checkout_dir / "script-languages",
            self.custom_pip_file.relative_to(self.checkout_dir),
        )

    def restore_custom_conda_file(self):
        """
        Restores the custom conda packages file from Git. All changes will be overwritten.
        """
        GitAccess.checkout_file(
            self.checkout_dir / "script-languages",
            self.custom_conda_file.relative_to(self.checkout_dir),
        )

    def export(self) -> None:
        """
        Exports the current SLC to the export directory.
        """
        with current_directory(self.checkout_dir):
            exaslct_api.export(
                flavor_path=(str(self._flavor_path_rel),),
                export_path=str(self.workspace.export_path),
                output_directory=str(self.workspace.output_path),
                release_name=self.language_alias,
                compression_strategy=self.compression_strategy,
            )

    def export_no_copy(self) -> None:
        """
        Exports the current SLC to the internal output directory only, without copying to the export directory.
        """
        with current_directory(self.checkout_dir):
            exaslct_api.export(
                flavor_path=(str(self._flavor_path_rel),),
                output_directory=str(self.workspace.output_path),
                release_name=self.language_alias,
                compression_strategy=self.compression_strategy,
            )

    def _generate_bfs_params_from_secret_store(self):
        bfs_params: dict[str, Any] = {
            k: self.secrets.get(v)
            for k, v in [
                ("bucketfs_host", CKey.bfs_host_name),
                ("bucketfs_port", CKey.bfs_port),
                ("bucketfs_user", CKey.bfs_user),
                ("bucketfs_password", CKey.bfs_password),
                ("bucketfs_name", CKey.bfs_service),
                ("bucket", CKey.bfs_bucket),
            ]
        }

        bucketfs_use_https = optional_str_to_bool(self.secrets.get(CKey.bfs_encryption))
        if bucketfs_use_https is not None:
            bfs_params["bucketfs_use_https"] = bucketfs_use_https
        use_ssl_cert_validation = optional_str_to_bool(self.secrets.get(CKey.cert_vld))
        if use_ssl_cert_validation is not None:
            bfs_params["use_ssl_cert_validation"] = use_ssl_cert_validation
        ssl_cert_path = self.secrets.get(CKey.trusted_ca)
        if ssl_cert_path is not None:
            bfs_params["ssl_cert_path"] = ssl_cert_path
        return bfs_params

    def deploy(self):
        """
        Deploys the current script-languages-container to the database and
        stores the activation string in the Secure Configuration Storage.
        """
        bfs_params = self._generate_bfs_params_from_secret_store()

        with current_directory(self.checkout_dir):
            result = exaslct_api.deploy(
                flavor_path=(str(self._flavor_path_rel),),
                **bfs_params,
                path_in_bucket=constants.PATH_IN_BUCKET,
                release_name=self.language_alias,
                output_directory=str(self.workspace.output_path),
                compression_strategy=self.compression_strategy,
            )
            deploy_result = result[self._flavor_path_rel]["release"]
            builder = deploy_result.language_definition_builder
            components = builder.generate_definition_components()
            builder.add_custom_alias(components[0].alias, self.language_alias)
            lang_def = builder.generate_definition()
            self.secrets.save(self._alias_key, lang_def)

    @property
    def _alias_key(self):
        return constants.SLC_ACTIVATION_KEY_PREFIX + self.language_alias

    @property
    def activation_key(self) -> str:
        """
        Returns the language activation string for the uploaded script-language-container.
        Can be used in `ALTER SESSION` or `ALTER_SYSTEM` SQL commands to activate
        the language of the uploaded script-language-container.
        """
        try:
            return self.secrets[self._alias_key]
        except AttributeError as ex:
            raise SlcError(
                "Secure Configuration Storage does not contains an activation key."
            ) from ex

    def generate_activation_key(self, add_to_secret_store: bool) -> str:
        """
        Generates the language activation string for the uploaded script-language-container.
        Can be used in `ALTER SESSION` or `ALTER_SYSTEM` SQL commands to activate
        the language of the uploaded script-language-container, for example, if you want to use the SLC in your SQL Editor.
        Furthermore, this method can be used as fallback if the deploy-function failed for some reason,
        but the script-language-container was uploaded to the BucketFS.
        Then it will register the Language activation for this SLC in the Secure Configuration Store.
        """
        bfs_params = self._generate_bfs_params_from_secret_store()

        with current_directory(self.checkout_dir):
            builder = get_language_definition_builder(
                flavor_path=str(self._flavor_path_rel),
                bucketfs_name=bfs_params["bucketfs_name"],
                bucket_name=bfs_params["bucket"],
                path_in_bucket=constants.PATH_IN_BUCKET,
                container_name=f"{self.flavor}-release-{self.language_alias}",  # Currently this can't be retrieved from exaslct. Need to use a hard coded value here.
            )
            components = builder.generate_definition_components()
            builder.add_custom_alias(components[0].alias, self.language_alias)
            lang_def = builder.generate_definition()
            if add_to_secret_store:
                self.secrets.save(self._alias_key, lang_def)
            return lang_def

    def append_custom_pip_packages(self, pip_packages: list[PipPackageDefinition]):
        """
        Appends packages to the custom pip packages file.
        Note: This method is not idempotent: Multiple calls with the same
        package definitions will result in duplicated entries.
        """
        append_packages(self.custom_pip_file, PipPackageDefinition, pip_packages)

    def append_custom_conda_packages(
        self, conda_packages: list[CondaPackageDefinition]
    ):
        """
        Appends packages to the custom conda packages file.
        Note: This method is not idempotent: Multiple calls with the same
        package definitions will result in duplicated entries.
        """
        append_packages(self.custom_conda_file, CondaPackageDefinition, conda_packages)

    @property
    def docker_image_tags(self) -> list[str]:
        """
        Return list of Docker image tags related to the current SLC.
        """
        image_name = constants.SLC_DOCKER_IMG_NAME
        prefix = f"{image_name}:{self.flavor}"
        with ContextDockerClient() as docker_client:
            images = docker_client.images.list(name=image_name)
            return [tag for img in images if (tag := img.tags[0]).startswith(prefix)]

    @classmethod
    def clean_docker_images(cls, output_path: Path = Path.cwd()) -> None:
        """
        Deletes all SLC related local docker images.
        :param output_path: Defines the path where the log files are stored.
        """
        exaslct_api.clean_all_images(
            output_directory=str(output_path),
        )

    @classmethod
    def list_available_flavors(cls) -> list[str]:
        owner = "exasol"
        repo = "script-languages-release"
        path = "flavors"

        url = f"https://api.github.com/repos/{owner}/{repo}/contents/{path}"
        params = {"ref": constants.SLC_RELEASE_TAG}

        try:
            response = requests.get(url, params=params, timeout=30)
            data = response.json()
            return [
                (item["name"])
                for item in data
                if item["type"] in ("dir", "symlink")
                and item["name"].startswith("template")
            ]
        except requests.exceptions.RequestException as ex:
            raise SlcError("Unable to fetch flavor definitions.") from ex
