using System;

using AIRuntime.Runtime.Providers.Fallback;


namespace AIRuntime.Runtime.Providers.Health
{
    /// <summary>
    /// Determines when and for how long a provider should
    /// be temporarily removed from provider selection.
    /// </summary>
    public sealed class ProviderCooldownPolicy
    {
        private int _failureThreshold =
            3;


        private TimeSpan _baseCooldown =
            TimeSpan.FromSeconds(
                30);


        private TimeSpan _maximumCooldown =
            TimeSpan.FromMinutes(
                5);


        private double _cooldownMultiplier =
            2.0;


        public int FailureThreshold
        {
            get
            {
                return _failureThreshold;
            }

            set
            {
                if (value <= 0)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        "Failure threshold must be greater than zero.");
                }


                _failureThreshold =
                    value;
            }
        }


        public TimeSpan BaseCooldown
        {
            get
            {
                return _baseCooldown;
            }

            set
            {
                if (value < TimeSpan.Zero)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        "Base cooldown cannot be negative.");
                }


                _baseCooldown =
                    value;
            }
        }


        public TimeSpan MaximumCooldown
        {
            get
            {
                return _maximumCooldown;
            }

            set
            {
                if (value < TimeSpan.Zero)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        "Maximum cooldown cannot be negative.");
                }


                _maximumCooldown =
                    value;
            }
        }


        public double CooldownMultiplier
        {
            get
            {
                return _cooldownMultiplier;
            }

            set
            {
                if (
                    double.IsNaN(value) ||
                    double.IsInfinity(value) ||
                    value < 1.0)
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(value),
                        "Cooldown multiplier must be at least 1.");
                }


                _cooldownMultiplier =
                    value;
            }
        }


        /// <summary>
        /// Returns true when the failure should contribute
        /// towards a provider cooldown.
        /// </summary>
        public bool CountsTowardsCooldown(
            ProviderFailureKind failureKind)
        {
            switch (failureKind)
            {
                case ProviderFailureKind.Timeout:
                case ProviderFailureKind.Unavailable:
                case ProviderFailureKind.RateLimited:
                case ProviderFailureKind.ModelUnavailable:
                case ProviderFailureKind.Authentication:
                case ProviderFailureKind.InvalidResponse:
                case ProviderFailureKind.EmptyResponse:
                    return true;

                case ProviderFailureKind.InvalidRequest:
                case ProviderFailureKind.Cancelled:
                case ProviderFailureKind.Unknown:
                default:
                    return false;
            }
        }


        /// <summary>
        /// Calculates the cooldown duration for a provider.
        ///
        /// No cooldown is returned until the consecutive
        /// failure threshold has been reached.
        /// </summary>
        public TimeSpan GetCooldown(
            int consecutiveFailures,
            ProviderFailureKind failureKind)
        {
            if (consecutiveFailures < 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(consecutiveFailures));
            }


            if (
                !CountsTowardsCooldown(
                    failureKind) ||
                consecutiveFailures <
                _failureThreshold)
            {
                return TimeSpan.Zero;
            }


            int cooldownLevel =
                consecutiveFailures -
                _failureThreshold;


            double multiplier =
                Math.Pow(
                    _cooldownMultiplier,
                    cooldownLevel);


            double milliseconds =
                _baseCooldown.TotalMilliseconds *
                multiplier;


            //--------------------------------------------------
            // Provider-specific minimum cooldowns
            //--------------------------------------------------

            TimeSpan categoryMinimum =
                GetCategoryMinimum(
                    failureKind);


            milliseconds =
                Math.Max(
                    milliseconds,
                    categoryMinimum.TotalMilliseconds);


            milliseconds =
                Math.Min(
                    milliseconds,
                    _maximumCooldown.TotalMilliseconds);


            return TimeSpan.FromMilliseconds(
                milliseconds);
        }


        public void ResetToDefaults()
        {
            _failureThreshold =
                3;


            _baseCooldown =
                TimeSpan.FromSeconds(
                    30);


            _maximumCooldown =
                TimeSpan.FromMinutes(
                    5);


            _cooldownMultiplier =
                2.0;
        }


        private static TimeSpan GetCategoryMinimum(
            ProviderFailureKind failureKind)
        {
            switch (failureKind)
            {
                case ProviderFailureKind.RateLimited:
                    return TimeSpan.FromMinutes(
                        1);

                case ProviderFailureKind.ModelUnavailable:
                    return TimeSpan.FromMinutes(
                        2);

                case ProviderFailureKind.Authentication:
                    return TimeSpan.FromMinutes(
                        5);

                default:
                    return TimeSpan.Zero;
            }
        }
    }
}
