using EonaCat.Json;
using EonaCat.Json.Serialization;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace EonaCat.Logger.Extensions
{
    // This file is part of the EonaCat project(s) which is released under the Apache License.
    // See the LICENSE file or go to https://EonaCat.com/License for full license details.

    public enum DumpFormat
    {
        Json,
        Xml,
        Tree
    }

    public static class ObjectExtensions
    {
        /// <summary>
        /// Dumps any object to a string in JSON, XML, or detailed tree format.
        /// </summary>
        /// <param name="currentObject">Object to dump</param>
        /// <param name="format">"json" (default), "xml", or "tree"</param>
        /// <param name="detailed">For JSON: include private/internal fields. Ignored for tree format</param>
        /// <param name="maxDepth">Optional max depth for tree dump. Null = no limit</param>
        /// <param name="maxCollectionItems">Optional max items to display in collections. Null = show all</param>
        /// <returns>String representation of the object</returns>
        public static string Dump(this object currentObject, DumpFormat format = DumpFormat.Json, bool detailed = false, int? maxDepth = null, int? maxCollectionItems = null)
        {
            if (currentObject == null)
            {
                return "null";
            }

            try
            {
                switch (format)
                {
                    case DumpFormat.Xml:
                        return DumpXml(currentObject);
                    case DumpFormat.Tree:
                        return DumpTree(currentObject, maxDepth, maxCollectionItems);
                    case DumpFormat.Json:
                    default:
                        return DumpJson(currentObject, detailed);
                }
            }
            catch (Exception ex)
            {
                return $"Error dumping object: {ex.Message}";
            }
        }

        private static string DumpJson(object currentObject, bool isDetailed)
        {
            var settings = new JsonSerializerSettings
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                Formatting = Formatting.Indented
            };

            if (isDetailed)
            {
                settings.ContractResolver = new DefaultContractResolver
                {
                    IgnoreSerializableAttribute = false,
                    IgnoreSerializableInterface = false
                };
            }

            return JsonHelper.ToJson(currentObject, settings);
        }

        private static string DumpXml(object currentObject)
        {
            try
            {
                var xmlSerializer = new XmlSerializer(currentObject.GetType());
                using (var stringWriter = new StringWriter())
                {
                    xmlSerializer.Serialize(stringWriter, currentObject);
                    return stringWriter.ToString();
                }
            }
            catch (Exception ex)
            {
                return $"XML serialization failed: {ex.Message}";
            }
        }

        private static string DumpTree(object currentObject, int? maxDepth, int? maxCollectionItems)
        {
            var stringBuilder = new StringBuilder();
            var visitedHashSet = new HashSet<object>(new ReferenceEqualityComparer());
            DumpTreeInternal(currentObject, stringBuilder, 0, visitedHashSet, maxDepth, maxCollectionItems);
            return stringBuilder.ToString();
        }

        private static void DumpTreeInternal(object currentObject, StringBuilder stringBuilder, int indent, HashSet<object> visited, int? maxDepth, int? maxCollectionItems)
        {
            string indentation = new string(' ', indent * 2);

            if (currentObject == null)
            {
                stringBuilder.AppendLine($"{indentation}null");
                return;
            }

            Type type = currentObject.GetType();
            string typeName = type.FullName;

            if (IsPrimitive(type))
            {
                stringBuilder.AppendLine($"{indentation}{currentObject} ({typeName})");
                return;
            }

            if (visited.Contains(currentObject))
            {
                stringBuilder.AppendLine($"{indentation}<<circular reference to {typeName}>>");
                return;
            }

            if (maxDepth.HasValue && indent >= maxDepth.Value)
            {
                stringBuilder.AppendLine($"{indentation}<<max depth reached: {typeName}>>");
                return;
            }

            visited.Add(currentObject);

            if (currentObject is IEnumerable enumerable && !(currentObject is string))
            {
                int count = 0;

                foreach (var _ in enumerable)
                {
                    count++;
                }

                if (maxCollectionItems.HasValue && count > maxCollectionItems.Value)
                {
                    stringBuilder.AppendLine($"{indentation}{typeName} [<<{count} items, collapsed>>]");
                    return;
                }

                stringBuilder.AppendLine($"{indentation}{typeName} [");

                foreach (var item in enumerable)
                {
                    DumpTreeInternal(item, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems);
                }
                stringBuilder.AppendLine($"{indentation}]");
            }
            else
            {
                stringBuilder.AppendLine($"{indentation}{typeName} {{");
                var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
                var members = type.GetFields(flags);

                foreach (var field in members)
                {
                    object value = null;

                    try
                    {
                        value = field.GetValue(currentObject);
                    }
                    catch
                    {
                        value = "<<unavailable>>";
                    }

                    stringBuilder.Append($"{indentation}  {field.Name} = ");
                    DumpTreeInternal(value, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems);
                }

                var properties = type.GetProperties(flags);

                foreach (var current in properties)
                {
                    if (current.GetIndexParameters().Length > 0)
                    {
                        continue;
                    }

                    object value = null;
                    try { value = current.GetValue(currentObject); } catch { value = "<<unavailable>>"; }
                    stringBuilder.Append($"{indentation}  {current.Name} = ");
                    DumpTreeInternal(value, stringBuilder, indent + 1, visited, maxDepth, maxCollectionItems);
                }

                stringBuilder.AppendLine($"{indentation}}}");
            }
        }

        private static bool IsPrimitive(Type type)
        {
            return type.IsPrimitive
                   || type.IsEnum
                   || type == typeof(string)
                   || type == typeof(decimal)
                   || type == typeof(DateTime)
                   || type == typeof(DateTimeOffset)
                   || type == typeof(Guid)
                   || type == typeof(TimeSpan);
        }

        private class ReferenceEqualityComparer : IEqualityComparer<object>
        {
            public new bool Equals(object x, object y) => ReferenceEquals(x, y);
            public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
        }

        public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
        {
            if (items == null || action == null)
            {
                return;
            }

            foreach (var item in items)
            {
                action(item);
            }
        }

        /// <summary>Check if collection is null or empty</summary>
        public static bool IsNullOrEmpty<T>(this IEnumerable<T> items) => items == null || !items.Any();

        /// <summary>Check if collection has items</summary>
        public static bool HasItems<T>(this IEnumerable<T> items) => !items.IsNullOrEmpty();

        /// <summary>Safe get by index</summary>
        public static T SafeGet<T>(this IList<T> list, int index, T defaultValue = default)
        {
            if (list == null || index < 0 || index >= list.Count)
            {
                return defaultValue;
            }

            return list[index];
        }

        /// <summary>Convert collection to delimited string</summary>
        public static string ToDelimitedString<T>(this IEnumerable<T> items, string delimiter = ", ")
        {
            return items == null ? "" : string.Join(delimiter, items);
        }

        public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s);

        public static string Truncate(this string s, int maxLength)
        {
            if (string.IsNullOrEmpty(s))
            {
                return s;
            }

            return s.Length <= maxLength ? s : s.Substring(0, maxLength);
        }

        public static bool ContainsIgnoreCase(this string s, string value) =>
            s?.IndexOf(value ?? "", StringComparison.OrdinalIgnoreCase) >= 0;

        public static string OrDefault(this string s, string defaultValue) =>
            string.IsNullOrEmpty(s) ? defaultValue : s;

        public static bool IsWeekend(this DateTime date) =>
            date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday;

        public static DateTime StartOfDay(this DateTime date) =>
            date.Date;

        public static DateTime EndOfDay(this DateTime date) =>
            date.Date.AddDays(1).AddTicks(-1);

        /// <summary>
        /// Log an object only if a condition is true.
        /// </summary>
        public static async Task LogIfAsync(this LogManager logger, object currentObject, Func<object, bool> condition,
            ELogType logType = ELogType.INFO, string message = null)
        {
            if (logger == null || currentObject == null || condition == null)
            {
                return;
            }

            if (condition(currentObject))
            {
                string output = message ?? currentObject.Dump();
                await logger.WriteAsync(output, logType);
            }
        }


        public static IDisposable BeginLoggingScope(this ILogger logger, object context)
        {
            if (logger == null || context == null)
            {
                return null;
            }

            return logger.BeginScope(context.ToDictionary());
        }

        public static void LogExecutionTime(this ILogger logger, Action action, string operationName)
        {
            if (logger == null || action == null)
            {
                return;
            }

            var sw = System.Diagnostics.Stopwatch.StartNew();
            action();
            sw.Stop();
            logger.LogInformation("{Operation} executed in {ElapsedMilliseconds}ms", operationName, sw.ElapsedMilliseconds);
        }

        /// <summary>
        /// Converts a Unix timestamp, expressed as the number of seconds since the Unix epoch, to a local DateTime
        /// value.
        /// </summary>
        /// <remarks>The returned DateTime is expressed in the local time zone. To obtain a UTC DateTime,
        /// use DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime instead.</remarks>
        /// <param name="timestamp">The Unix timestamp representing the number of seconds that have elapsed since 00:00:00 UTC on 1 January
        /// 1970.</param>
        /// <returns>A DateTime value that represents the local date and time equivalent of the specified Unix timestamp.</returns>
        public static DateTime FromUnixTimestamp(this long timestamp) =>
            DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;

        /// <summary>
        /// Executes the specified task without waiting for its completion and handles any exceptions that occur during
        /// its execution.
        /// </summary>
        /// <remarks>Use this method to start a task when you do not need to await its completion but want
        /// to ensure that exceptions are observed. This method should be used with caution, as exceptions may be
        /// handled asynchronously and may not be propagated to the calling context. Avoid using this method for tasks
        /// that must complete before continuing execution.</remarks>
        /// <param name="task">The task to execute in a fire-and-forget manner. Cannot be null.</param>
        /// <param name="onError">An optional callback that is invoked if the task throws an exception. If not provided, exceptions are
        /// written to the console.</param>
        public static async void FireAndForget(this Task task, Action<Exception> onError = null)
        {
            if (task == null)
            {
                return;
            }

            try { await task; }
            catch (Exception ex)
            {
                if (onError != null)
                {
                    onError(ex);
                }
                else
                {
                    Console.WriteLine("FireAndForget Exception: " + ex.FormatExceptionToMessage());
                }
            }
        }

        /// <summary>Check if object has property</summary>
        public static bool HasProperty(this object obj, string name) =>
            obj != null && obj.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) != null;

        /// <summary>Get property value safely</summary>
        public static object GetPropertyValue(this object obj, string name)
        {
            if (obj == null)
            {
                return null;
            }

            var prop = obj.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            return prop?.GetValue(obj);
        }

        /// <summary>
        /// Creates a dictionary containing the public and non-public instance properties and fields of the specified
        /// object.
        /// </summary>
        /// <remarks>Indexed properties are excluded from the resulting dictionary. Both public and
        /// non-public instance members are included. If multiple members share the same name, property values will
        /// overwrite field values with the same name.</remarks>
        /// <param name="obj">The object whose properties and fields are to be included in the dictionary. Can be null.</param>
        /// <returns>A dictionary with the names and values of the object's properties and fields. If the object is null, returns
        /// an empty dictionary.</returns>
        public static Dictionary<string, object> ToDictionary(this object obj)
        {
            if (obj == null)
            {
                return new Dictionary<string, object>();
            }

            var dict = new Dictionary<string, object>();
            var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
            foreach (var prop in obj.GetType().GetProperties(flags))
            {
                if (prop.GetIndexParameters().Length > 0)
                {
                    continue;
                }

                dict[prop.Name] = prop.GetValue(obj);
            }
            foreach (var field in obj.GetType().GetFields(flags))
            {
                dict[field.Name] = field.GetValue(obj);
            }
            return dict;
        }
    }
}
