using System;
using System.Collections.Generic;


namespace AIRuntime.Runtime.Providers.Fallback
{
    /// <summary>
    /// Controls how transient provider failures are retried
    /// before AIRuntime moves to the next fallback provider.
    ///
    /// MaxRetries describes additional attempts after the
    /// initial provider request.
    /// </summary>
    public sealed class ProviderRetryPolicy
    {
        private readonly HashSet<ProviderFailureKind> _retryableFailures =
            new HashSet<ProviderFailureKind>();


        private int _maxRetries =
            1;


        private TimeSpan _initialDelay =
            TimeSpan.FromMilliseconds(
                500);


        private double _backoffMultiplier =
            2.0;


        private TimeSpan _maximumDelay =
            TimeSpan.FromSeconds(
                5);


        public ProviderRetryPolicy()
        {
            ResetToDefaults();
        }


        /// <summary>
        /// Number of additional attempts allowed for each provider.
        ///
        /// Zero disables retries.
        /// </summary>
        public int MaxRetries
        {
            get
            {
                return _maxRetries;
            }

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


                _maxRetries =
                    value;
            }
        }


        /// <summary>
        /// Delay before the first retry.
        /// </summary>
        public TimeSpan InitialDelay
        {
            get
            {
                return _initialDelay;
            }

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


                _initialDelay =
                    value;
            }
        }


        /// <summary>
        /// Multiplier applied after each retry.
        ///
        /// A value of 1 produces a fixed retry delay.
        /// </summary>
        public double BackoffMultiplier
        {
            get
            {
                return _backoffMultiplier;
            }

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


                _backoffMultiplier =
                    value;
            }
        }


        /// <summary>
        /// Maximum delay permitted between retries.
        /// </summary>
        public TimeSpan MaximumDelay
        {
            get
            {
                return _maximumDelay;
            }

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


                _maximumDelay =
                    value;
            }
        }


        /// <summary>
        /// Returns true when the specified failure category
        /// may be retried on the same provider.
        /// </summary>
        public bool ShouldRetry(
            ProviderFailureKind failureKind)
        {
            return _retryableFailures.Contains(
                failureKind);
        }


        /// <summary>
        /// Enables or disables retrying a failure category.
        /// </summary>
        public void SetRetryable(
            ProviderFailureKind failureKind,
            bool retryable)
        {
            if (retryable)
            {
                _retryableFailures.Add(
                    failureKind);
            }
            else
            {
                _retryableFailures.Remove(
                    failureKind);
            }
        }


        /// <summary>
        /// Calculates the delay before a retry.
        ///
        /// retryNumber is one-based:
        /// 1 represents the first retry.
        /// </summary>
        public TimeSpan GetDelay(
            int retryNumber)
        {
            if (retryNumber <= 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(retryNumber),
                    "Retry number must be greater than zero.");
            }


            if (
                _initialDelay == TimeSpan.Zero ||
                _maximumDelay == TimeSpan.Zero)
            {
                return TimeSpan.Zero;
            }


            double multiplier =
                Math.Pow(
                    _backoffMultiplier,
                    retryNumber -
                    1);


            double milliseconds =
                _initialDelay.TotalMilliseconds *
                multiplier;


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


            return TimeSpan.FromMilliseconds(
                milliseconds);
        }


        /// <summary>
        /// Restores the default retry configuration.
        /// </summary>
        public void ResetToDefaults()
        {
            _maxRetries =
                1;


            _initialDelay =
                TimeSpan.FromMilliseconds(
                    500);


            _backoffMultiplier =
                2.0;


            _maximumDelay =
                TimeSpan.FromSeconds(
                    5);


            _retryableFailures.Clear();


            _retryableFailures.Add(
                ProviderFailureKind.Timeout);


            _retryableFailures.Add(
                ProviderFailureKind.Unavailable);


            _retryableFailures.Add(
                ProviderFailureKind.RateLimited);


            // A completed response with no assistant payload is safe to
            // replay once. Unlike malformed tool arguments, no partial tool
            // call can have been dispatched.
            _retryableFailures.Add(
                ProviderFailureKind.EmptyResponse);


            // Invalid model output is not a transient transport failure.
            // Replaying the same request against the same provider tends to
            // reproduce malformed tool calls and consume rate-limit budget.
            // The conversation engine owns one bounded schema-repair turn.
        }
    }
}
