using System;
using System.Collections.Generic;
using System.Threading;
using VCommon.Logging;

namespace VCommon.Diagnostics
{
    public abstract class Retry
    {
        public static void Do(Action action, int maxTry = 3, int retryIntervalMs = 500
            , Action<int> onAttempting = null
            , Action<int> onSuccess = null
            , Func<int, Exception, bool> onFail = null
            , Action<IReadOnlyList<Exception>> onAbort = null)
        {
            Do<object>(() =>
            {
                action();
                return null;
            }, maxTry, retryIntervalMs, onAttempting, onSuccess, onFail, onAbort);
        }

        public static T Do<T>(Func<T> func, int maxTry = 3, int retryIntervalMs = 500
            , Action<int> onAttempting = null
            , Action<int> onSuccess = null
            , Func<int, Exception, bool> onFail = null
            , Action<IReadOnlyList<Exception>> onAbort = null)
        {
            var exceptions = new List<Exception>(maxTry);

            var attempt = 0;

            while (++attempt < maxTry)
            {
                try
                {
                    onAttempting?.Invoke(attempt);
                    var r = func();
                    onSuccess?.Invoke(attempt);
                    return r;
                }
                catch (Exception e)
                {
                    exceptions.Add(e);
                    if (false == (onFail?.Invoke(attempt, e) ?? true)) return default(T);
                    Thread.Sleep(retryIntervalMs);
                }
            }

            var arr = exceptions.ToArray();
            onAbort?.Invoke(arr);
            throw new AggregateException(arr);
        }
    }

    public class RetryPolicy
    {
        public int MaxTry { get; }
        public int RetryIntervalMs { get; }
        public string Tag { get; }
        public bool ThrowOnAbort { get; set; }

        public RetryPolicy(string tag = null, int maxTry = 3, int retryIntervalMs = 500)
        {
            if (maxTry < 1) throw new ArgumentOutOfRangeException(nameof(maxTry));
            if (retryIntervalMs < 0) throw new ArgumentOutOfRangeException(nameof(maxTry));

            MaxTry = maxTry;
            RetryIntervalMs = retryIntervalMs;
            Tag = tag;
        }

        public void Do(Action action) => Retry.Do(action, MaxTry, RetryIntervalMs, OnAttempting, OnSuccess, OnFail, OnAbort);

        public T Do<T>(Func<T> func) => Retry.Do(func, MaxTry, RetryIntervalMs, OnAttempting, OnSuccess, OnFail, OnAbort);

        protected virtual void OnAttempting(int attempted)
        {
            //Logger.Debug($"Attempting {Tag}", new { attempted });
        }

        protected virtual void OnSuccess(int attempted)
        {
            //Logger.Debug($"Success {Tag}", new { attempted });
        }

        protected virtual bool OnFail(int attempted, Exception exception)
        {
            Logger.Warn($"Fail {Tag}", new { attempted });
            return true;
        }

        protected virtual void OnAbort(IReadOnlyList<Exception> exceptions)
        {
            Logger.Error($"Abort {Tag}", new { exceptions });
            if (ThrowOnAbort) throw new AggregateException(exceptions);
        }
    }
}