################################################################################
#  Licensed to the Apache Software Foundation (ASF) under one
#  or more contributor license agreements.  See the NOTICE file
#  distributed with this work for additional information
#  regarding copyright ownership.  The ASF licenses this file
#  to you under the Apache License, Version 2.0 (the
#  "License"); you may not use this file except in compliance
#  with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
# limitations under the License.
################################################################################
from abc import ABCMeta
from datetime import timedelta
from typing import Optional

from py4j.java_gateway import get_java_class

from pyflink.java_gateway import get_gateway
from pyflink.util.java_utils import to_j_duration, from_j_duration

__all__ = ['RestartStrategies', 'RestartStrategyConfiguration']


class RestartStrategyConfiguration(object, metaclass=ABCMeta):
    """
    Abstract configuration for restart strategies.
    """

    def __init__(self, j_restart_strategy_configuration):
        self._j_restart_strategy_configuration = j_restart_strategy_configuration

    def get_description(self) -> str:
        """
        Returns a description which is shown in the web interface.

        :return: Description of the restart strategy.
        """
        return self._j_restart_strategy_configuration.getDescription()

    def __eq__(self, other):
        return isinstance(other, self.__class__) and \
            self._j_restart_strategy_configuration == \
            other._j_restart_strategy_configuration

    def __hash__(self):
        return self._j_restart_strategy_configuration.hashCode()


class RestartStrategies(object):
    """
    This class defines methods to generate RestartStrategyConfigurations. These configurations are
    used to create RestartStrategies at runtime.

    The RestartStrategyConfigurations are used to decouple the core module from the runtime module.
    """

    class NoRestartStrategyConfiguration(RestartStrategyConfiguration):
        """
        Configuration representing no restart strategy.
        """

        def __init__(self, j_restart_strategy=None):
            if j_restart_strategy is None:
                gateway = get_gateway()
                self._j_restart_strategy_configuration = \
                    gateway.jvm.RestartStrategies.NoRestartStrategyConfiguration()
                super(RestartStrategies.NoRestartStrategyConfiguration, self)\
                    .__init__(self._j_restart_strategy_configuration)
            else:
                super(RestartStrategies.NoRestartStrategyConfiguration, self) \
                    .__init__(j_restart_strategy)

    class FixedDelayRestartStrategyConfiguration(RestartStrategyConfiguration):
        """
        Configuration representing a fixed delay restart strategy.
        """

        def __init__(self, restart_attempts=None, delay_between_attempts_interval=None,
                     j_restart_strategy=None):
            if j_restart_strategy is None:
                if not isinstance(delay_between_attempts_interval, (timedelta, int)):
                    raise TypeError("The delay_between_attempts_interval 'failure_interval' "
                                    "only supports integer and datetime.timedelta, current input "
                                    "type is %s." % type(delay_between_attempts_interval))
                gateway = get_gateway()
                self._j_restart_strategy_configuration = \
                    gateway.jvm.RestartStrategies\
                    .fixedDelayRestart(
                        restart_attempts, to_j_duration(delay_between_attempts_interval))
                super(RestartStrategies.FixedDelayRestartStrategyConfiguration, self)\
                    .__init__(self._j_restart_strategy_configuration)
            else:
                super(RestartStrategies.FixedDelayRestartStrategyConfiguration, self) \
                    .__init__(j_restart_strategy)

        def get_restart_attempts(self) -> int:
            return self._j_restart_strategy_configuration.getRestartAttempts()

        def get_delay_between_attempts_interval(self) -> timedelta:
            return from_j_duration(
                self._j_restart_strategy_configuration.getDelayBetweenAttemptsInterval())

    class FailureRateRestartStrategyConfiguration(RestartStrategyConfiguration):
        """
        Configuration representing a failure rate restart strategy.
        """

        def __init__(self,
                     max_failure_rate=None,
                     failure_interval=None,
                     delay_between_attempts_interval=None,
                     j_restart_strategy=None):
            if j_restart_strategy is None:
                if not isinstance(failure_interval, (timedelta, int)):
                    raise TypeError("The parameter 'failure_interval' "
                                    "only supports integer and datetime.timedelta, current input "
                                    "type is %s." % type(failure_interval))
                if not isinstance(delay_between_attempts_interval, (timedelta, int)):
                    raise TypeError("The delay_between_attempts_interval 'failure_interval' "
                                    "only supports integer and datetime.timedelta, current input "
                                    "type is %s." % type(delay_between_attempts_interval))
                gateway = get_gateway()
                self._j_restart_strategy_configuration = \
                    gateway.jvm.RestartStrategies\
                    .FailureRateRestartStrategyConfiguration(max_failure_rate,
                                                             to_j_duration(failure_interval),
                                                             to_j_duration(
                                                                 delay_between_attempts_interval))
                super(RestartStrategies.FailureRateRestartStrategyConfiguration, self)\
                    .__init__(self._j_restart_strategy_configuration)
            else:
                super(RestartStrategies.FailureRateRestartStrategyConfiguration, self)\
                    .__init__(j_restart_strategy)

        def get_max_failure_rate(self) -> int:
            return self._j_restart_strategy_configuration.getMaxFailureRate()

        def get_failure_interval(self) -> timedelta:
            return from_j_duration(self._j_restart_strategy_configuration.getFailureInterval())

        def get_delay_between_attempts_interval(self) -> timedelta:
            return from_j_duration(self._j_restart_strategy_configuration
                                       .getDelayBetweenAttemptsInterval())

    class FallbackRestartStrategyConfiguration(RestartStrategyConfiguration):
        """
        Restart strategy configuration that could be used by jobs to use cluster level restart
        strategy. Useful especially when one has a custom implementation of restart strategy set via
        config.yaml.
        """

        def __init__(self, j_restart_strategy=None):
            if j_restart_strategy is None:
                gateway = get_gateway()
                self._j_restart_strategy_configuration = \
                    gateway.jvm.RestartStrategies.FallbackRestartStrategyConfiguration()
                super(RestartStrategies.FallbackRestartStrategyConfiguration, self)\
                    .__init__(self._j_restart_strategy_configuration)
            else:
                super(RestartStrategies.FallbackRestartStrategyConfiguration, self)\
                    .__init__(j_restart_strategy)

    @staticmethod
    def _from_j_restart_strategy(j_restart_strategy) -> Optional[RestartStrategyConfiguration]:
        if j_restart_strategy is None:
            return None
        gateway = get_gateway()
        NoRestartStrategyConfiguration = gateway.jvm.RestartStrategies\
            .NoRestartStrategyConfiguration
        FixedDelayRestartStrategyConfiguration = gateway.jvm.RestartStrategies\
            .FixedDelayRestartStrategyConfiguration
        FailureRateRestartStrategyConfiguration = gateway.jvm.RestartStrategies\
            .FailureRateRestartStrategyConfiguration
        FallbackRestartStrategyConfiguration = gateway.jvm.RestartStrategies\
            .FallbackRestartStrategyConfiguration
        clz = j_restart_strategy.getClass()
        if clz.getName() == get_java_class(NoRestartStrategyConfiguration).getName():
            return RestartStrategies.NoRestartStrategyConfiguration(
                j_restart_strategy=j_restart_strategy)
        elif clz.getName() == get_java_class(FixedDelayRestartStrategyConfiguration).getName():
            return RestartStrategies.FixedDelayRestartStrategyConfiguration(
                j_restart_strategy=j_restart_strategy)
        elif clz.getName() == get_java_class(FailureRateRestartStrategyConfiguration).getName():
            return RestartStrategies.FailureRateRestartStrategyConfiguration(
                j_restart_strategy=j_restart_strategy)
        elif clz.getName() == get_java_class(FallbackRestartStrategyConfiguration).getName():
            return RestartStrategies.FallbackRestartStrategyConfiguration(
                j_restart_strategy=j_restart_strategy)
        else:
            raise Exception("Unsupported java RestartStrategyConfiguration: %s" % clz.getName())

    @staticmethod
    def no_restart() -> 'NoRestartStrategyConfiguration':
        """
        Generates NoRestartStrategyConfiguration.

        :return: The :class:`NoRestartStrategyConfiguration`.
        """
        return RestartStrategies.NoRestartStrategyConfiguration()

    @staticmethod
    def fall_back_restart() -> 'FallbackRestartStrategyConfiguration':
        return RestartStrategies.FallbackRestartStrategyConfiguration()

    @staticmethod
    def fixed_delay_restart(restart_attempts: int, delay_between_attempts: int) -> \
            'FixedDelayRestartStrategyConfiguration':
        """
        Generates a FixedDelayRestartStrategyConfiguration.

        :param restart_attempts: Number of restart attempts for the FixedDelayRestartStrategy.
        :param delay_between_attempts: Delay in-between restart attempts for the
                                       FixedDelayRestartStrategy, the input could be integer value
                                       in milliseconds or datetime.timedelta object.
        :return: The :class:`FixedDelayRestartStrategyConfiguration`.
        """
        return RestartStrategies.FixedDelayRestartStrategyConfiguration(restart_attempts,
                                                                        delay_between_attempts)

    @staticmethod
    def failure_rate_restart(failure_rate: int, failure_interval: int, delay_interval: int) -> \
            'FailureRateRestartStrategyConfiguration':
        """
        Generates a FailureRateRestartStrategyConfiguration.

        :param failure_rate: Maximum number of restarts in given interval ``failure_interval``
                             before failing a job.
        :param failure_interval: Time interval for failures, the input could be integer value
                                 in milliseconds or datetime.timedelta object.
        :param delay_interval: Delay in-between restart attempts, the input could be integer value
                               in milliseconds or datetime.timedelta object.
        """
        return RestartStrategies.FailureRateRestartStrategyConfiguration(failure_rate,
                                                                         failure_interval,
                                                                         delay_interval)
