using EonaCat.Json;
using EonaCat.Json.Serialization;
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.LogStack.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>
        /// Executes an action on the object if it satisfies a predicate.
        /// </summary>
        public static T If<T>(this T obj, Func<T, bool> predicate, Action<T> action)
        {
            if (obj != null && predicate(obj))
            {
                action(obj);
            }

            return obj;
        }

        /// <summary>
        /// Executes an action on the object if it does NOT satisfy a predicate.
        /// </summary>
        public static T IfNot<T>(this T obj, Func<T, bool> predicate, Action<T> action)
        {
            if (obj != null && !predicate(obj))
            {
                action(obj);
            }

            return obj;
        }

        /// <summary>
        /// Executes a function on an object if not null, returns object itself.
        /// Useful for chaining.
        /// </summary>
        public static T Tap<T>(this T obj, Action<T> action)
        {
            if (obj != null)
            {
                action(obj);
            }

            return obj;
        }

        /// <summary>
        /// Returns true if object implements a given interface.
        /// </summary>
        public static bool Implements<TInterface>(this object obj)
        {
            if (obj == null)
            {
                return false;
            }

            return typeof(TInterface).IsAssignableFrom(obj.GetType());
        }

        /// <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}";
            }
        }

        /// <summary>
        /// Returns a default value if the object is null.
        /// </summary>
        public static T OrDefault<T>(this T obj, T defaultValue = default) =>
            obj == null ? defaultValue : obj;

        /// <summary>
        /// Returns the object if not null; otherwise executes a function to get a fallback.
        /// </summary>
        public static T OrElse<T>(this T obj, Func<T> fallback) =>
            obj != null ? obj : fallback();

        /// <summary>
        /// Converts an object to JSON string with optional formatting.
        /// </summary>
        public static string ToJson(this object obj, bool indented = false)
        {
            try
            {
                return obj == null
                    ? string.Empty
                    : !indented ? Json.JsonHelper.ToJson(obj, Formatting.None) : Json.JsonHelper.ToJson(obj, Formatting.Indented);
            }
            catch
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// Converts an object to string safely, returns empty string if null.
        /// </summary>
        public static string SafeToString(this object obj) =>
            obj?.ToString() ?? string.Empty;

        /// <summary>
        /// Checks if an object is null or default.
        /// </summary>
        public static bool IsNullOrDefault<T>(this T obj) =>
            EqualityComparer<T>.Default.Equals(obj, default);

        /// <summary>
        /// Safely casts an object to a specific type, returns default if cast fails.
        /// </summary>
        public static T SafeCast<T>(this object obj)
        {
            if (obj is T variable)
            {
                return variable;
            }

            return default;
        }

        /// <summary>
        /// Safely tries to convert object to integer.
        /// </summary>
        public static int ToInt(this object obj, int defaultValue = 0)
        {
            if (obj == null)
            {
                return defaultValue;
            }

            return int.TryParse(obj.ToString(), out var val) ? val : defaultValue;
        }

        /// <summary>
        /// Safely tries to convert object to long.
        /// </summary>
        public static long ToLong(this object obj, long defaultValue = 0)
        {
            if (obj == null)
            {
                return defaultValue;
            }

            return long.TryParse(obj.ToString(), out var val) ? val : defaultValue;
        }

        /// <summary>
        /// Safely tries to convert object to double.
        /// </summary>
        public static double ToDouble(this object obj, double defaultValue = 0)
        {
            if (obj == null)
            {
                return defaultValue;
            }

            return double.TryParse(obj.ToString(), out var val) ? val : defaultValue;
        }

        /// <summary>
        /// Safely tries to convert object to bool.
        /// </summary>
        public static bool ToBool(this object obj, bool defaultValue = false)
        {
            if (obj == null)
            {
                return defaultValue;
            }

            return bool.TryParse(obj.ToString(), out var val) ? val : defaultValue;
        }

        /// <summary>
        /// Checks if an object is of a specific type.
        /// </summary>
        public static bool IsType<T>(this object obj) => obj is T;

        /// <summary>
        /// Executes an action if the object is not null.
        /// </summary>
        public static void IfNotNull<T>(this T obj, Action<T> action)
        {
            if (obj != null)
            {
                action(obj);
            }
        }

        /// <summary>
        /// Executes an action if the object is null.
        /// </summary>
        public static void IfNull<T>(this T obj, Action action)
        {
            if (obj == null)
            {
                action();
            }
        }

        /// <summary>
        /// Wraps the object into a single-item enumerable.
        /// </summary>
        public static IEnumerable<T> AsEnumerable<T>(this T obj)
        {
            if (obj != null)
            {
                yield return obj;
            }
        }

        /// <summary>
        /// Safely returns a string representation with max length truncation.
        /// </summary>
        public static string ToSafeString(this object obj, int maxLength)
        {
            string str = obj.SafeToString();
            return str.Length <= maxLength ? str : str.Substring(0, maxLength);
        }

        /// <summary>
        /// Returns object hash code safely (0 if null).
        /// </summary>
        public static int SafeHashCode(this object obj) =>
            obj?.GetHashCode() ?? 0;

        /// <summary>
        /// Returns the object or throws a custom exception if null.
        /// </summary>
        public static T OrThrow<T>(this T obj, Func<Exception> exceptionFactory)
        {
            if (obj == null)
            {
                throw exceptionFactory();
            }

            return obj;
        }

        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);

        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;
        }

        /// <summary>
        /// Converts any object to a human-readable log string, including collections and nested objects.
        /// </summary>
        public static string ToLogString(this object obj, int maxDepth = 3, int currentDepth = 0)
        {
            if (obj == null)
            {
                return "null";
            }

            if (currentDepth >= maxDepth)
            {
                return "...";
            }

            // Handle strings separately
            if (obj is string str)
            {
                return str;
            }

            // Handle IEnumerable
            if (obj is IEnumerable enumerable)
            {
                var items = new List<string>();
                foreach (var item in enumerable)
                {
                    items.Add(item.ToLogString(maxDepth, currentDepth + 1));
                }
                return "[" + string.Join(", ", items) + "]";
            }

            // Handle primitive types
            var type = obj.GetType();
            if (type.IsPrimitive || obj is decimal || obj is DateTime || obj is Guid)
            {
                return obj.ToString();
            }

            // Handle objects with properties
            try
            {
                var props = type.GetProperties();
                var sb = new StringBuilder("{");
                bool first = true;
                foreach (var p in props)
                {
                    if (!first)
                    {
                        sb.Append(", ");
                    }

                    var val = p.GetValue(obj);
                    sb.Append($"{p.Name}={val.ToLogString(maxDepth, currentDepth + 1)}");
                    first = false;
                }
                sb.Append("}");
                return sb.ToString();
            }
            catch
            {
                return obj.ToString();
            }
        }

        /// <summary>
        /// Checks if an object is considered "empty": null, empty string, empty collection.
        /// </summary>
        public static bool IsEmpty(this object obj)
        {
            if (obj == null)
            {
                return true;
            }

            if (obj is string str)
            {
                return string.IsNullOrWhiteSpace(str);
            }

            if (obj is ICollection col)
            {
                return col.Count == 0;
            }

            if (obj is IEnumerable enumerable)
            {
                return !enumerable.Cast<object>().Any();
            }

            return false;
        }

        /// <summary>
        /// Executes an action if the object is not null and not empty.
        /// </summary>
        public static void IfNotEmpty<T>(this T obj, Action<T> action)
        {
            if (!obj.IsEmpty())
            {
                action(obj);
            }
        }

        /// <summary>
        /// Returns a default value if the object is null or empty.
        /// </summary>
        public static T OrDefaultIfEmpty<T>(this T obj, T defaultValue)
        {
            return obj.IsEmpty() ? defaultValue : obj;
        }

        /// <summary>
        /// Returns true if the object is numeric (int, float, double, decimal, long, etc.).
        /// </summary>
        public static bool IsNumeric(this object obj)
        {
            if (obj == null)
            {
                return false;
            }

            return double.TryParse(obj.ToString(), out _);
        }

        /// <summary>
        /// Converts an object to a numeric double, returns default if conversion fails.
        /// </summary>
        public static double ToDoubleSafe(this object obj, double defaultValue = 0)
        {
            if (obj == null)
            {
                return defaultValue;
            }

            return double.TryParse(obj.ToString(), out var d) ? d : defaultValue;
        }

        /// <summary>
        /// Converts an object to a numeric int, returns default if conversion fails.
        /// </summary>
        public static int ToIntSafe(this object obj, int defaultValue = 0)
        {
            if (obj == null)
            {
                return defaultValue;
            }

            return int.TryParse(obj.ToString(), out var i) ? i : defaultValue;
        }

        /// <summary>
        /// Returns the type name of an object safely.
        /// </summary>
        public static string GetTypeName(this object obj) =>
            obj?.GetType().Name ?? "null";

        /// <summary>
        /// Executes a function if object is not null and returns a fallback value otherwise.
        /// </summary>
        public static TResult Map<T, TResult>(this T obj, Func<T, TResult> mapper, TResult fallback = default)
        {
            if (obj == null)
            {
                return fallback;
            }

            return mapper(obj);
        }

        /// <summary>
        /// Masks sensitive strings (like passwords, tokens). Keeps first and last 2 characters visible.
        /// </summary>
        public static string MaskSensitive(this string str)
        {
            if (string.IsNullOrEmpty(str) || str.Length <= 4)
            {
                return "****";
            }

            int len = str.Length - 4;
            return str.Substring(0, 2) + new string('*', len) + str.Substring(str.Length - 2, 2);
        }

        /// <summary>
        /// Masks sensitive data in any object property that matches a keyword.
        /// </summary>
        public static void MaskProperties(this object obj, params string[] keywords)
        {
            if (obj == null || keywords == null || keywords.Length == 0)
            {
                return;
            }

            var props = obj.GetType().GetProperties();
            foreach (var p in props)
            {
                if (!p.CanRead || !p.CanWrite)
                {
                    continue;
                }

                if (keywords.Any(k => p.Name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0))
                {
                    var val = p.GetValue(obj) as string;
                    if (!string.IsNullOrEmpty(val))
                    {
                        p.SetValue(obj, val.MaskSensitive());
                    }
                }
            }
        }
    }
}
