using System;
using System.Reflection;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Runtime.Helpers;

namespace MCPForUnity.Editor.Tools.Cameras
{
    internal static class CameraConfigure
    {
        #region Tier 1 — Basic Camera

        internal static object SetBasicCameraTarget(JObject @params)
        {
            var go = CameraHelpers.FindTargetGameObject(@params);
            if (go == null) return new ErrorResponse("Target Camera not found.");

            var cam = go.GetComponent<UnityEngine.Camera>();
            if (cam == null) return new ErrorResponse($"No Camera component on '{go.name}'.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            var lookAtToken = props["lookAt"] ?? props["look_at"] ?? props["follow"];
            if (lookAtToken == null)
                return new ErrorResponse("'follow' or 'lookAt' property is required.");

            var target = CameraHelpers.ResolveGameObjectRef(lookAtToken);
            if (target == null)
                return new ErrorResponse($"Target '{lookAtToken}' not found.");

            Undo.RecordObject(go.transform, "Set Camera Target");
            go.transform.LookAt(target.transform);
            CameraHelpers.MarkDirty(go);

            return new
            {
                success = true,
                message = $"Camera '{go.name}' now looking at '{target.name}'.",
                data = new { instanceID = go.GetInstanceIDCompat() }
            };
        }

        internal static object SetBasicCameraLens(JObject @params)
        {
            var go = CameraHelpers.FindTargetGameObject(@params);
            if (go == null) return new ErrorResponse("Target Camera not found.");

            var cam = go.GetComponent<UnityEngine.Camera>();
            if (cam == null) return new ErrorResponse($"No Camera component on '{go.name}'.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            Undo.RecordObject(cam, "Set Camera Lens");

            if (props["fieldOfView"] != null)
                cam.fieldOfView = ParamCoercion.CoerceFloat(props["fieldOfView"], cam.fieldOfView);
            if (props["nearClipPlane"] != null)
                cam.nearClipPlane = ParamCoercion.CoerceFloat(props["nearClipPlane"], cam.nearClipPlane);
            if (props["farClipPlane"] != null)
                cam.farClipPlane = ParamCoercion.CoerceFloat(props["farClipPlane"], cam.farClipPlane);
            if (props["orthographicSize"] != null)
                cam.orthographicSize = ParamCoercion.CoerceFloat(props["orthographicSize"], cam.orthographicSize);

            CameraHelpers.MarkDirty(go);
            return new
            {
                success = true,
                message = $"Lens properties set on Camera '{go.name}'.",
                data = new { instanceID = go.GetInstanceIDCompat() }
            };
        }

        internal static object SetBasicCameraPriority(JObject @params)
        {
            var go = CameraHelpers.FindTargetGameObject(@params);
            if (go == null) return new ErrorResponse("Target Camera not found.");

            var cam = go.GetComponent<UnityEngine.Camera>();
            if (cam == null) return new ErrorResponse($"No Camera component on '{go.name}'.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            float depth = ParamCoercion.CoerceFloat(props["priority"], cam.depth);

            Undo.RecordObject(cam, "Set Camera Depth");
            cam.depth = depth;
            CameraHelpers.MarkDirty(go);

            return new
            {
                success = true,
                message = $"Camera '{go.name}' depth set to {depth}.",
                data = new { instanceID = go.GetInstanceIDCompat(), depth }
            };
        }

        #endregion

        #region Tier 2 — Cinemachine

        internal static object SetCinemachineTarget(JObject @params)
        {
            var cmCamera = CameraHelpers.FindCinemachineCamera(@params);
            if (cmCamera == null) return new ErrorResponse("Target CinemachineCamera not found.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();

            Undo.RecordObject(cmCamera, "Set Cinemachine Target");

            if (props.ContainsKey("follow"))
                CameraHelpers.SetTransformTarget(cmCamera, "Follow", props["follow"]);
            if (props.ContainsKey("lookAt") || props.ContainsKey("look_at"))
                CameraHelpers.SetTransformTarget(cmCamera, "LookAt", props["lookAt"] ?? props["look_at"]);

            CameraHelpers.MarkDirty(cmCamera.gameObject);

            return new
            {
                success = true,
                message = $"Targets set on CinemachineCamera '{cmCamera.gameObject.name}'.",
                data = new { instanceID = cmCamera.gameObject.GetInstanceIDCompat() }
            };
        }

        internal static object SetCinemachineLens(JObject @params)
        {
            var cmCamera = CameraHelpers.FindCinemachineCamera(@params);
            if (cmCamera == null) return new ErrorResponse("Target CinemachineCamera not found.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            Undo.RecordObject(cmCamera, "Set Cinemachine Lens");

            // Lens is a struct field — use SerializedProperty for reliable setting
            using var so = new SerializedObject(cmCamera);
            var lensProp = so.FindProperty("Lens") ?? so.FindProperty("m_Lens");
            if (lensProp == null)
                return new ErrorResponse("Could not find Lens property on CinemachineCamera.");

            SetFloatSubProp(lensProp, "FieldOfView", props["fieldOfView"]);
            SetFloatSubProp(lensProp, "NearClipPlane", props["nearClipPlane"]);
            SetFloatSubProp(lensProp, "FarClipPlane", props["farClipPlane"]);
            SetFloatSubProp(lensProp, "OrthographicSize", props["orthographicSize"]);
            SetFloatSubProp(lensProp, "Dutch", props["dutch"]);

            so.ApplyModifiedProperties();
            CameraHelpers.MarkDirty(cmCamera.gameObject);

            return new
            {
                success = true,
                message = $"Lens properties set on CinemachineCamera '{cmCamera.gameObject.name}'.",
                data = new { instanceID = cmCamera.gameObject.GetInstanceIDCompat() }
            };
        }

        internal static object SetCinemachinePriority(JObject @params)
        {
            var cmCamera = CameraHelpers.FindCinemachineCamera(@params);
            if (cmCamera == null) return new ErrorResponse("Target CinemachineCamera not found.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            int priority = ParamCoercion.CoerceInt(props["priority"], 10);

            // PrioritySettings is a struct with Enabled + m_Value — use SerializedProperty
            using var so = new SerializedObject(cmCamera);
            var priorityProp = so.FindProperty("Priority");
            if (priorityProp != null)
            {
                var enabledProp = priorityProp.FindPropertyRelative("Enabled");
                var valueProp = priorityProp.FindPropertyRelative("m_Value");
                if (enabledProp != null) enabledProp.boolValue = true;
                if (valueProp != null) valueProp.intValue = priority;
                so.ApplyModifiedProperties();
            }
            else
            {
                Undo.RecordObject(cmCamera, "Set Cinemachine Priority");
                CameraHelpers.SetReflectionProperty(cmCamera, "Priority", priority);
            }
            CameraHelpers.MarkDirty(cmCamera.gameObject);

            return new
            {
                success = true,
                message = $"Priority set to {priority} on CinemachineCamera '{cmCamera.gameObject.name}'.",
                data = new { instanceID = cmCamera.gameObject.GetInstanceIDCompat(), priority }
            };
        }

        internal static object SetBody(JObject @params)
        {
            var cmCamera = CameraHelpers.FindCinemachineCamera(@params);
            if (cmCamera == null) return new ErrorResponse("Target CinemachineCamera not found.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            var go = cmCamera.gameObject;

            // Optionally swap body component
            string bodyTypeName = ParamCoercion.CoerceString(props["bodyType"] ?? props["body_type"], null);
            Component bodyComponent;

            if (bodyTypeName != null)
            {
                bodyComponent = SwapPipelineComponent(go, "Body", bodyTypeName);
                if (bodyComponent == null)
                    return new ErrorResponse($"Could not resolve body component type '{bodyTypeName}'.");
            }
            else
            {
                bodyComponent = CameraHelpers.GetPipelineComponent(cmCamera, "Body");
                if (bodyComponent == null)
                    return new ErrorResponse("No Body component found on this CinemachineCamera. Provide 'bodyType' to add one.");
            }

            // Set properties on body component
            SetComponentProperties(bodyComponent, props, new[] { "bodyType", "body_type" });
            CameraHelpers.MarkDirty(go);

            return new
            {
                success = true,
                message = $"Body configured on CinemachineCamera '{go.name}'.",
                data = new { instanceID = go.GetInstanceIDCompat(), body = bodyComponent.GetType().Name }
            };
        }

        internal static object SetAim(JObject @params)
        {
            var cmCamera = CameraHelpers.FindCinemachineCamera(@params);
            if (cmCamera == null) return new ErrorResponse("Target CinemachineCamera not found.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            var go = cmCamera.gameObject;

            string aimTypeName = ParamCoercion.CoerceString(props["aimType"] ?? props["aim_type"], null);
            Component aimComponent;

            if (aimTypeName != null)
            {
                aimComponent = SwapPipelineComponent(go, "Aim", aimTypeName);
                if (aimComponent == null)
                    return new ErrorResponse($"Could not resolve aim component type '{aimTypeName}'.");
            }
            else
            {
                aimComponent = CameraHelpers.GetPipelineComponent(cmCamera, "Aim");
                if (aimComponent == null)
                    return new ErrorResponse("No Aim component found. Provide 'aimType' to add one.");
            }

            SetComponentProperties(aimComponent, props, new[] { "aimType", "aim_type" });
            CameraHelpers.MarkDirty(go);

            return new
            {
                success = true,
                message = $"Aim configured on CinemachineCamera '{go.name}'.",
                data = new { instanceID = go.GetInstanceIDCompat(), aim = aimComponent.GetType().Name }
            };
        }

        internal static object SetNoise(JObject @params)
        {
            var cmCamera = CameraHelpers.FindCinemachineCamera(@params);
            if (cmCamera == null) return new ErrorResponse("Target CinemachineCamera not found.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            var go = cmCamera.gameObject;

            // Get or add noise component
            var noiseType = CameraHelpers.ResolveComponentType("CinemachineBasicMultiChannelPerlin");
            if (noiseType == null)
                return new ErrorResponse("CinemachineBasicMultiChannelPerlin type not found.");

            var noiseComponent = go.GetComponent(noiseType);
            bool added = false;
            if (noiseComponent == null)
            {
                noiseComponent = Undo.AddComponent(go, noiseType);
                added = true;
            }

            Undo.RecordObject(noiseComponent, "Set Cinemachine Noise");
            SetComponentProperties(noiseComponent, props, Array.Empty<string>());
            CameraHelpers.MarkDirty(go);

            return new
            {
                success = true,
                message = added
                    ? $"Added noise to CinemachineCamera '{go.name}'."
                    : $"Noise configured on CinemachineCamera '{go.name}'.",
                data = new { instanceID = go.GetInstanceIDCompat(), added }
            };
        }

        internal static object AddExtension(JObject @params)
        {
            var cmCamera = CameraHelpers.FindCinemachineCamera(@params);
            if (cmCamera == null) return new ErrorResponse("Target CinemachineCamera not found.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            string extTypeName = ParamCoercion.CoerceString(
                props["extensionType"] ?? props["extension_type"], null);
            if (string.IsNullOrEmpty(extTypeName))
                return new ErrorResponse("'extensionType' property is required.");

            var extType = CameraHelpers.ResolveComponentType(extTypeName);
            if (extType == null)
                return new ErrorResponse($"Extension type '{extTypeName}' not found.");

            var go = cmCamera.gameObject;
            var existing = go.GetComponent(extType);
            if (existing != null)
                return new { success = true, message = $"Extension '{extTypeName}' already exists on '{go.name}'." };

            var ext = Undo.AddComponent(go, extType);
            SetComponentProperties(ext, props, new[] { "extensionType", "extension_type" });
            CameraHelpers.MarkDirty(go);

            return new
            {
                success = true,
                message = $"Extension '{extTypeName}' added to CinemachineCamera '{go.name}'.",
                data = new { instanceID = go.GetInstanceIDCompat(), extensionType = extTypeName }
            };
        }

        internal static object RemoveExtension(JObject @params)
        {
            var cmCamera = CameraHelpers.FindCinemachineCamera(@params);
            if (cmCamera == null) return new ErrorResponse("Target CinemachineCamera not found.");

            var props = CameraHelpers.ExtractProperties(@params) ?? new JObject();
            string extTypeName = ParamCoercion.CoerceString(
                props["extensionType"] ?? props["extension_type"], null);
            if (string.IsNullOrEmpty(extTypeName))
                return new ErrorResponse("'extensionType' property is required.");

            var extType = CameraHelpers.ResolveComponentType(extTypeName);
            if (extType == null)
                return new ErrorResponse($"Extension type '{extTypeName}' not found.");

            var go = cmCamera.gameObject;
            var ext = go.GetComponent(extType);
            if (ext == null)
                return new ErrorResponse($"Extension '{extTypeName}' not found on '{go.name}'.");

            Undo.DestroyObjectImmediate(ext);
            CameraHelpers.MarkDirty(go);

            return new
            {
                success = true,
                message = $"Extension '{extTypeName}' removed from CinemachineCamera '{go.name}'.",
                data = new { instanceID = go.GetInstanceIDCompat() }
            };
        }

        #endregion

        #region Helpers

        private static void SetFloatSubProp(SerializedProperty parent, string subPropName, JToken value)
        {
            if (value == null || value.Type == JTokenType.Null) return;
            var sub = parent.FindPropertyRelative(subPropName)
                   ?? parent.FindPropertyRelative("m_" + subPropName);
            if (sub != null && sub.propertyType == SerializedPropertyType.Float)
                sub.floatValue = ParamCoercion.CoerceFloat(value, sub.floatValue);
        }

        private static Component SwapPipelineComponent(GameObject go, string stage, string newTypeName)
        {
            var newType = CameraHelpers.ResolveComponentType(newTypeName);
            if (newType == null) return null;

            // Remove existing component of same pipeline stage
            var cmCamera = go.GetComponent(CameraHelpers.CinemachineCameraType);
            if (cmCamera != null)
            {
                var existing = CameraHelpers.GetPipelineComponent(cmCamera, stage);
                if (existing != null && existing.GetType() != newType)
                    Undo.DestroyObjectImmediate(existing);
            }

            // Add new component if not already present
            var comp = go.GetComponent(newType);
            if (comp == null)
                comp = Undo.AddComponent(go, newType);

            return comp;
        }

        private static void SetComponentProperties(Component component, JObject props, string[] skipKeys)
        {
            if (component == null || props == null) return;

            var skipSet = new System.Collections.Generic.HashSet<string>(
                skipKeys, StringComparer.OrdinalIgnoreCase);

            Undo.RecordObject(component, $"Configure {component.GetType().Name}");

            foreach (var kv in props)
            {
                if (skipSet.Contains(kv.Key)) continue;
                ComponentOps.SetProperty(component, kv.Key, kv.Value, out _);
            }
        }

        #endregion
    }
}
