using audit.Model;

namespace audit_logs.Utils;
using System;
using System.Collections.Generic;
using MongoDB.Bson;
using Newtonsoft.Json.Linq;
using Utils;

public class JsonComparer
{
    public List<AuditRecord.Change> GetDifferences(object? originalJson, object? modifiedJson)
    {
        var changes = new List<AuditRecord.Change>();

        // Convertir los objetos a cadenas JSON; usar "{}" si el objeto es null
        var originalJsonString = originalJson != null 
            ? JsonConverter.ConvertObjectToJson(originalJson) 
            : "{}";
        var modifiedJsonString = modifiedJson != null 
            ? JsonConverter.ConvertObjectToJson(modifiedJson) 
            : "{}";

        // Intentar parsear a JObject solo si el JSON es un objeto
        var originalObj = ParseAsJObject(originalJsonString);
        var modifiedObj = ParseAsJObject(modifiedJsonString);
        //eliminamos aquellas claves que contengan null, por ejemplo  "allergies": [
        //      {
        //        "optionType": null,
        //        "name": "Sí",
        //        "iconDefault": "icCheck",
        //        "iconLight": null,
        //        "iconDark": null,
        //        "color": null,
        //        "bgColor": null,
        //        "description": null,
        //        "initDate": null,
        //        "endDate": null
        //      }
        //    ]
        
        if (originalObj != null)
        {
            originalObj = JsonClean.CleanJObject(originalObj);
        }
        if (modifiedObj != null)
        {
            modifiedObj =  JsonClean.CleanJObject(modifiedObj);
        }
        // Comparar los objetos JSON
        CompareJTokens(originalObj, modifiedObj, changes, string.Empty);
        return changes;
    }

    /// <summary>
    /// Intenta parsear una cadena JSON como JObject. Devuelve null si no es un objeto.
    /// </summary>
    /// <param name="json">La cadena JSON a validar.</param>
    /// <returns>Un JObject si es válido, de lo contrario null.</returns>
    private static JObject? ParseAsJObject(string json)
    {
        try
        {
            var token = JToken.Parse(json);
            return token as JObject; // Devuelve null si no es un JObject
        }
        catch
        {
            return null; // Si ocurre un error, no es un JSON válido
        }
    }


    private void CompareJTokens(JToken original, JToken modified, List<AuditRecord.Change> changes, string path)
    {
        foreach (var property in modified.Children<JProperty>())
        {
            var fieldPath = string.IsNullOrEmpty(path) ? property.Name : $"{path}.{property.Name}";
            var originalValue = original[property.Name];
            var modifiedValue = property.Value;
            // Ignorar campos donde ambos valores son null
            if (originalValue == null && modifiedValue.Type == JTokenType.Null)
            {
                continue;
            }

            if (originalValue == null)
            {
                // Campo nuevo
                changes.Add(new AuditRecord.Change
                {
                    Field = fieldPath,
                    OldValue = BsonNull.Value,
                    NewValue = ConvertToBsonValue(modifiedValue),
                    ValueType = GetBsonType(modifiedValue)
                });
            }
            else if (!JToken.DeepEquals(originalValue, modifiedValue))
            {
                if (originalValue.Type == JTokenType.Object && modifiedValue.Type == JTokenType.Object)
                {
                    // Comparar objetos anidados
                    CompareJTokens(originalValue, modifiedValue, changes, fieldPath);
                }
                else
                {
                    // Campo modificado
                    changes.Add(new AuditRecord.Change
                    {
                        Field = fieldPath,
                        OldValue = ConvertToBsonValue(originalValue),
                        NewValue = ConvertToBsonValue(modifiedValue),
                        ValueType = GetBsonType(modifiedValue)
                    });
                }
            }
        }

        // Campos eliminados
        foreach (var property in original.Children<JProperty>())
        {
            var fieldPath = string.IsNullOrEmpty(path) ? property.Name : $"{path}.{property.Name}";
            if (modified[property.Name] == null)
            {
                changes.Add(new AuditRecord.Change
                {
                    Field = fieldPath,
                    OldValue = ConvertToBsonValue(property.Value),
                    NewValue = BsonNull.Value,
                    ValueType = GetBsonType(property.Value)
                });
            }
        }
    }

    /*private BsonValue ConvertToBsonValue(JToken token)
    {
        return token.Type switch
        {
            JTokenType.String => new BsonString(token.ToString()),
            JTokenType.Integer => new BsonInt32(token.ToObject<int>()),
            JTokenType.Float => new BsonDouble(token.ToObject<double>()),
            JTokenType.Boolean => new BsonBoolean(token.ToObject<bool>()),
            JTokenType.Date => new BsonDateTime(token.ToObject<DateTime>()),
            JTokenType.Null => BsonNull.Value,
            JTokenType.Object => BsonDocument.Parse(token.ToString()),
            JTokenType.Array => new BsonArray(token.ToObject<List<BsonValue>>()),
            _ => new BsonString(token.ToString())
        };
    }*/
    private BsonValue ConvertToBsonValue(JToken token)
    {
        switch (token.Type)
        {
            case JTokenType.String:
                return new BsonString(token.ToString());
            case JTokenType.Integer:
                return BsonValue.Create(token.ToObject<int>());
            case JTokenType.Float:
                return BsonValue.Create(token.ToObject<double>());
            case JTokenType.Boolean:
                return new BsonBoolean(token.ToObject<bool>());
            case JTokenType.Date:
                return new BsonDateTime(token.ToObject<DateTime>());
            case JTokenType.Null:
                return BsonNull.Value;
            case JTokenType.Object:
                return BsonDocument.Parse(token.ToString());
            case JTokenType.Array:
                var array = new BsonArray();
                foreach (var item in token.Children())
                {
                    array.Add(ConvertToBsonValue(item));
                }
                return array;
            default:
                return new BsonString(token.ToString()); // Trata cualquier otro tipo como string
        }
    }


    private string GetBsonType(JToken token)
    {
        return token.Type switch
        {
            JTokenType.String => "string",
            JTokenType.Integer => "int",
            JTokenType.Float => "double",
            JTokenType.Boolean => "bool",
            JTokenType.Date => "date",
            JTokenType.Null => "null",
            JTokenType.Object => "object",
            JTokenType.Array => "array",
            _ => "unknown"
        };
    }
}
