using System;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;

namespace Jovian.Logger {
    /// <summary>
    /// Sends log messages to the Custom Console editor window over TCP.
    /// Add this component to a GameObject in your scene (or create one at runtime)
    /// to enable remote logging from device builds.
    /// </summary>
    public class RemoteLogSender : MonoBehaviour {
        [Tooltip("IP address of the machine running the Unity Editor")]
        [SerializeField] private string editorHost = "127.0.0.1";
        [Tooltip("Port the Custom Console is listening on")]
        [SerializeField] private int editorPort = 9876;
        [Tooltip("Automatically connect on start")]
        [SerializeField] private bool autoConnect = true;
        [Tooltip("Retry connection every N seconds when disconnected")]
        [SerializeField] private float reconnectInterval = 5f;

        private TcpClient client;
        private NetworkStream stream;
        private readonly ConcurrentQueue<string> sendQueue = new ConcurrentQueue<string>();
        private Thread sendThread;
        private volatile bool running;
        private volatile bool connected;
        private float reconnectTimer;

        private static RemoteLogSender instance;

        /// <summary>Whether the sender is currently connected to the editor.</summary>
        public bool IsConnected => connected;

        /// <summary>The editor host address.</summary>
        public string EditorHost {
            get => editorHost;
            set => editorHost = value;
        }

        /// <summary>The editor port.</summary>
        public int EditorPort {
            get => editorPort;
            set => editorPort = value;
        }

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

        private void OnEnable() {
            Application.logMessageReceivedThreaded += OnUnityLogReceived;
            LoggerUtility.FormattedLogCallback += OnJovianLogReceived;
            LoggerUtility.WatchCallback += OnWatch;
            LoggerUtility.UnwatchCallback += OnUnwatch;

            if (autoConnect) {
                Connect();
            }
        }

        private void OnDisable() {
            Application.logMessageReceivedThreaded -= OnUnityLogReceived;
            LoggerUtility.FormattedLogCallback -= OnJovianLogReceived;
            LoggerUtility.WatchCallback -= OnWatch;
            LoggerUtility.UnwatchCallback -= OnUnwatch;
            Disconnect();
        }

        private void Update() {
            if (!connected && autoConnect) {
                reconnectTimer += Time.unscaledDeltaTime;
                if (reconnectTimer >= reconnectInterval) {
                    reconnectTimer = 0f;
                    Connect();
                }
            }
        }

        /// <summary>Connect to the Custom Console editor.</summary>
        public void Connect() {
            if (connected) return;

            try {
                client = new TcpClient();
                client.NoDelay = true;
                client.SendTimeout = 2000;
                var result = client.BeginConnect(editorHost, editorPort, null, null);
                bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(2));
                if (!success || !client.Connected) {
                    client.Close();
                    client = null;
                    return;
                }
                client.EndConnect(result);
                stream = client.GetStream();
                connected = true;
                running = true;

                sendThread = new Thread(SendLoop) {
                    IsBackground = true,
                    Name = "RemoteLogSender"
                };
                sendThread.Start();

                // Send handshake
                EnqueueMessage("{\"handshake\":true,\"app\":\"" + Application.productName + "\",\"platform\":\"" + Application.platform + "\"}");
                Debug.Log($"[RemoteLog] Connected to editor at {editorHost}:{editorPort}");
            } catch (Exception e) {
                Debug.LogWarning($"[RemoteLog] Failed to connect: {e.Message}");
                CleanupConnection();
            }
        }

        /// <summary>Disconnect from the editor.</summary>
        public void Disconnect() {
            running = false;
            CleanupConnection();
        }

        private void CleanupConnection() {
            connected = false;
            try { stream?.Close(); } catch { }
            try { client?.Close(); } catch { }
            stream = null;
            client = null;
        }

        private void OnJovianLogReceived((JovianLogType logType, LogCategory logCategory, string message) log) {
            if (!connected) return;
            var json = BuildLogJson(log.message, "", (int)log.logType, (int)log.logCategory, true,
                DateTime.Now.ToString("HH:mm:ss.fff"), Time.frameCount);
            EnqueueMessage(json);
        }

        private static readonly string[] LoggerPrefixes = {
            "INFO -> ", "ERROR -> ", "WARNING -> ",
            "EXCEPTION -> ", "ASSERT -> ", "SPAM -> "
        };

        private static bool IsLoggerFormattedMessage(string condition) {
            foreach (var prefix in LoggerPrefixes) {
                if (condition.StartsWith(prefix, StringComparison.Ordinal)) return true;
            }
            if (condition.StartsWith("<color=#", StringComparison.Ordinal) && condition.Length > 22) {
                var afterTag = condition.AsSpan(15);
                foreach (var prefix in LoggerPrefixes) {
                    if (afterTag.StartsWith(prefix.AsSpan(), StringComparison.Ordinal)) return true;
                }
            }
            return false;
        }

        private void OnUnityLogReceived(string condition, string stackTrace, UnityEngine.LogType type) {
            if (!connected) return;
            // Skip logger-formatted messages — those come via OnCustomLog
            if (IsLoggerFormattedMessage(condition)) return;

            int loggerTyper = type switch {
               LogType.Error => (int)JovianLogType.Error,
               LogType.Assert => (int)JovianLogType.Assert,
               LogType.Warning => (int)JovianLogType.Warning,
               LogType.Exception => (int)JovianLogType.Exception,
                _ => (int)JovianLogType.Info,
            };

            string message = condition;
            if (type is LogType.Error or LogType.Assert or LogType.Exception && !string.IsNullOrEmpty(stackTrace)) {
                message = $"{condition}\n{stackTrace}";
            }

            var json = BuildLogJson(message, stackTrace ?? "", loggerTyper, (int)LogCategory.General, false,
                DateTime.Now.ToString("HH:mm:ss.fff"), Time.frameCount);
            EnqueueMessage(json);
        }

        private void OnWatch((string key, string value, LogCategory category) watch) {
            if (!connected) return;
            var sb = new StringBuilder(128);
            sb.Append("{\"watch\":true,\"wk\":");
            AppendJsonString(sb, watch.key);
            sb.Append(",\"wv\":");
            AppendJsonString(sb, watch.value);
            sb.Append(",\"c\":").Append((int)watch.category);
            sb.Append(",\"ts\":");
            AppendJsonString(sb, DateTime.Now.ToString("HH:mm:ss.fff"));
            sb.Append(",\"fc\":").Append(Time.frameCount);
            sb.Append('}');
            EnqueueMessage(sb.ToString());
        }

        private void OnUnwatch(string key) {
            if (!connected) return;
            var sb = new StringBuilder(64);
            sb.Append("{\"unwatch\":true,\"wk\":");
            AppendJsonString(sb, key);
            sb.Append('}');
            EnqueueMessage(sb.ToString());
        }

        private static string BuildLogJson(string message, string stackTrace, int type, int category, bool isCustom, string timestamp, int frame) {
            // Manual JSON building to avoid allocations from JsonUtility
            var sb = new StringBuilder(256);
            sb.Append("{\"m\":");
            AppendJsonString(sb, message);
            sb.Append(",\"s\":");
            AppendJsonString(sb, stackTrace);
            sb.Append(",\"t\":").Append(type);
            sb.Append(",\"c\":").Append(category);
            sb.Append(",\"f\":").Append(isCustom ? "true" : "false");
            sb.Append(",\"ts\":");
            AppendJsonString(sb, timestamp);
            sb.Append(",\"fc\":").Append(frame);
            sb.Append('}');
            return sb.ToString();
        }

        private static void AppendJsonString(StringBuilder sb, string value) {
            if (value == null) { sb.Append("\"\""); return; }
            sb.Append('"');
            foreach (char c in value) {
                switch (c) {
                    case '"': sb.Append("\\\""); break;
                    case '\\': sb.Append("\\\\"); break;
                    case '\n': sb.Append("\\n"); break;
                    case '\r': sb.Append("\\r"); break;
                    case '\t': sb.Append("\\t"); break;
                    default: sb.Append(c); break;
                }
            }
            sb.Append('"');
        }

        private void EnqueueMessage(string json) {
            // Cap queue size to prevent memory issues if sending is slow
            if (sendQueue.Count < 10000) {
                sendQueue.Enqueue(json);
            }
        }

        private void SendLoop() {
            while (running) {
                try {
                    if (sendQueue.TryDequeue(out string json)) {
                        byte[] data = Encoding.UTF8.GetBytes(json + "\n");
                        stream.Write(data, 0, data.Length);
                    } else {
                        Thread.Sleep(5);
                    }
                } catch (Exception) {
                    running = false;
                    connected = false;
                    break;
                }
            }
        }

        private void OnDestroy() {
            if (instance == this) instance = null;
            Disconnect();
        }
    }
}
