"""Module for Broker providers.

This module defines the `Provider` class, which is the base class for all Broker providers.
It provides useful methods for registering provider actions and must be inherited by all
Broker providers.

Attributes:
    PROVIDERS (dict): Dictionary of provider names and classes.
    PROVIDER_ACTIONS (dict): Dictionary of provider actions and their corresponding methods.
    PROVIDER_HELP (dict): Dictionary providing information to construct `broker providers --help`

Classes:
    Provider: Base class for all Broker providers.

Usage:
    To create a new Broker provider, create a new class that inherits from the `Provider` class
    and implements the required methods. For example:

    ```
    from broker.providers import Provider

    class MyProvider(Provider):
        def provider_help(self):
            # implementation here

        def get_inventory(self, **inventory_opts):
            # implementation here
    ```

Note: The `Provider` class should not be used directly.

"""

from abc import ABCMeta, abstractmethod
import inspect
import logging
from pathlib import Path

import dynaconf

from broker import exceptions
from broker.settings import clone_global_settings

logger = logging.getLogger(__name__)

# populate a list of all provider module names
_provider_imports = [
    f.stem for f in Path(__file__).parent.glob("*.py") if f.is_file() and f.stem != "__init__"
]

# ProviderName: ProviderClassObject
PROVIDERS = {}
# action: (InterfaceClass, "method_name")
PROVIDER_ACTIONS = {}
# action: (InterfaceClass, "method_name")
PROVIDER_HELP = []


class ProviderMeta(ABCMeta):
    """Metaclass that registers provider classes and actions."""

    def __new__(cls, name, bases, attrs):
        """Register provider classes and actions."""
        new_cls = super().__new__(cls, name, bases, attrs)
        if name != "Provider":
            PROVIDERS[name] = new_cls
            logger.debug(f"Registered provider {name}")
            for attr, obj in attrs.items():
                if attr == "provider_help":
                    # register the help options based on the function arguments
                    for _name, param in inspect.signature(obj).parameters.items():
                        if _name not in ("self", "kwargs", "broker_settings"):
                            # (name, prov_cls, is_flag, help_override)
                            PROVIDER_HELP.append(
                                (
                                    _name,
                                    new_cls,
                                    isinstance(param.default, bool),
                                    getattr(obj, "_help_overrides", {}).get(_name),
                                )
                            )
                            logger.debug(f"Registered help option {_name} for provider {name}")
                elif hasattr(obj, "_as_action"):
                    for action in obj._as_action:
                        PROVIDER_ACTIONS[action] = (new_cls, attr)
                        logger.debug(f"Registered action {action} for provider {name}")
        return new_cls


class Provider(metaclass=ProviderMeta):
    """Abstract base class for all providers.

    This class should be subclassed by all provider implementations. It provides a
    metaclass that registers provider classes and actions.

    Attributes:
        _validators (list): A list of Dynaconf Validators specific to the provider.
        _checkout_options (list): A list of checkout options to add to each command.
        _execute_options (list): A list of execute options to add to each command.
        _fresh_settings (dynaconf.Dynaconf): A clone of the global settings object.
        _sensitive_attrs (list): A list of sensitive attributes that should not be logged.
    """

    # Populate with a list of Dynaconf Validators specific to your provider
    _validators = []
    # Populate these to add your checkout and execute options to each command
    # _checkout_options = [click.option("--workflow", type=str, help="Help text")]
    _checkout_options = []
    _execute_options = []
    _extend_options = []
    _sensitive_attrs = []

    def __init__(self, broker_settings=None, **kwargs):
        cls_name = self.__class__.__name__
        if broker_settings:
            self._settings = broker_settings
            # Add provider validators to custom settings
            if validators := getattr(self.__class__, "_validators", None):
                self._settings.validators.extend(validators)
            logger.debug(
                f"{cls_name} provider using custom settings object: {broker_settings.to_dict()}"
            )
        else:
            self._settings = clone_global_settings()
            logger.debug(f"{cls_name} provider using global settings")
        self._construct_params = []
        logger.debug(f"{cls_name} provider instantiated with {kwargs=}")
        self.instance = kwargs.pop(f"{cls_name}", None)
        self._validate_settings(self.instance)

    def _validate_settings(self, instance_name=None):
        """Load and validate provider settings.

        Each provider's settings can include an instances dict with specific instance
        details.
        One instance should have a "default" key set to True, if instances are defined.
        General provider settings should live on the top level for that provider.

        :param instance_name: A string matching an instance name
        """
        section_name = self.__class__.__name__.upper()
        # if the provider has instances, load the instance settings
        if self._settings.get(section_name, {}).get("instances"):
            fresh_settings = self._settings.get(section_name).copy()
            instance_name = instance_name or getattr(self, "instance", None)
            # first check to see if we have a direct match
            if not (instance_values := fresh_settings.instances.get(instance_name)):
                # if no direct match is found, or no instance is provided, find the default
                for name, values in fresh_settings.instances.items():
                    if values.get("default") or len(fresh_settings.instances) == 1:
                        instance_name, instance_values = name, values
                        break
            self.instance = instance_name  # store the instance name on the provider
            fresh_settings.update(instance_values)
            self._settings[section_name] = fresh_settings
            if not instance_values.get("override_envars"):
                # if a provider instance doesn't want to override envars, load them
                self._settings.execute_loaders(loaders=[dynaconf.loaders.env_loader])
        # add our validators
        self._settings.validators.extend(self._validators)
        # use selective validation to only validate the instance settings
        try:
            self._settings.validators.validate(only=section_name)
        except dynaconf.ValidationError as err:
            raise exceptions.ConfigurationError(err) from err

    def _set_attributes(self, obj, attrs):
        obj.__dict__.update(attrs)

    @abstractmethod
    def construct_host(self, host_cls, provider_params, **kwargs):
        """Construct a host object from a host class and include relevent provider params."""

    @abstractmethod
    def provider_help(self, broker_settings=None):
        """Help options that will be added to the CLI.

        Anything other than 'self', 'kwargs', and 'broker_settings' will be added as a help option
        To specify a flag, set the default value to a boolean
        Everything else should default to None
        """

    @abstractmethod
    def get_inventory(self, **kwargs):
        """Pull inventory information from the provider."""

    @abstractmethod
    def extend(self):
        """Extend the reservation of a host. Not all providers support this."""

    @abstractmethod
    def release(self, host_obj):
        """Release/return a host to the provider. Often this is a deletion or removal."""

    def __repr__(self):
        """Return a string representation of the provider."""
        inner = ", ".join(
            f"{k}={'******' if k in self._sensitive_attrs and v else v}"
            for k, v in self.__dict__.items()
            if not k.startswith("_") and not callable(v)
        )
        return f"{self.__class__.__name__}({inner})"

    @staticmethod
    def register_action(*as_names):
        """Decorate a provider method to register it as a provider action.

        :param as_names: One or more action names to register the decorated function as
        """

        def decorator(func):
            func._as_action = as_names or [func.__name__]
            return func

        return decorator

    @staticmethod
    def help_override(**overrides):
        """Decorate the provider_help function to overwrite auto-generated help text."""

        def wrapper(func):
            func._help_overrides = overrides
            return func

        return wrapper
