import logging
import os
import re
from time import sleep
from typing import List

from azure.core.exceptions import (
    ClientAuthenticationError,
    ResourceExistsError,
    ResourceNotFoundError,
)
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

from aleph_secrets_manager.src.port import Secret, SecretsManagerPort


class AzureClientFactory:
    @staticmethod
    def create(vault_name: str, cross_tenant: bool = False) -> SecretClient:
        credential = DefaultAzureCredential(
            additionally_allowed_tenants=["*"] if cross_tenant else None
        )
        vault_url = f"https://{vault_name}.vault.azure.net"
        client = SecretClient(vault_url=vault_url, credential=credential)
        return client


class AzureSecretsManager(SecretsManagerPort):
    def __init__(self, client: SecretClient):
        self.client = client

    def write_secret(self, secret):
        converted_key = self._convert_write(secret.key)
        successfully_deleted = False

        while not successfully_deleted:
            try:
                self.client.set_secret(converted_key, secret.value)
                successfully_deleted = True
            except ResourceExistsError as e:
                code = self._extract_error_code(e.message)

                if self._is_secret_recoverable(code):
                    self.client.begin_recover_deleted_secret(converted_key)

                logging.warning(f"🚨 Encountered error writing {secret.key}: \n{e}")
                logging.warning(f"Retrying writing of {secret.key}...")
                sleep(1)
            except Exception as e:
                raise

    def _extract_error_code(self, error_msg: str) -> str:
        """Get the internal error code for an Azure Resource error"""
        match = re.search(r'"code":\s*"([^"]+)"', error_msg)
        code = match.group(1)
        return code

    def _is_secret_recoverable(self, code: str) -> bool:
        """Checks if a secret key can be recovered from an error code"""
        return code == "ObjectIsDeletedButRecoverable"

    def read_secret(self, key):
        converted_key = self._convert_write(key)

        try:
            az_secret = self.client.get_secret(converted_key)
            return Secret(key=self._convert_read(az_secret.name), value=az_secret.value)
        except ResourceNotFoundError:
            raise ValueError(
                f"❌ Failed to find secret '{key}' in {self.client.vault_url}"
            )
        except Exception as e:
            raise

    def delete_secret(self, key):
        converted_key = self._convert_write(key)

        try:
            deleted_secret = self.client.begin_delete_secret(converted_key).result()
        except ResourceNotFoundError:
            raise ValueError(
                f"❌ Failed to delete secret '{key}' from {self.client.vault_url} as it does not exist."
            )
        except Exception as e:
            raise

    def list_all_secrets(self):
        return [
            self._convert_read(secret_properties.name)
            for secret_properties in self.client.list_properties_of_secrets()
        ]

    def read_all_secrets(self):
        keys = self.list_all_secrets()
        return [self.read_secret(key) for key in keys]

    def _convert_read(self, key: str):
        return key.replace("-", "_")

    def _convert_write(self, key: str):
        return key.replace("_", "-")

    def load_secrets_to_env(self, names: List[str]) -> None:
        """
        Load secrets from Azure Key Vault into env var.
        """

        try:
            for name in names:
                secret = self.read_secret(name)
                os.environ[secret.key] = secret.value
        except Exception as e:
            raise EnvironmentError(
                f"Failed to load secrets from key vault '{self.client.vault_url}'. Check that the vault name is accurate, and the identity has access to the vault. "
            ) from e
