import datetime
import logging
import threading
import time
import warnings
from functools import wraps
from typing import List, Union, Callable

from .basic import Action, ActionList
from ..mixin import Status, Configuration, InstanceRegistry


logger = logging.getLogger(__name__)


class ConfiguredAction(InstanceRegistry, Configuration, Status):
    UUID_PREFIX = 'ca-'

    def __init__(self,
                 action: Union[Callable, Action, str],
                 *args,
                 configuration_name: str = None,
                 next_action: Union[str, 'ConfiguredAction'] = None,
                 **kwargs,
                 ):
        """
        An action that is configured for execution. Arguments and Keyword arguments may be specified and will
        be handed to the action on execution unless overridden by the user. (Using kwargs over args is strongly
        recommended.)

        :param action: action to perform (callable)
        :param args: arguments for the action
        :param configuration_name: optional name for the configuration (for key retrieval)
        :param next_action: optional next action for sequence building
        :param kwargs: keyword arguments for the action
        """
        # todo ability to retrieve the executions of a given configured action
        InstanceRegistry.__init__(self)
        self._action: Action = None
        self._config_name: str = None
        self._next_action = None
        # register action and store attribute
        self.action = action
        Configuration.__init__(
            self,
            *self.action.default_args,
            **self.action.default_kwargs,
        )
        Status.__init__(self)
        self.update_args(*args)
        self.update_kwargs(**kwargs)
        # generate name if not specified
        if configuration_name is None:
            configuration_name = self._generate_unique_name(self.action)
        else:
            if configuration_name in self.get_configuration_names():
                raise NameError(f'The configuration name "{configuration_name}" is already used, please choose a '
                                f'unique name for the configuration. ')
        self.name = configuration_name
        if next_action is not None:
            self.next_action = next_action

    def __str__(self):
        return f'{self.__class__.__name__} {self.name}'

    def __repr__(self):
        return self.__str__()

    def __call__(self, *args, **kwargs):
        return self.action(*args, **kwargs)

    def __copy__(self):
        return self.__class__(
            self.action,
            self.next_action,
            self.args,
            self.name,
            self.kwargs,
        )

    def __deepcopy__(self, memodict={}):
        return self.__copy__()

    @property
    def action(self) -> Action:
        """The action to be tracked"""
        return self._action

    @action.setter
    def action(self, value: Union[Callable, Action, str]):
        if value is not None:
            self._action = Action.register_action(value)

    @property
    def name(self) -> str:
        """name for the configuration"""
        return self._config_name

    @name.setter
    def name(self, value: str):
        if value == self.name:
            return
        if value is not None:
            # check if already defined
            if value in self.get_configuration_names():
                raise NameError(f'The configuration name "{value}" is already used, please choose a '
                                f'unique name for the configuration. ')
            self._config_name = value

    @property
    def next_action(self) -> 'ConfiguredAction':
        """The next action in the sequence (for contextual action linking)"""
        return self._next_action

    @next_action.setter
    def next_action(self,
                    value: Union[str, 'ConfiguredAction'],
                    ):
        if value is None:
            self._next_action = None
            return
        # retrieve instance by key if not defined
        if isinstance(value, ConfiguredAction) is False:
            value = self.class_instance_by_id(value)
        self._next_action = value

    @next_action.deleter
    def next_action(self):
        self._next_action = None

    @property
    def previous_actions(self) -> List['ConfiguredAction']:
        """returns a list of configured actions which have this instance as a next step"""
        return [
            config
            for config in self.class_registry
            if config.next_action is self
        ]

    @classmethod
    def _generate_unique_name(cls, action: Action) -> str:
        """
        Generates a unique numbered name based on an Action instance. The resulting name will be unique in the
        registry.

        :param action: action to base naming off of
        :return: generated name
        """
        i = 2
        current_names = cls.get_configuration_names()
        if action.name not in current_names:
            return action.name
        while True:
            target_name = f'{action.name}_{i}'
            if f'{action.name}_{i}' not in current_names:
                return target_name
            i += 1

    @staticmethod
    def _match_instance_key(inst: "ConfiguredAction", identifier: str) -> bool:
        """
        Checks the provided identifier key against the instance provided, returning True if matching and False if not

        :param inst: InstanceRegistry instance
        :param identifier: string identifier
        :return: match
        """
        if inst._uuid == identifier:
            return True
        elif inst.uuid == identifier:
            return True
        elif inst.name == identifier:  # match configuration name
            return True
        return False

    @classmethod
    def get_configuration_by_name(cls, configuration_name: str) -> 'ConfiguredAction':
        """
        Retrieves a ConfiguredAction by name

        :param configuration_name: name identifier of configuration
        :return: ConfiguredAction
        """
        warnings.warn(
            'the get_configuration_by_name method has been deprecated, use class_instance_by_id',
            DeprecationWarning,
            stacklevel=2,
        )
        return cls.class_instance_by_id(configuration_name)

    @classmethod
    def get_configuration_names(cls) -> List[str]:
        """
        Retrieves the names of all configurations.

        Warning: return will be sorted, but order does not correspond to order in registry (indexing based on this
        return is not recommended).
        """
        return sorted([config.name for config in cls.class_registry if config.name is not None])

    @classmethod
    def get_configurations_of_action(cls, action: Union[str, Action]) -> List['ConfiguredAction']:
        """
        Retrieves all of the Configurations associated with the specified action

        :param action: action to reference
        :return: list of ConfiguredAction instances
        """
        if isinstance(action, Action) is False:
            action = Action.register_action(action)
        return [
            config
            for config in cls.class_registry
            if config.action is action
        ]

    @classmethod
    def remove_configuration_from_registry(cls, configuration: Union[str, 'ConfiguredAction']):
        """
        Removes the specified configuration from the configuration registry

        :param configuration: name or instance of the configuration
        """
        raise NotImplementedError('this method is not currently implemented')

    @classmethod
    def ensure_configuration(cls,
                             configuration: Union[str, Callable, Action, 'ConfiguredAction'],
                             ) -> 'ConfiguredAction':
        """
        Ensures that the provided configuration is a class instance, instantiating or converting as necessary.

        :param configuration: configuration instance, name, or key
        :raises ValueError: if a string is provided and an action is not registered
        :return: associated ConfiguredAction
        """
        # if already a configuration, return
        if isinstance(configuration, ConfiguredAction):
            return configuration
        # if a string key, retrieve
        elif type(configuration) is str:
            try:
                # try to retrieve configuration by name
                return ConfiguredAction.class_instance_by_id(configuration)
            except NameError:
                # attempt to create a configuration from a registered action
                return ConfiguredAction(
                    Action.register_action(configuration)
                )
        # if an action, create
        elif isinstance(configuration, Action):
            # if an action was provided, create configuration
            return ConfiguredAction(configuration)
        # if a callable, create an action
        elif callable(configuration):
            try:  # attempt to return an existing configuration of the function
                return ConfiguredAction.class_instance_by_id(configuration.__name__)
            except NameError:  # if undefined, register action and create configuration
                # if a callable was provided, register
                return ConfiguredAction(Action.register_action(configuration))
        raise ValueError(f'the value {configuration} could not be interpreted as a configuration')

    def duplicate_configuration(self, new_name: str = None) -> 'ConfiguredAction':
        """
        Duplicates a configuration with the provided name.

        :param new_name: new name for the generated ConfiguredAction instance
        :return: duplicated configuration
        """
        if new_name is None:
            new_name = self._generate_unique_name(self.action)
        return self.__class__(
            action=self.action,
            configuration_name=new_name,
            next_action=self.next_action,
            **self.kwargs,
        )

    def get_tracked_from_config(self, **kwargs) -> "TrackedAction":
        """
        Generates a tracked instance from the configured action

        :param kwargs: additional kwargs that differ from the instance configuration
        :return: TrackedAction (or subclass) instance.
        """
        return TrackedAction(
            configuration=self,
            **kwargs,
        )

    def as_dict(self) -> dict:
        """dictionary of relevant information"""
        if self.next_action is not None:
            next_action_name = self.next_action.name
        else:
            next_action_name = None
        out = {
            'name': self.name,
            'arguments': self.args,
            'keyword_arguments': self.kwargs,
            'status_code': self.status_code,
            'status': self.status_string,
            'next_action': next_action_name,
        }
        return out


class ConfiguredActionList(ActionList):
    def __init__(self, *actions: Union[Action, ConfiguredAction]):
        """
        A manager for a list of Configured Actions.

        :param actions: actions to sequence
        """
        self._action_list: List[ConfiguredAction] = []
        super().__init__(*actions)

    @staticmethod
    def _ensure_type(action: Union[str, Action, ConfiguredAction]) -> ConfiguredAction:
        """Ensures that the provided action is of the correct type for the list"""
        if isinstance(action, ConfiguredAction):
            return action
        elif type(action) is str:
            return ConfiguredAction.class_instance_by_id(action)
        else:
            return ConfiguredAction(action)


def ensure_triggered(fn: Callable):
    """decorator which will do the thing you want"""
    @wraps(fn)
    def decorated(self: 'TrackedAction', *args, **kwargs):
        if self.triggered is False:
            raise AttributeError(
                f'{fn.__name__} may not be accessed until after the {self.__class__.__name__} instance '
                f'is triggered or called'
            )
        return fn(self, *args, **kwargs)
    return decorated


class TrackedAction(InstanceRegistry, Configuration, Status):
    UUID_PREFIX = 'ta-'

    def __init__(self,
                 configuration: Union[str, Callable, Action, ConfiguredAction],
                 *args,
                 **kwargs,
                 ):
        """
        Creates a wrapped action which will track the time of start, completion, and return of the action.

        :param configuration: configuration to reference or action to perform (callable)
        :param args: arguments for the instance's action execution
        :param kwargs: keyword arguments for the instance's action execution
        """
        if isinstance(configuration, TrackedAction):
            raise ValueError('a TrackedAction instance may not be created from another TrackedAction instance, '
                             'use .copy_configuration instance method')
        InstanceRegistry.__init__(self)
        Configuration.__init__(self)
        Status.__init__(self)
        self._triggered = False
        self._configuration: ConfiguredAction = None
        self.configuration = configuration
        self.update_args(*args)
        self.update_kwargs(**kwargs)

        # error state tracker
        self.error: bool = False
        self.error_details = None
        self._time_started = None
        self._time_completed = None
        self._action_return = None

    def __call__(self, *args, **kwargs):
        if self._triggered is True:
            raise RuntimeError(f'multiple executions of a {self.__class__.__name__} instance is not permitted')
        self._triggered = True
        # update args and kwargs and store
        self.update_args(*args)
        self.update_kwargs(**kwargs)
        logger.debug('beginning action execution')
        self.freeze = True   # freeze settings
        self._time_started = time.time()  # set start time
        try:
            self._action_return = self.configuration.action(
                *self.args,
                **self.kwargs
            )
            logger.debug(f'action {self.configuration.action.name} completed successfully')
        except Exception as e:
            logger.error(f'error encountered when executing action {self.configuration.name}: {e}')
            self.error = True
            self.error_details = e
        self._time_completed = time.time()  # save completed time

    def __repr__(self):
        # todo have a separate str which indicates pending/triggered/complete as in timepoint
        strings = []
        args = self._arg_string
        if args != '':
            strings.append(args)
        kw = self._kwarg_string
        if kw != '':
            strings.append(kw)
        return f'{self.__class__.__name__}: {self.configuration.action.name}({", ".join(strings)})'

    def trigger(self, *args, **kwargs):
        """trigger execution of the tracked action"""
        self.__call__(*args, **kwargs)

    @property
    def triggered(self) -> bool:   # todo decide which of these to keep
        """whether the time point has been triggered"""
        return self._triggered

    @property
    def started(self) -> bool:
        """whether the timepoint has been started"""
        return self._triggered

    @property
    def action(self) -> Action:
        """configured action associated with the TrackedAction instances"""
        warnings.warn(  # v 6.0.0
            'the action attribute has been deprecated, access .configuration.action instead',
            DeprecationWarning,
            stacklevel=2,
        )
        return self.configuration.action

    @property
    def configuration(self) -> ConfiguredAction:
        """the configuration associated with the tracked action"""
        return self._configuration

    def _ensure_unfrozen(fn: Callable):
        """decorator which will do the thing you want"""
        @wraps(fn)
        def decorated(self: 'TrackedAction', *args, **kwargs):
            if self._freeze is True:
                raise ValueError(f'the instance is frozen and setting or updating of the args or kwargs is disabled')
            return fn(self, *args, **kwargs)
        return decorated

    @configuration.setter
    @_ensure_unfrozen
    def configuration(self, value: Union[str, Callable, Action, ConfiguredAction]):
        value = ConfiguredAction.ensure_configuration(value)
        self._configuration = value
        self.update_args(*self._configuration.args)
        self.update_kwargs(**self._configuration.kwargs)

    @property
    def time_started(self) -> float:
        """time stamp when the action was started"""
        return self._time_started

    @property
    def time_completed(self) -> float:
        """time stamp when the action was completed"""
        return self._time_completed

    @property
    def started_timestamp(self) -> datetime.datetime:
        """timestamp for when the action was started"""
        if self.time_started is not None:
            return datetime.datetime.fromtimestamp(self.time_started)

    @property
    def action_duration(self) -> float:
        """task duration (s)"""
        if self._time_completed is not None:
            return self._time_completed - self._time_started

    @property
    def method_return(self):
        """DEPRECATED retrieve action return"""
        warnings.warn(  # v 5.5.0
            'method_return is deprecated, use action_return instead',
            DeprecationWarning,
            stacklevel=2,
        )
        return self._action_return

    @property
    @ensure_triggered
    def action_return(self):
        """the return of the method once complete"""
        return self._action_return

    @property
    def next_action(self) -> ConfiguredAction:
        """next action to execute based on the configuration"""
        return self.configuration.next_action

    @property
    def status_code(self) -> int:
        """returns a status code representative of the state of the Action"""
        if self.error is True:
            code = -1
        elif self.time_started is None:
            code = 1
        elif self.time_completed is None:
            code = 2
        else:
            code = 3
        return code

    def as_dict(self) -> dict:
        """dictionary of relevant information"""
        out = {
            'name': self.configuration.name,
            'configuration': self.configuration.name,
            'arguments': self.args,
            'keyword_arguments': self.kwargs,
            'status_code': self.status_code,
            'status': self.status_string,
        }
        if self.time_started is not None:
            out['time_started'] = self.time_started
            out['timestamp'] = str(self.started_timestamp)
        if self.time_completed is not None:
            out['time_completed'] = self.time_completed
            out['duration'] = self.time_completed - self.time_started
            if self.error is True:
                out['error_during_execution'] = True
                out['error_details'] = str(self.error_details)
            else:  # if completed and no error, include the method return
                out['action_return'] = self.action_return
        return out

    def copy_configuration(self) -> "TrackedAction":
        """
        Copies the configuration of a TrackedAction instance.

        :return: TrackedAction instance with identical configuration
        """
        return self.__class__(
            self.configuration,
            *self.args,
            **self.kwargs,
        )

    @classmethod
    def create_from_configured(cls, source: Union[Action, ConfiguredAction]) -> "TrackedAction":
        """
        Creates a TrackedAction instance from a provided Action or ConfiguredAction

        :param source: action to generate from
        :return: instantiated TrackedAction
        """
        warnings.warn(
            'creating TrackedAction instances from ConfiguredAction instances is now the intended method of '
            'instantiation please update your code to instantiate directly',
            DeprecationWarning,
            stacklevel=2,
        )
        return cls(source)

    _ensure_unfrozen = staticmethod(_ensure_unfrozen)


class ThreadedTrackedAction(TrackedAction):
    UUID_PREFIX = 'tta-'

    def __init__(self,
                 configuration: Union[str, Callable, Action, ConfiguredAction],
                 *args,
                 **kwargs,
                 ):
        """
        A threaded version of a tracked action. This action may be executed asynchronously.

        :param configuration: action to perform (callable)
        :param Action, ConfiguredAction, next_step: for creating a sequence of actions, the next_action is the action
            that should be executed after this action has been executed
        :param args: arguments for the action
        :param kwargs: keyword arguments for the action
        """
        TrackedAction.__init__(
            self,
            configuration=configuration,
            *args,
            **kwargs,
        )
        self._lock = threading.Lock()
        self._thread: threading.Thread = None

    def _executor(self, *args, **kwargs):
        """target method for the internal thread"""
        with self._lock:
            super(ThreadedTrackedAction, self).__call__(*args, **kwargs)

    def _trigger(self, *args, **kwargs):
        """
        Triggers the action execution

        :param args: arguments for the call
        :param kwargs: kwargs for the call
        """
        if self.triggered is True:
            raise SystemError(f'multiple executions of the same {self.__class__.__name__} are not supported')
        logger.debug(f'triggering execution of {self.__str__()}')
        self.update_args(*args)
        self.update_kwargs(**kwargs)
        self._thread = threading.Thread(
            target=self._executor,
            name=f'{self.uuid} executor',
            daemon=True,
            args=self.args,
            kwargs=self.kwargs,
        )
        self._thread.start()

    def trigger(self, *args, **kwargs):
        """
        Triggers the action execution

        :param args: arguments for the call
        :param kwargs: kwargs for the call
        """
        self._trigger(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        self._trigger(*args, **kwargs)

    @property
    def in_progress(self) -> bool:
        """
        whether the action is in progress (the action is in the process of being executed)
        """
        if self.triggered is True:
            return self._lock.locked()
        return False

    @property
    def complete(self) -> bool:
        """whether the actions have completed"""
        if self.triggered is True:  # if instance has been triggered, check lock state
            return not self._lock.locked()
        return False

    def wait_for_completion(self):
        """
        Waits for the completion of the time point
        """
        if self.in_progress is True:
            logger.info('waiting for action to complete')
            self._thread.join()  # todo support timeout


class TrackedActionList(ActionList):
    def __init__(self, *actions: Union[Action, ConfiguredAction, TrackedAction]):
        """
        A manager for a list of Tracked Actions.

        :param actions: actions to sequence
        """
        self._action_list: List[TrackedAction] = []
        super().__init__(*actions)
        # todo define what happens when you copy the list (should create unexecuted trackedactions)

    @staticmethod
    def _ensure_type(action: Union[str, Action, ConfiguredAction, TrackedAction]) -> TrackedAction:
        """Ensures that the provided action is of the correct type for the list"""
        if isinstance(action, TrackedAction):
            return action
        else:
            return TrackedAction(action)
