using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEditor;
using LLM.Editor.Helper;
using Newtonsoft.Json.Linq;
using JetBrains.Annotations;
using Object = UnityEngine.Object;

namespace LLM.Editor.Commands
{
    [Serializable]
    public class MemberValue
    {
        public string memberName;
        public JToken value;
    }

    [Serializable]
    public class SetComponentValueParams
    {
        public string subjectIdentifier;
        public string componentName;
        public List<MemberValue> members;
    }

    [UsedImplicitly]
    public class SetComponentValueCommand : ICommand
    {
        private readonly SetComponentValueParams _params;

        public SetComponentValueCommand(string jsonParams)
        {
            _params = jsonParams?.FromJson<SetComponentValueParams>();
        }

        public CommandOutcome Execute(Data.CommandContext context)
        {
            if (_params == null || string.IsNullOrEmpty(_params.subjectIdentifier) || string.IsNullOrEmpty(_params.componentName))
            {
                context.ErrorMessage = "Invalid parameters. Subject and component names are required.";
                return CommandOutcome.Error;
            }

            if (_params.members == null || !_params.members.Any())
            {
                context.ErrorMessage = "No members provided to set.";
                return CommandOutcome.Error;
            }

            var targetObject = CommandUtility.ResolveIdentifier(context, _params.subjectIdentifier);
            if (!targetObject)
            {
                context.ErrorMessage = $"Could not find subject with logical name '{_params.subjectIdentifier}'.";
                return CommandOutcome.Error;
            }

            if (targetObject is not GameObject go)
            {
                context.ErrorMessage = $"The identified subject '{targetObject.name}' is not a GameObject.";
                return CommandOutcome.Error;
            }

            var targetComponent = _params.componentName == "UnityEngine.Transform" ? go.transform : go.GetComponent(GetTypeByName(_params.componentName));
            
            if (!targetComponent)
            {
                context.ErrorMessage = $"Could not find component '{_params.componentName}' on subject '{_params.subjectIdentifier}'.";
                return CommandOutcome.Error;
            }

            foreach (var member in _params.members)
            {
                var outcome = SetMemberValue(context, targetComponent, member.memberName, member.value);
                if (outcome == CommandOutcome.Error)
                {
                    // The error message will be set by SetMemberValue
                    return CommandOutcome.Error;
                }
            }

            return CommandOutcome.Success;
        }

        private CommandOutcome SetMemberValue(Data.CommandContext context, Component targetComponent, string memberName, JToken value)
        {
            var serializedObject = new SerializedObject(targetComponent);
            var serializedProperty = serializedObject.FindProperty(memberName);

            if (serializedProperty != null)
            {
                return SetValueWithSerializedProperty(context, serializedObject, serializedProperty, value);
            }

            var propertyInfo = targetComponent.GetType().GetProperty(memberName, BindingFlags.Public | BindingFlags.Instance);
            if (propertyInfo != null && propertyInfo.CanWrite)
            {
                return SetValueWithReflection(context, targetComponent, propertyInfo, value);
            }

            context.ErrorMessage = $"Could not find a writable serialized field or public property named '{memberName}' on component '{targetComponent.GetType().Name}'.";
            return CommandOutcome.Error;
        }

        private CommandOutcome SetValueWithSerializedProperty(Data.CommandContext context, SerializedObject serializedObject, SerializedProperty property, JToken value)
        {
            serializedObject.Update();
            try
            {
                switch (property.propertyType)
                {
                    case SerializedPropertyType.ObjectReference:
                        var objToAssign = GetObjectReferenceValue(context, property, value);
                        if (objToAssign == null && value.Type != JTokenType.Null) return CommandOutcome.Error;
                        property.objectReferenceValue = objToAssign;
                        break;
                    case SerializedPropertyType.Float:
                        property.floatValue = value.Value<float>();
                        break;
                    case SerializedPropertyType.Integer:
                        property.intValue = value.Value<int>();
                        break;
                    case SerializedPropertyType.Boolean:
                        property.boolValue = value.Value<bool>();
                        break;
                    case SerializedPropertyType.String:
                        property.stringValue = value.Value<string>();
                        break;
                    case SerializedPropertyType.Vector3:
                        property.vector3Value = value.ToVector3();
                        break;
                    case SerializedPropertyType.Quaternion:
                        property.quaternionValue = value.ToQuaternion();
                        break;
                    default:
                        context.ErrorMessage = $"The property type '{property.propertyType}' is not currently supported for setting values via SerializedObject.";
                        return CommandOutcome.Error;
                }
            }
            catch (Exception e)
            {
                context.ErrorMessage = $"Failed to set value for '{property.name}'. Error: {e.Message}";
                return CommandOutcome.Error;
            }
            
            serializedObject.ApplyModifiedProperties();
            Debug.Log($"[SetComponentValueCommand] Successfully set '{property.name}' on '{serializedObject.targetObject.GetType().Name}' using SerializedObject.");
            return CommandOutcome.Success;
        }

        private CommandOutcome SetValueWithReflection(Data.CommandContext context, Component target, PropertyInfo propertyInfo, JToken value)
        {
            Undo.RecordObject(target, $"Set {propertyInfo.Name}");
            try
            {
                object valueToSet;
                var targetType = propertyInfo.PropertyType;

                if (targetType == typeof(Vector3)) valueToSet = value.ToVector3();
                else if (targetType == typeof(Quaternion)) valueToSet = value.ToQuaternion();
                else if (targetType.IsSubclassOf(typeof(Object)) || targetType == typeof(Object))
                {
                    valueToSet = GetObjectReferenceValue(context, null, value);
                    if (valueToSet == null && value.Type != JTokenType.Null) return CommandOutcome.Error;
                }
                else
                {
                    valueToSet = Convert.ChangeType(value.ToObject(targetType), targetType);
                }
                
                propertyInfo.SetValue(target, valueToSet);
            }
            catch (Exception e)
            {
                context.ErrorMessage = $"Failed to set value for '{propertyInfo.Name}' using Reflection. Error: {e.Message}";
                return CommandOutcome.Error;
            }
            
            EditorUtility.SetDirty(target);
            Debug.Log($"[SetComponentValueCommand] Successfully set '{propertyInfo.Name}' on '{target.GetType().Name}' using Reflection.");
            return CommandOutcome.Success;
        }

        private Object GetObjectReferenceValue(Data.CommandContext context, SerializedProperty property, JToken value)
        {
            var referenceData = value.ToObject<ObjectReferenceValue>();
            if (referenceData == null)
            {
                if (value.Type == JTokenType.Null) return null;
                context.ErrorMessage = "The 'value' for an ObjectReference must be an object with an 'identifier' or be null.";
                return null;
            }

            var baseObject = CommandUtility.ResolveIdentifier(context, referenceData.identifier);
            if (!baseObject)
            {
                context.ErrorMessage = $"Could not resolve the base identifier '{referenceData.identifier}' for the object reference.";
                return null;
            }

            Object objectToAssign = baseObject;
            if (baseObject is GameObject go && !string.IsNullOrEmpty(referenceData.childPath))
            {
                var childTransform = go.transform.Find(referenceData.childPath);
                if (!childTransform)
                {
                    context.ErrorMessage = $"Could not find child '{referenceData.childPath}' on object '{baseObject.name}'.";
                    return null;
                }
                objectToAssign = childTransform.gameObject;
                
                var componentType = GetTypeFromProperty(property);
                if (componentType != null && childTransform.TryGetComponent(componentType, out var component))
                {
                    objectToAssign = component;
                }
            }
            
            return objectToAssign;
        }
        
        private static Type GetTypeFromProperty(SerializedProperty property)
        {
            if (property == null) return null;
            var typeString = property.type;
            var startIndex = typeString.IndexOf('<');
            var endIndex = typeString.IndexOf('>');
            if (startIndex == -1 || endIndex == -1) return null;
            var managedTypeName = typeString.Substring(startIndex + 1, endIndex - startIndex - 1).Trim();
            return GetTypeByName(managedTypeName);
        }

        private static Type GetTypeByName(string typeName)
        {
            return AppDomain.CurrentDomain.GetAssemblies()
                .Select(assembly => assembly.GetType(typeName, false))
                .FirstOrDefault(type => type != null);
        }
        
        [Serializable]
        private class ObjectReferenceValue
        {
            public string identifier;
            public string childPath;
        }
    }
}
