using Microsoft.Extensions.Logging;
using System.Text;

namespace DigitalData.Core.DTO
{
    /// <summary>
    /// Provides extension methods for data transfer objects (DTOs).
    /// </summary>
    public static class DTOExtensions
    {
        /// <summary>
        /// Adds a single message to the result, if not null.
        /// </summary>
        /// <typeparam name="T">The type of the result.</typeparam>
        /// <param name="result">The result to add the message to.</param>
        /// <param name="message">The message to add.</param>
        /// <returns>The updated result.</returns>
        public static T Message<T>(this T result, string? message) where T : Result
        {
            if(message is not null)
                result.Messages.Add(message);
            return result;
        }

        internal static IEnumerable<T> FilterNull<T>(this IEnumerable<T?> list)
        {
            foreach (var item in list)
                if(item is not null)
                    yield return item;
        }

        /// <summary>
        /// Adds multiple messages to the result, after removing nulls.
        /// </summary>
        /// <typeparam name="T">The type of the result.</typeparam>
        /// <param name="result">The result to add the messages to.</param>
        /// <param name="messages">The messages to add.</param>
        /// <returns>The updated result.</returns>
        public static T Message<T>(this T result, params string?[] messages) where T : Result
        {
            result.Messages.AddRange(messages.FilterNull());
            return result;
        }

        /// <summary>
        /// Adds a collection of messages to the result.
        /// </summary>
        /// <typeparam name="T">The type of the result.</typeparam>
        /// <param name="result">The result to add the messages to.</param>
        /// <param name="messages">The collection of messages to add.</param>
        /// <returns>The updated result.</returns>
        public static T Message<T>(this T result, IEnumerable<string?> messages) where T : Result
        {
            result.Messages.AddRange(messages.FilterNull());
            return result;
        }

        /// <summary>
        /// Adds a notice to the result.
        /// </summary>
        /// <typeparam name="T">The type of the result.</typeparam>
        /// <param name="result">The result to add the notice to.</param>
        /// <param name="notice">The notice to add.</param>
        /// <returns>The updated result.</returns>
        public static T Notice<T>(this T result, Notice notice) where T : Result
        {
            result.Notices.Add(notice);
            return result;
        }

        /// <summary>
        /// Adds a collection of notices to the result.
        /// </summary>
        /// <typeparam name="T">The type of the result.</typeparam>
        /// <param name="result">The result to add the notices to.</param>
        /// <param name="notices">The collection of notices to add.</param>
        /// <returns>The updated result.</returns>
        public static T Notice<T>(this T result, IEnumerable<Notice> notices) where T : Result
        {
            result.Notices.AddRange(notices);
            return result;
        }

        /// <summary>
        /// Adds notices with a specific log level and flags to the result.
        /// </summary>
        /// <typeparam name="T">The type of the result.</typeparam>
        /// <param name="result">The result to add the notices to.</param>
        /// <param name="level">The log level of the notices.</param>
        /// <param name="flags">The flags associated with the notices.</param>
        /// <returns>The updated result.</returns>
        public static T Notice<T>(this T result, LogLevel level, params Enum[] flags) where T : Result
        {
            var notices = flags.Select(flag => new Notice()
            {
                Flag = flag,
                Level = level
            });
            result.Notices.AddRange(notices);
            return result;
        }

        /// <summary>
        /// Adds a notice with a specific log level, flag, and messages to the result.
        /// </summary>
        /// <typeparam name="T">The type of the result.</typeparam>
        /// <param name="result">The result to add the notice to.</param>
        /// <param name="level">The log level of the notice.</param>
        /// <param name="flag">The flag associated with the notice.</param>
        /// <param name="messages">The messages to add to the notice.</param>
        /// <returns>The updated result.</returns>
        public static T Notice<T>(this T result, LogLevel level, Enum flag, params string?[] messages) where T : Result
        {
            result.Notices.Add(new Notice()
            {
                Flag = flag,
                Level = level,
                Messages = messages.FilterNull().ToList()
            });
            return result;
        }

        /// <summary>
        /// Adds a notice with a specific log level and messages to the result.
        /// </summary>
        /// <typeparam name="T">The type of the result.</typeparam>
        /// <param name="result">The result to add the notice to.</param>
        /// <param name="level">The log level of the notice.</param>
        /// <param name="messages">The messages to add to the notice.</param>
        /// <returns>The updated result.</returns>
        public static T Notice<T>(this T result, LogLevel level, params string[] messages) where T : Result
        {
            result.Notices.Add(new Notice()
            {
                Flag = null,
                Level = level,
                Messages = messages.FilterNull().ToList()
            });
            return result;
        }

        /// <summary>
        /// Checks if any notice has the specified flag.
        /// </summary>
        /// <param name="notices">The collection of notices to check.</param>
        /// <param name="flag">The flag to check for.</param>
        /// <returns>True if any notice has the specified flag; otherwise, false.</returns>
        public static bool HasFlag(this IEnumerable<Notice> notices, Enum flag) => notices.Any(n => n.Flag?.ToString() == flag.ToString());

        /// <summary>
        /// Checks if any notice has any of the specified flags.
        /// </summary>
        /// <param name="notices">The collection of notices to check.</param>
        /// <param name="flags">The flags to check for.</param>
        /// <returns>True if any notice has any of the specified flags; otherwise, false.</returns>
        public static bool HasAnyFlag(this IEnumerable<Notice> notices, params Enum[] flags) => flags.Any(f => notices.HasFlag(f));

        /// <summary>
        /// Executes a function based on the success or failure of the task result, 
        /// without using result data.
        /// </summary>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="tResult">The task returning a result to evaluate.</param>
        /// <param name="Success">The function to execute if the result is successful.</param>
        /// <param name="Fail">The function to execute if the result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static I? Then<I>(this Result result, Func<I> Success)
        {
            return result.IsSuccess ? Success() : default;
        }

        /// <summary>
        /// Executes a function based on the success or failure of the task result,
        /// using the data in the result.
        /// </summary>
        /// <typeparam name="T">The type of the data in the result.</typeparam>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="tResult">The task returning a data result to evaluate.</param>
        /// <param name="Success">The function to execute if the data result is successful.</param>
        /// <param name="Fail">The function to execute if the data result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static async Task<I?> ThenAsync<I>(this Result result, Func<Task<I>> SuccessAsync)
        {
            return result.IsSuccess ? await SuccessAsync() : default;
        }

        /// <summary>
        /// Executes a function based on the success or failure of the result.
        /// </summary>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="result">The result to evaluate.</param>
        /// <param name="Success">The function to execute if the result is successful.</param>
        /// <param name="Fail">The function to execute if the result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static I Then<I>(this Result result, Func<I> Success, Func<List<string>, List<Notice>, I> Fail)
        {
            return result.IsSuccess ? Success() : Fail(result.Messages, result.Notices);
        }

        /// <summary>
        /// Asynchronously executes a function based on the success or failure of the result.
        /// </summary>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="result">The result to evaluate.</param>
        /// <param name="SuccessAsync">The asynchronous function to execute if the result is successful.</param>
        /// <param name="Fail">The function to execute if the result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static async Task<I> ThenAsync<I>(this Result result, Func<Task<I>> SuccessAsync, Func<List<string>, List<Notice>, I> Fail)
        {
            return result.IsSuccess ? await SuccessAsync() : Fail(result.Messages, result.Notices);
        }

        /// <summary>
        /// Executes a function based on the success or failure of the data result.
        /// </summary>
        /// <typeparam name="T">The type of the data in the result.</typeparam>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="result">The data result to evaluate.</param>
        /// <param name="Success">The function to execute if the data result is successful.</param>
        /// <param name="Fail">The function to execute if the data result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static I Then<T, I>(this DataResult<T> result, Func<T, I> Success, Func<List<string>, List<Notice>, I> Fail)
        {
            return result.IsSuccess ? Success(result.Data) : Fail(result.Messages, result.Notices);
        }

        /// <summary>
        /// Asynchronously executes a function based on the success or failure of the data result.
        /// </summary>
        /// <typeparam name="T">The type of the data in the result.</typeparam>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="result">The data result to evaluate.</param>
        /// <param name="SuccessAsync">The asynchronous function to execute if the data result is successful.</param>
        /// <param name="Fail">The function to execute if the data result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static async Task<I> ThenAsync<T, I>(this DataResult<T> result, Func<T, Task<I>> SuccessAsync, Func<List<string>, List<Notice>, I> Fail)
        {
            return result.IsSuccess ? await SuccessAsync(result.Data) : Fail(result.Messages, result.Notices);
        }

        /// <summary>
        /// Asynchronously executes a function based on the success or failure of a task returning a result.
        /// </summary>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="tResult">The task returning a result to evaluate.</param>
        /// <param name="Success">The function to execute if the result is successful.</param>
        /// <param name="Fail">The function to execute if the result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static async Task<I> ThenAsync<I>(this Task<Result> tResult, Func<I> Success, Func<List<string>, List<Notice>, I> Fail)
        {
            Result result = await tResult;
            return result.IsSuccess ? Success() : Fail(result.Messages, result.Notices);
        }

        /// <summary>
        /// Asynchronously executes a function based on the success or failure of a task returning a result.
        /// </summary>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="tResult">The task returning a result to evaluate.</param>
        /// <param name="SuccessAsync">The asynchronous function to execute if the result is successful.</param>
        /// <param name="Fail">The function to execute if the result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static async Task<I> ThenAsync<I>(this Task<Result> tResult, Func<Task<I>> SuccessAsync, Func<List<string>, List<Notice>, I> Fail)
        {
            Result result = await tResult;
            return result.IsSuccess ? await SuccessAsync() : Fail(result.Messages, result.Notices);
        }

        /// <summary>
        /// Asynchronously executes a function based on the success or failure of a task returning a data result.
        /// </summary>
        /// <typeparam name="T">The type of the data in the result.</typeparam>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="tResult">The task returning a data result to evaluate.</param>
        /// <param name="Success">The function to execute if the data result is successful.</param>
        /// <param name="Fail">The function to execute if the data result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static async Task<I> ThenAsync<T, I>(this Task<DataResult<T>> tResult, Func<T, I> Success, Func<List<string>, List<Notice>, I> Fail)
        {
            DataResult<T> result = await tResult;
            return result.IsSuccess ? Success(result.Data) : Fail(result.Messages, result.Notices);
        }

        /// <summary>
        /// Asynchronously executes a function based on the success or failure of a task returning a data result.
        /// </summary>
        /// <typeparam name="T">The type of the data in the result.</typeparam>
        /// <typeparam name="I">The type of the return value.</typeparam>
        /// <param name="tResult">The task returning a data result to evaluate.</param>
        /// <param name="SuccessAsync">The asynchronous function to execute if the data result is successful.</param>
        /// <param name="Fail">The function to execute if the data result is a failure.</param>
        /// <returns>The result of the executed function.</returns>
        public static async Task<I> ThenAsync<T, I>(this Task<DataResult<T>> tResult, Func<T, Task<I>> SuccessAsync, Func<List<string>, List<Notice>, I> Fail)
        {
            DataResult<T> result = await tResult;
            return result.IsSuccess ? await SuccessAsync(result.Data) : Fail(result.Messages, result.Notices);
        }

        /// <summary>
        /// Joins the values into a single string with optional start, separator, and end strings.
        /// </summary>
        /// <typeparam name="T">The type of the values.</typeparam>
        /// <param name="values">The values to join.</param>
        /// <param name="start">The starting string.</param>
        /// <param name="separator">The separator string.</param>
        /// <param name="end">The ending string.</param>
        /// <returns>The joined string.</returns>
        public static string Join<T>(this IEnumerable<T> values, string start = "", string seperator = ". ", string end = ".") 
            => new StringBuilder(start).Append(string.Join(seperator, values)).Append(end).ToString();

        /// <summary>
        /// Logs the notices using the specified logger.
        /// </summary>
        /// <param name="logger">The logger to use.</param>
        /// <param name="notices">The collection of notices to log.</param>
        /// <param name="start">The starting string for each notice.</param>
        /// <param name="separator">The separator string for messages in each notice.</param>
        /// <param name="end">The ending string for each notice.</param>
        public static void LogNotice(this ILogger logger, IEnumerable<Notice> notices, string start = ": ", string seperator = ". ", string end = ".\n")
        {
            foreach(LogLevel level in Enum.GetValues(typeof(LogLevel)))
            {
                var logNotices = notices.Where(n => n.Level == level);

                if (!logNotices.Any())
                    continue;

                var sb = new StringBuilder();
                foreach(Notice notice in logNotices)
                {
                    if (notice.Flag is not null)
                        sb.Append(notice.Flag);

                    if (notice.Messages.Any())
                        sb.Append(start).Append(string.Join(seperator, notice.Messages)).AppendLine(end);
                    else sb.Append(end);
                }
                logger.Log(level, sb.ToString());
            }
        }

        /// <summary>
        /// Logs the notices from a result using the specified logger.
        /// </summary>
        /// <param name="logger">The logger to use.</param>
        /// <param name="result">The result containing the notices to log.</param>
        /// <param name="start">The starting string for each notice.</param>
        /// <param name="separator">The separator string for messages in each notice.</param>
        /// <param name="end">The ending string for each notice.</param>
        public static void LogNotice(this ILogger logger, Result result, string start = ": ", string seperator = ". ", string end = ".\n")
            => logger.LogNotice(notices: result.Notices, start: start, seperator: seperator, end: end);

        /// <summary>
        /// Determines if the data result is right (true).
        /// </summary>
        /// <param name="bResult">The data result to evaluate.</param>
        /// <returns>True if the data result is true; otherwise, false.</returns>
        public static bool IsRight(this DataResult<bool> bResult) => bResult.Data;

        /// <summary>
        /// Determines if the data result is wrong (false).
        /// </summary>
        /// <param name="bResult">The data result to evaluate.</param>
        /// <returns>True if the data result is false; otherwise, false.</returns>
        public static bool IsWrong(this DataResult<bool> bResult) => !bResult.Data;
    }
}