#nullable enable

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Networking;

namespace HyperstoneSdk
{
    public class HyperstoneClient : MonoBehaviour
    {
        public enum LogLevel
        {
            Debug = 0,
            Info = 1,
            Warning = 2,
            Error = 3
        }

        public LogLevel CurrentLogLevel = LogLevel.Info;

        private WaitForSecondsRealtime _waitForSessionTrackingInterval = new(Constants.SessionTrackingIntervalSeconds);
        private bool _isInitialized;
        private string? _apiUrl;
        private string? _appId;
        private string? _appSecret;
        private string? _currentUid;
        private HyperstoneConfig? _currentConfig;
        private MetricsContainer _metricsCollectionBuffer = new();
        private List<Action<HyperstoneConfig>> _listeners = new();
        private Coroutine? _waitSendMetricsCoroutine;
        private Coroutine? _sessionTrackingRoutine;
        private float _nextAllowedSendTime;
        private DateTime? _lastPauseTime;
        private float _lastEngagementPulseTime;
        private StringBuilder _logSb = new StringBuilder();
        private bool _configIsConsumed = false;
        private IHyperstoneStorage? _storage;

        public void Initialize(string appId, string appSecret, string apiUrl = Constants.DefaultApiUrl)
        {
            if (_isInitialized)
            {
                LogWarning("Client is already initialized");
                return;
            }

            LogInfo($"Initializing with App Id: {appId}");

            _metricsCollectionBuffer.Clear();

            _apiUrl = apiUrl;
            _appId = appId;
            _appSecret = appSecret;
            _storage = new PlayerPrefsHyperstoneStorage(appId);

            _currentUid = _storage.GetUid();

            var jsonConfig = _storage.GetJsonConfig();
            if (string.IsNullOrEmpty(jsonConfig))
            {
                LogDebug("No existing config found, using empty config");
                _currentConfig = HyperstoneConfig.FromJson("{}");
            }
            else
            {
                _currentConfig = HyperstoneConfig.FromJson(jsonConfig);
                LogDebug($"Loaded config from json: {jsonConfig}");
                NotifyConfigListeners(_currentConfig);
            }

            _storage.LoadMetricsReplacingInto(_metricsCollectionBuffer);
            _isInitialized = true;

            LogDebug($"Current UID: {(_currentUid ?? "null")}, Metrics in buffer: {_metricsCollectionBuffer.Count}");

            StartCoroutine(GetConfigRoutine());
            OnSessionStarted();
        }

        private void OnEnable()
        {
            if (_isInitialized)
            {
                LogDebug("Client OnEnable (restoring state)");

                if (_storage == null && !string.IsNullOrEmpty(_appId))
                {
                    LogDebug("Restoring storage...");
                    _storage = new PlayerPrefsHyperstoneStorage(_appId);
                }

                if (_waitForSessionTrackingInterval == null)
                {
                    _waitForSessionTrackingInterval = new(Constants.SessionTrackingIntervalSeconds);
                }

                if (_sessionTrackingRoutine == null)
                {
                    LogDebug("Restarting session tracking routine...");
                    _sessionTrackingRoutine = StartCoroutine(SessionTimeTracker());
                }
            }
        }

        public void LogAdImpression(string adPlatform, string adNetwork, string adFormat, double revenueUsd)
        {
            var metricLabels = new Label[]
            {
                new (MetricLabels.AdPlatform, adPlatform),
                new (MetricLabels.AdNetwork, adNetwork),
                new (MetricLabels.AdFormat, adFormat),
            };

            IncrementMetricValueInternal(SystemMetrics.TotalAdImpressions, 1, metricLabels);
            IncrementMetricValueInternal(SystemMetrics.TotalAdRevenue, (float)revenueUsd, metricLabels);
        }

        public void LogAdRequest(string adPlatform, string adFormat)
        {
            var metricLabels = new Label[]
            {
                new (MetricLabels.AdPlatform, adPlatform),
                new (MetricLabels.AdFormat, adFormat),
            };

            IncrementMetricValueInternal(SystemMetrics.TotalAdRequests, 1, metricLabels);
        }

        public void LogAdError(string adPlatform, string adNetwork, string adFormat, string failReason = "unknown")
        {
            var metricLabels = new Label[]
            {
                new (MetricLabels.AdPlatform, adPlatform),
                new (MetricLabels.AdNetwork, adNetwork),
                new (MetricLabels.AdFormat, adFormat),
                new (MetricLabels.FailReason, failReason),
            };

            IncrementMetricValueInternal(SystemMetrics.TotalAdErrors, 1, metricLabels);
        }

        public void LogIapTransactionSuccess(string productId, string currency, double price)
        {
            var metricLabels = new Label[]
            {
                new (MetricLabels.ProductId, productId),
                new (MetricLabels.Currency, currency),
            };

            IncrementMetricValueInternal(SystemMetrics.TotalIapTransactions, 1, metricLabels);
            IncrementMetricValueInternal(SystemMetrics.TotalIapRevenue, (float)price, metricLabels);
        }

        public void LogIapTransactionError(string productId, string failReason)
        {
            var metricLabels = new Label[]
            {
                new (MetricLabels.ProductId, productId),
                new (MetricLabels.FailReason, failReason),
            };

            IncrementMetricValueInternal(SystemMetrics.TotalIapErrors, 1, metricLabels);
        }

        #region Session tracking
        private void OnSessionStarted()
        {
            LogDebug("OnSessionStarted");
            IncrementMetricValueInternal(SystemMetrics.TotalSessions);
            StopAndFlushSessionTracking();
            _sessionTrackingRoutine = StartCoroutine(SessionTimeTracker());
        }

        private void OnApplicationPause(bool pauseStatus)
        {
            if (pauseStatus)
            {
                // App going to background
                LogDebug("Application paused");
                _lastPauseTime = DateTime.UtcNow;
                StopAndFlushSessionTracking();
            }
            else
            {
                // App returning from background
                LogDebug("Application resumed");
                if (_lastPauseTime != null)
                {
                    var timeAway = DateTime.UtcNow - _lastPauseTime.Value;
                    if (timeAway.TotalSeconds >= Constants.SessionTimeoutSeconds)
                    {
                        // User was away long enough to consider this a new session
                        LogInfo($"Long pause detected ({timeAway.TotalSeconds:F0}s), starting new session");
                        OnSessionStarted();
                    }
                    else
                    {
                        // Just resume tracking
                        StopAndFlushSessionTracking();
                        _sessionTrackingRoutine = StartCoroutine(SessionTimeTracker());
                    }
                }
            }
        }

        private IEnumerator SessionTimeTracker()
        {
            _lastEngagementPulseTime = Time.unscaledTime;
            while (true)
            {
                yield return _waitForSessionTrackingInterval;
                IncrementMetricValueInternal(SystemMetrics.TotalEngagementTime, Constants.EngagementTimeIncrementValue);
                _lastEngagementPulseTime = Time.unscaledTime;
            }
        }

        private void StopAndFlushSessionTracking()
        {
            if (_sessionTrackingRoutine != null)
            {
                float pendingSeconds = Time.unscaledTime - _lastEngagementPulseTime;
                if (pendingSeconds > 1f)
                {
                    IncrementMetricValueInternal(SystemMetrics.TotalEngagementTime, pendingSeconds / 60f);
                }

                StopCoroutine(_sessionTrackingRoutine);
                _sessionTrackingRoutine = null;
            }
        }

        private void OnDisable()
        {
            StopAndFlushSessionTracking();
        }
        #endregion

        public void WatchConfig(Action<HyperstoneConfig> configChanged)
        {
            if (!_listeners.Contains(configChanged))
            {
                _listeners.Add(configChanged);
            }

            if (_currentConfig != null)
            {
                configChanged(_currentConfig);
                _configIsConsumed = true;
            }
        }

        public void UnwatchConfig(Action<HyperstoneConfig> configChanged)
        {
            _listeners.Remove(configChanged);
        }

        private void IncrementMetricValueInternal(string metricName, float value = 1f)
        {
            LogDebug($"Incrementing metric: {metricName} by {value}");
            _metricsCollectionBuffer.Increment(metricName, value);
            _storage?.SaveMetrics(_metricsCollectionBuffer);
            TrySendMetrics();
        }

        private void IncrementMetricValueInternal(string metricName, float value = 1f, params Label[] labels)
        {
            LogDebug($"Incrementing metric: {metricName} by {value} with {labels.Length} labels");
            _metricsCollectionBuffer.Increment(metricName, value, labels);
            _storage?.SaveMetrics(_metricsCollectionBuffer);
            TrySendMetrics();
        }

        public void IncrementCustomMetric(string name, float value)
        {
            IncrementCustomMetric(name, value, Array.Empty<Label>());
        }

        public void IncrementCustomMetric(string name, float value, params Label[] labels)
        {
            if (!_isInitialized)
            {
                LogWarning("Cannot record metric, Hyperstone client is not initialized yet");
                return;
            }

            if (string.IsNullOrEmpty(name) || !Regex.IsMatch(name, @"^[a-zA-Z0-9-_]{1,100}$"))
            {
                LogError($"Invalid metric name: {name}");
                return;
            }

            if (name[0] == '_')
            {
                LogError("Custom metrics could not start with _, it is reserved for internal use");
                return;
            }

            IncrementMetricValueInternal(name, value, labels);
        }

        public void LogCustomEvent(string name, params Label[] labels)
        {
            IncrementCustomMetric(name, 1, labels);
        }

        private string GetUserPropertiesAsJson()
        {
            var jsonBuilder = new JsonBuilder();
            jsonBuilder.BeginObject();

            jsonBuilder.WriteObjectProperty(UserProperties.SdkVersion, Constants.SdkVersion);
            jsonBuilder.WriteObjectProperty(UserProperties.UnityPlatform, Application.platform.ToString());
            jsonBuilder.WriteObjectProperty(UserProperties.UnityVersion, Application.unityVersion);
            jsonBuilder.WriteObjectProperty(UserProperties.App, Application.identifier);
            jsonBuilder.WriteObjectProperty(UserProperties.AppVersion, Application.version);
            jsonBuilder.WriteObjectProperty(UserProperties.Os, SystemInfo.operatingSystem);
            jsonBuilder.WriteObjectProperty(UserProperties.DeviceModel, SystemInfo.deviceModel);
            jsonBuilder.WriteObjectProperty(UserProperties.DeviceType, GetDetailedDeviceType());
            jsonBuilder.WriteObjectProperty(UserProperties.Lang, System.Globalization.CultureInfo.CurrentCulture.Name);
            jsonBuilder.WriteObjectProperty(UserProperties.TimeZone, TimeZoneInfo.Local.ToString());

            if (Application.isEditor || Debug.isDebugBuild)
            {
                jsonBuilder.WriteObjectProperty(UserProperties.IsDeveloper, true);
            }

            jsonBuilder.EndObject();
            return jsonBuilder.ToString();
        }

        private IEnumerator GetConfigRoutine()
        {
            if (string.IsNullOrEmpty(_appId) || string.IsNullOrEmpty(_appSecret))
            {
                LogWarning("Trying to request config while not initialized");
                yield break;
            }

            LogDebug("Requesting config...");

            using UnityWebRequest request = UnityWebRequest.Post($"{_apiUrl}/v1/c", GetUserPropertiesAsJson(), "application/json");

            request.SetRequestHeader(Constants.HeaderAppId, _appId);
            request.SetRequestHeader(Constants.HeaderAppSecret, _appSecret);
            if (!string.IsNullOrEmpty(_currentUid))
            {
                request.SetRequestHeader(Constants.HeaderUid, _currentUid);
            }

            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                var jsonConfig = request.downloadHandler.text;
                _configIsConsumed = false;
                _currentConfig = HyperstoneConfig.FromJson(jsonConfig);
                _currentUid = request.GetResponseHeader(Constants.HeaderUid);
                // _metricsBuffer.Clear();  // new UID -> new metrics ?????

                LogInfo($"Received config {jsonConfig} for UID: {_currentUid}");

                _storage?.SetUid(_currentUid);
                _storage?.SetJsonConfig(jsonConfig);

                NotifyConfigListeners(_currentConfig);
                TrySendMetrics();
            }
            else
            {
                LogWarning($"Failed to obtain config: {request.error}");
            }
        }

        #region Metric send

        private void TrySendMetrics()
        {
            // 1. All metrics should be sent!
            // 2. After send there should be a cooldown to reduce pressure on backend
            // 3. Metric sending should have a small debounce window
            // 4. Debounce window should not block continuous frequent metric sending (like each frame)

            if (_waitSendMetricsCoroutine == null)
            {
                var now = Time.unscaledTime;
                var waitSeconds = Mathf.Max(Constants.MetricSendDebounceSeconds, _nextAllowedSendTime - now);
                LogDebug($"Scheduling metric send after {waitSeconds}s");
                _waitSendMetricsCoroutine = StartCoroutine(WaitSendMetricsRoutine(waitSeconds));
                _nextAllowedSendTime = now + waitSeconds + Constants.MetricSendCooldownSeconds;
            }
        }

        private IEnumerator WaitSendMetricsRoutine(float waitSeconds)
        {
            yield return new WaitForSecondsRealtime(waitSeconds);
            _waitSendMetricsCoroutine = null;
            StartCoroutine(SendMetricsRoutine());
        }

        private IEnumerator SendMetricsRoutine()
        {
            if (string.IsNullOrEmpty(_appId) || string.IsNullOrEmpty(_appSecret))
            {
                LogWarning("Trying to send metrics while not initialized");
                yield break;
            }

            if (string.IsNullOrEmpty(_currentUid))
            {
                LogWarning("Should retrieve config before sending metrics");
                yield break;
            }

            if (_metricsCollectionBuffer.Count == 0)
            {
                LogDebug("No metrics to send");
                yield break;
            }

            // Flush collection buffer -> send buffer
            var metricsSendBuffer = new MetricsContainer();
            metricsSendBuffer.Add(_metricsCollectionBuffer);
            _metricsCollectionBuffer.Clear();

            var metricsString = metricsSendBuffer.ToString();
            LogDebug($"Sending {metricsSendBuffer.Count} metrics: {metricsString}");

            using UnityWebRequest request = UnityWebRequest.Post($"{_apiUrl}/v1/i", metricsString, "text/plain");

            request.SetRequestHeader(Constants.HeaderAppId, _appId);
            request.SetRequestHeader(Constants.HeaderAppSecret, _appSecret);
            request.SetRequestHeader(Constants.HeaderUid, _currentUid);
            if (_configIsConsumed) { request.SetRequestHeader(Constants.HeaderConfigConsumed, "1"); }

            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                LogWarning($"Failed to send metrics: {request.error}, restoring");
                _metricsCollectionBuffer.Add(metricsSendBuffer);
            }
            else
            {
                LogInfo("Metrics sent successfully");
                _storage?.SaveMetrics(_metricsCollectionBuffer);
            }
        }
        #endregion

        public void ClearData()
        {
            _currentUid = null;
            _currentConfig = null;
            _configIsConsumed = false;
            _storage?.Clear();
            _metricsCollectionBuffer.Clear();
            LogInfo("All stored data is cleared");
        }

        private string GetDetailedDeviceType()
        {
            if (SystemInfo.deviceType == DeviceType.Handheld)
            {
                float dpi = Screen.dpi;
                if (dpi <= 0) return "handheld"; // Cannot determine

                float width = Screen.width / dpi;
                float height = Screen.height / dpi;
                float diagonalInches = Mathf.Sqrt(width * width + height * height);

                return diagonalInches >= 7f ? "tablet" : "phone";
            }
            return SystemInfo.deviceType.ToString().ToLowerInvariant();
        }

        private void NotifyConfigListeners(HyperstoneConfig config)
        {
            foreach (var listener in _listeners)
            {
                if (listener != null)
                {
                    try
                    {
                        listener.Invoke(config);
                        _configIsConsumed = true;
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e);
                        LogError("Error while notifying config changes");
                    }
                }
            }
        }

        private void Log(LogLevel level, string message)
        {
            if (level < CurrentLogLevel) return;

            _logSb ??= new StringBuilder();
            _logSb.Clear();
            _logSb.Append("[Hyperstone] ");
            _logSb.Append(message);

            switch (level)
            {
                case LogLevel.Debug:
                case LogLevel.Info:
                    Debug.Log(_logSb.ToString(), this);
                    break;
                case LogLevel.Warning:
                    Debug.LogWarning(_logSb.ToString(), this);
                    break;
                case LogLevel.Error:
                    Debug.LogError(_logSb.ToString(), this);
                    break;
            }
        }

        private void LogDebug(string message) => Log(LogLevel.Debug, message);
        private void LogInfo(string message) => Log(LogLevel.Info, message);
        private void LogWarning(string message) => Log(LogLevel.Warning, message);
        private void LogError(string message) => Log(LogLevel.Error, message);
    }
}
