using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using OkonaPad.Networking.Binary;
using UnityEngine;
using UnityEngine.InputSystem;

namespace OkonaPad.Input
{
    /// <summary>
    /// Lightweight input bridge for Unity WebGL builds.
    /// Receives input from the Okona website shell via JavaScript interop.
    /// No networking code - the website handles all WebSocket connections.
    /// Player spawning is handled by PlayerInputManager's automatic join behavior.
    ///
    /// This implementation uses synchronous processing compatible with all platforms,
    /// including WebGL which does not support C# multithreading.
    ///
    /// The server/shell acts as the authority on controller state. Unity responds to:
    /// - OnControllerConnected: New connection or reconnection
    /// - OnControllerDisconnected: Temporary disconnect (device preserved)
    /// - OnControllerRemoved: Permanent removal (device destroyed)
    /// </summary>
    public class OkonaInputBridge : MonoBehaviour
    {
        private static OkonaInputBridge _instance;
        public static OkonaInputBridge Instance => _instance;

        private bool _enableDebugLogging;

        /// <summary>
        /// Connection state for a controller slot.
        /// </summary>
        public enum DeviceConnectionState
        {
            /// <summary>Controller is connected and active.</summary>
            Connected,
            /// <summary>Controller temporarily disconnected, device preserved for reconnection.</summary>
            Disconnected
        }

        // Device tracking - simple dictionaries (no threading needed)
        private readonly Dictionary<int, OkonaPadControllerDevice> _devices = new();
        private readonly Dictionary<int, DeviceConnectionState> _connectionStates = new();
        private readonly Dictionary<int, OkonaPadControllerState> _deviceStates = new();

        // Message queue - processed synchronously each frame
        private readonly Queue<(int controllerId, byte[] data)> _incomingMessages = new();

        // Per-frame state accumulation
        private readonly Dictionary<int, OkonaPadControllerState> _frameStates = new(8);

        // Stats
        private long _parsedCount;
        private long _droppedCount;
        private int _connectionAttempts;
        private string _lastError;

        private const int MaxMessagesPerFrame = 128;
        private const int MaxQueueSize = 1000;

        internal struct ParsedMsg
        {
            public int ControllerId;
            public MessageType Type;
            public ButtonCode Button;
            public DpadDirection Direction;
            public byte State;
            public short X, Y;
        }

        /// <summary>
        /// Event fired when a new device is connected for the first time.
        /// </summary>
        public event Action<int, OkonaPadControllerDevice> OnDeviceConnected;

        /// <summary>
        /// Event fired when a device temporarily disconnects. The device is preserved
        /// and the player remains paired. Games should show a "reconnect controller" UI.
        /// </summary>
        public event Action<int> OnDeviceDisconnected;

        /// <summary>
        /// Event fired when a previously disconnected device reconnects.
        /// The same device instance is resumed. Games should hide the "reconnect" UI.
        /// </summary>
        public event Action<int, OkonaPadControllerDevice> OnDeviceReconnected;

        /// <summary>
        /// Event fired when a device is permanently removed by the server.
        /// The device is destroyed and the player should be removed from the game.
        /// </summary>
        public event Action<int> OnDeviceRemoved;

        /// <summary>
        /// Event fired when rumble/haptic feedback is requested.
        /// The website shell should subscribe to this and forward to the phone controller.
        /// Parameters: controllerId, lowFrequency, highFrequency
        /// </summary>
        public static event Action<int, float, float> OnRumbleRequested;

#if UNITY_WEBGL && !UNITY_EDITOR
        [DllImport("__Internal")]
        private static extern void OkonaSendRumbleToController(int controllerId, int durationMs);
#endif

        public void EnableDebugLogging(bool enable)
        {
            _enableDebugLogging = enable;
        }

        private void LogDebug(string message)
        {
            if (_enableDebugLogging)
                Debug.Log(message);
        }

        private void LogWarning(string message)
        {
            if (_enableDebugLogging)
                Debug.LogWarning(message);
        }

        private void LogError(string message)
        {
            Debug.LogError(message);
        }

        private void Awake()
        {
            if (_instance != null && _instance != this)
            {
                Destroy(gameObject);
                return;
            }
            _instance = this;
            DontDestroyOnLoad(gameObject);

            OkonaPadControllerDevice.OnRumbleRequested += HandleRumbleRequest;
        }

        private bool _isQuitting;

        private void OnApplicationQuit()
        {
            _isQuitting = true;
        }

        private void OnDestroy()
        {
            if (_instance == this)
                _instance = null;

            OkonaPadControllerDevice.OnRumbleRequested -= HandleRumbleRequest;

            if (_isQuitting)
            {
                _devices.Clear();
                _connectionStates.Clear();
                _deviceStates.Clear();
                return;
            }

            // Clean up players and devices
            var devices = new List<OkonaPadControllerDevice>(_devices.Values);

            foreach (var pi in PlayerInput.all)
            {
                foreach (var d in devices)
                {
                    if (d != null && pi.devices.Contains(d))
                    {
                        Destroy(pi.gameObject);
                        break;
                    }
                }
            }

            foreach (var d in devices)
            {
                if (d == null || !d.added) continue;
                try
                {
                    InputSystem.RemoveDevice(d);
                }
                catch (Exception e)
                {
                    LogWarning($"While removing device {d.displayName}: {e}");
                }
            }

            _devices.Clear();
            _connectionStates.Clear();
            _deviceStates.Clear();
        }

        /// <summary>
        /// Called by the website shell when a controller connects or reconnects.
        /// This is the entry point for JavaScript interop.
        /// </summary>
        public void OnControllerConnected(int controllerId)
        {
            // Ensure singleton is set (may be called before Awake in WebGL)
            if (_instance == null)
                _instance = this;

            // If called on non-singleton instance, redirect to singleton
            if (_instance != this)
            {
                _instance.OnControllerConnected(controllerId);
                return;
            }

            try
            {
                _connectionAttempts++;

                // Check if device already exists (reconnection scenario)
                if (_devices.TryGetValue(controllerId, out var existingDevice) && existingDevice != null)
                {
                    // Reconnection - mark as connected and fire reconnect event
                    _connectionStates[controllerId] = DeviceConnectionState.Connected;
                    LogDebug($"[OkonaInputBridge] Controller {controllerId} reconnected");
                    OnDeviceReconnected?.Invoke(controllerId, existingDevice);
                    return;
                }

                // New connection - create device
                var device = OkonaPadControllerDevice.Create(controllerId);
                if (device != null)
                {
                    if (device.deviceId == InputDevice.InvalidDeviceId)
                    {
                        LogError($"[OkonaInputBridge] Device created but not added to Input System for Controller {controllerId}");
                        return;
                    }

                    if (!device.enabled)
                    {
                        LogWarning($"[OkonaInputBridge] Device for Controller {controllerId} created but not yet enabled");
                    }

                    _devices[controllerId] = device;
                    _connectionStates[controllerId] = DeviceConnectionState.Connected;
                    _deviceStates[controllerId] = new OkonaPadControllerState();

                    // Queue initial state event to ensure device is recognized
                    device.UpdateState(_deviceStates[controllerId]);

                    OnDeviceConnected?.Invoke(controllerId, device);
                }
                else
                {
                    LogError($"[OkonaInputBridge] Failed to create device for Controller {controllerId}");
                }
            }
            catch (Exception e)
            {
                _lastError = $"Connect error: {e.Message}";
            }
        }

        /// <summary>
        /// Called by the website shell when a controller temporarily disconnects.
        /// The device is preserved for reconnection - the server will call OnControllerRemoved
        /// if the controller should be permanently removed.
        /// </summary>
        public void OnControllerDisconnected(int controllerId)
        {
            // Ensure singleton is set (may be called before Awake in WebGL)
            if (_instance == null)
                _instance = this;

            // If called on non-singleton instance, redirect to singleton
            if (_instance != this)
            {
                _instance.OnControllerDisconnected(controllerId);
                return;
            }

            try
            {
                if (!_devices.TryGetValue(controllerId, out var device))
                {
                    LogWarning($"[OkonaInputBridge] OnControllerDisconnected called for unknown controller {controllerId}");
                    return;
                }

                // Mark as disconnected but keep the device
                _connectionStates[controllerId] = DeviceConnectionState.Disconnected;

                // Reset device to neutral state to avoid stuck inputs
                if (device != null)
                {
                    var neutralState = new OkonaPadControllerState();
                    _deviceStates[controllerId] = neutralState;
                    try
                    {
                        device.UpdateState(neutralState);
                    }
                    catch (Exception e)
                    {
                        LogWarning($"[OkonaInputBridge] Failed to reset device state: {e.Message}");
                    }
                }

                LogDebug($"[OkonaInputBridge] Controller {controllerId} disconnected (device preserved)");
                OnDeviceDisconnected?.Invoke(controllerId);
            }
            catch (Exception e)
            {
                LogError($"[OkonaInputBridge] Error in OnControllerDisconnected for Controller {controllerId}: {e}");
            }
        }

        /// <summary>
        /// Called by the website shell when a controller should be permanently removed.
        /// This destroys the device and the player should be removed from the game.
        /// </summary>
        public void OnControllerRemoved(int controllerId)
        {
            // Ensure singleton is set (may be called before Awake in WebGL)
            if (_instance == null)
                _instance = this;

            // If called on non-singleton instance, redirect to singleton
            if (_instance != this)
            {
                _instance.OnControllerRemoved(controllerId);
                return;
            }

            try
            {
                if (!_devices.TryGetValue(controllerId, out var device))
                {
                    LogWarning($"[OkonaInputBridge] OnControllerRemoved called for unknown controller {controllerId}");
                    return;
                }

                // Remove from tracking
                _devices.Remove(controllerId);
                _connectionStates.Remove(controllerId);
                _deviceStates.Remove(controllerId);

                // Remove device from InputSystem - this triggers PlayerInput.onDeviceLost
                if (device != null)
                {
                    try
                    {
                        InputSystem.RemoveDevice(device);
                    }
                    catch (Exception e)
                    {
                        LogError($"[OkonaInputBridge] RemoveDevice failed: {e}");
                    }
                }

                LogDebug($"[OkonaInputBridge] Controller {controllerId} permanently removed");
                OnDeviceRemoved?.Invoke(controllerId);
            }
            catch (Exception e)
            {
                LogError($"[OkonaInputBridge] Error in OnControllerRemoved for Controller {controllerId}: {e}");
            }
        }

        /// <summary>
        /// Called by the website shell with binary input data from a controller.
        /// This is the main entry point for input messages.
        /// </summary>
        public void ReceiveControllerMessage(int controllerId, byte[] data)
        {
            // Ensure singleton is set (may be called before Awake in WebGL)
            if (_instance == null)
                _instance = this;

            // If called on non-singleton instance, redirect to singleton
            if (_instance != this)
            {
                _instance.ReceiveControllerMessage(controllerId, data);
                return;
            }

            EnqueueMessage(controllerId, data);
        }

        /// <summary>
        /// Called by JavaScript via SendMessage with JSON-encoded data.
        /// Format: { "controllerId": 1, "data": "base64encodedstring" }
        /// This is needed because Unity's SendMessage can only pass strings.
        /// </summary>
        public void ReceiveControllerMessageBase64(string jsonData)
        {
            // Ensure singleton is set (may be called before Awake in WebGL)
            if (_instance == null)
                _instance = this;

            // If called on non-singleton instance, redirect to singleton
            if (_instance != this)
            {
                _instance.ReceiveControllerMessageBase64(jsonData);
                return;
            }

            try
            {
                // Simple JSON parsing without external dependencies
                // Expected format: {"controllerId":1,"data":"base64string"}
                var controllerIdStart = jsonData.IndexOf("\"controllerId\":", StringComparison.Ordinal);
                var dataStart = jsonData.IndexOf("\"data\":\"", StringComparison.Ordinal);

                if (controllerIdStart < 0 || dataStart < 0)
                {
                    LogError("[OkonaInputBridge] Invalid JSON format for ReceiveControllerMessageBase64");
                    return;
                }

                // Parse controllerId
                var idStart = controllerIdStart + 15; // length of "\"controllerId\":"
                var idEnd = jsonData.IndexOfAny(new[] { ',', '}' }, idStart);
                var controllerIdStr = jsonData.Substring(idStart, idEnd - idStart).Trim();
                if (!int.TryParse(controllerIdStr, out var controllerId))
                {
                    LogError($"[OkonaInputBridge] Failed to parse controllerId: {controllerIdStr}");
                    return;
                }

                // Parse base64 data
                var base64Start = dataStart + 8; // length of "\"data\":\""
                var base64End = jsonData.IndexOf('"', base64Start);
                var base64Data = jsonData.Substring(base64Start, base64End - base64Start);

                var data = Convert.FromBase64String(base64Data);
                EnqueueMessage(controllerId, data);
            }
            catch (Exception e)
            {
                LogError($"[OkonaInputBridge] Error parsing message: {e.Message}");
            }
        }

        private void EnqueueMessage(int controllerId, byte[] data)
        {
            // Drop oldest messages if queue is full
            while (_incomingMessages.Count >= MaxQueueSize)
            {
                _incomingMessages.Dequeue();
                _droppedCount++;
            }

            _incomingMessages.Enqueue((controllerId, data));
        }

        private void Update()
        {
            int processed = 0;
            _frameStates.Clear();

            // Process queued messages synchronously
            while (processed < MaxMessagesPerFrame && _incomingMessages.Count > 0)
            {
                var (controllerId, data) = _incomingMessages.Dequeue();

                if (TryParseSingleFrame(controllerId, data, out var pm))
                {
                    AccumulateParsed(pm, _frameStates);
                    _parsedCount++;
                }
                else
                {
                    _droppedCount++;
                }
                processed++;
            }

            // Apply accumulated state to devices
            foreach (var kv in _frameStates)
            {
                var controllerId = kv.Key;
                var newState = kv.Value;

                if (!_devices.TryGetValue(controllerId, out var device) || device == null)
                    continue;

                // Don't send input for disconnected controllers
                if (_connectionStates.TryGetValue(controllerId, out var state) && state == DeviceConnectionState.Disconnected)
                    continue;
                if (!device.enabled || device.deviceId == InputDevice.InvalidDeviceId)
                    continue;

                _deviceStates[controllerId] = newState;

                try
                {
                    device.UpdateState(newState);
                }
                catch (InvalidOperationException e)
                {
                    LogWarning($"[OkonaInputBridge] Device State update failed (cid={controllerId}): {e.Message}");
                }
            }

            // Periodic stats logging
            if (Time.frameCount % 1000 == 0 && (_parsedCount > 0 || _droppedCount > 0))
            {
                LogDebug($"[OkonaInputBridge] Parsed: {_parsedCount}, Dropped: {_droppedCount}");
            }
        }

        internal static bool TryParseSingleFrame(int controllerId, byte[] data, out ParsedMsg pm)
        {
            pm = default;

            if (data == null || data.Length < 3) return false;

            ushort len = BinaryProtocolHelper.ReadUInt16BigEndian(data, 0);
            if (len != data.Length - 2) return false;

            var type = (MessageType)data[2];
            switch (type)
            {
                case MessageType.Button:
                    if (data.Length != 5) return false;
                    pm.ControllerId = controllerId;
                    pm.Type = type;
                    pm.Button = (ButtonCode)data[3];
                    pm.State = data[4];
                    return true;

                case MessageType.Dpad:
                    if (data.Length != 5) return false;
                    pm.ControllerId = controllerId;
                    pm.Type = type;
                    pm.Direction = (DpadDirection)data[3];
                    pm.State = data[4];
                    return true;

                case MessageType.Joystick:
                    if (data.Length != 7) return false;
                    pm.ControllerId = controllerId;
                    pm.Type = type;
                    pm.X = BinaryProtocolHelper.ReadInt16BigEndian(data, 3);
                    pm.Y = BinaryProtocolHelper.ReadInt16BigEndian(data, 5);
                    return true;

                case MessageType.Join:
                case MessageType.Pong:
                    pm.ControllerId = controllerId;
                    pm.Type = type;
                    return true;

                default:
                    return false;
            }
        }

        private void AccumulateParsed(in ParsedMsg pm, Dictionary<int, OkonaPadControllerState> accumulatedStates)
        {
            if (pm.Type == MessageType.Join || pm.Type == MessageType.Pong)
                return;

            OkonaPadControllerState state =
                accumulatedStates.TryGetValue(pm.ControllerId, out var accState) ? accState :
                    (_deviceStates.TryGetValue(pm.ControllerId, out var baseState) ? baseState : new OkonaPadControllerState());

            switch (pm.Type)
            {
                case MessageType.Button:
                    bool pressed = pm.State != 0;
                    switch (pm.Button)
                    {
                        case ButtonCode.A:
                            state = state.WithButton(OkonaPadControllerState.ButtonA, pressed);
                            break;
                        case ButtonCode.B:
                            state = state.WithButton(OkonaPadControllerState.ButtonB, pressed);
                            break;
                        case ButtonCode.X:
                            state = state.WithButton(OkonaPadControllerState.ButtonX, pressed);
                            break;
                        case ButtonCode.Y:
                            state = state.WithButton(OkonaPadControllerState.ButtonY, pressed);
                            break;
                        case ButtonCode.Start:
                            state = state.WithButton(OkonaPadControllerState.ButtonStart, pressed);
                            break;
                        case ButtonCode.Select:
                            state = state.WithButton(OkonaPadControllerState.ButtonSelect, pressed);
                            break;
                    }
                    break;

                case MessageType.Dpad:
                    bool dPressed = pm.State != 0;
                    switch (pm.Direction)
                    {
                        case DpadDirection.Up:
                            state = state.WithButton(OkonaPadControllerState.DpadUp, dPressed);
                            break;
                        case DpadDirection.Down:
                            state = state.WithButton(OkonaPadControllerState.DpadDown, dPressed);
                            break;
                        case DpadDirection.Left:
                            state = state.WithButton(OkonaPadControllerState.DpadLeft, dPressed);
                            break;
                        case DpadDirection.Right:
                            state = state.WithButton(OkonaPadControllerState.DpadRight, dPressed);
                            break;
                    }
                    UpdateStickFromDirectionalPad(ref state);
                    break;

                case MessageType.Joystick:
                    float fx = BinaryProtocolHelper.JoystickInt16ToFloat(pm.X);
                    float fy = BinaryProtocolHelper.JoystickInt16ToFloat(pm.Y);
                    state.leftStick = new Vector2(fx, fy);

                    const float T = 0.5f;
                    state = state.WithButton(OkonaPadControllerState.DpadUp, fy > T);
                    state = state.WithButton(OkonaPadControllerState.DpadDown, fy < -T);
                    state = state.WithButton(OkonaPadControllerState.DpadLeft, fx < -T);
                    state = state.WithButton(OkonaPadControllerState.DpadRight, fx > T);
                    break;
            }

            accumulatedStates[pm.ControllerId] = state;
        }

        private static void UpdateStickFromDirectionalPad(ref OkonaPadControllerState state)
        {
            var x = 0f;
            var y = 0f;

            if (state.IsButtonPressed(OkonaPadControllerState.DpadLeft))
                x = -1f;
            else if (state.IsButtonPressed(OkonaPadControllerState.DpadRight))
                x = 1f;

            if (state.IsButtonPressed(OkonaPadControllerState.DpadDown))
                y = -1f;
            else if (state.IsButtonPressed(OkonaPadControllerState.DpadUp))
                y = 1f;

            if (x != 0 && y != 0)
            {
                x *= 0.707f;
                y *= 0.707f;
            }

            state.leftStick = new Vector2(x, y);
        }

        private void HandleRumbleRequest(int controllerId, float lowFrequency, float highFrequency)
        {
            var duration = Mathf.RoundToInt(Mathf.Max(lowFrequency, highFrequency) * 100f);
            if (duration > 0)
            {
                OnRumbleRequested?.Invoke(controllerId, lowFrequency, highFrequency);

#if UNITY_WEBGL && !UNITY_EDITOR
                OkonaSendRumbleToController(controllerId, duration);
#endif
            }
        }

        /// <summary>
        /// Gets the device for a specific controller ID, or null if not found.
        /// Note: Device may exist but be in Disconnected state.
        /// </summary>
        public OkonaPadControllerDevice GetDevice(int controllerId)
        {
            _devices.TryGetValue(controllerId, out var device);
            return device;
        }

        /// <summary>
        /// Gets the connection state for a controller, or null if controller doesn't exist.
        /// </summary>
        public DeviceConnectionState? GetConnectionState(int controllerId)
        {
            if (_connectionStates.TryGetValue(controllerId, out var state))
                return state;
            return null;
        }

        /// <summary>
        /// Gets all controller IDs that have devices (both connected and disconnected).
        /// </summary>
        public int[] GetAllControllerIds()
        {
            return _devices.Keys.ToArray();
        }

        /// <summary>
        /// Gets all currently active (connected) controller IDs.
        /// </summary>
        public int[] GetConnectedControllerIds()
        {
            return _devices.Keys
                .Where(id => _connectionStates.TryGetValue(id, out var state) && state == DeviceConnectionState.Connected)
                .ToArray();
        }

        /// <summary>
        /// Gets all temporarily disconnected controller IDs.
        /// </summary>
        public int[] GetDisconnectedControllerIds()
        {
            return _devices.Keys
                .Where(id => _connectionStates.TryGetValue(id, out var state) && state == DeviceConnectionState.Disconnected)
                .ToArray();
        }

        /// <summary>
        /// Diagnostic info for debugging.
        /// </summary>
        public struct DiagnosticInfo
        {
            public long messagesReceived;
            public int queueSize;
            public int connectedDevices;
            public int disconnectedDevices;
            public int connectionAttempts;
            public string lastError;
        }

        /// <summary>
        /// Gets diagnostic information for the debug UI.
        /// </summary>
        public DiagnosticInfo GetDiagnostics()
        {
            int connected = 0;
            int disconnected = 0;
            foreach (var state in _connectionStates.Values)
            {
                if (state == DeviceConnectionState.Connected)
                    connected++;
                else
                    disconnected++;
            }

            return new DiagnosticInfo
            {
                messagesReceived = _parsedCount + _droppedCount,
                queueSize = _incomingMessages.Count,
                connectedDevices = connected,
                disconnectedDevices = disconnected,
                connectionAttempts = _connectionAttempts,
                lastError = _lastError
            };
        }
    }
}
