using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace eagle {
    public static class ClientTextDiskCache {
        private const int MaxDirtyEntriesBeforeFlush = 50;
        private static readonly TimeSpan MaxTimeBetweenFlushes = TimeSpan.FromSeconds(1);

        [Serializable]
        private class CacheEntry {
            public string llmId;
            public string text;
        }

        [Serializable]
        private class CacheFile {
            public List<CacheEntry> entries = new();
        }

        private class GameCache {
            public Dictionary<string, string> entries = new();
            public int dirtyEntries;
            public DateTime lastFlushUtc = DateTime.MinValue;
            public bool loaded;
        }

        private static readonly object Lock = new();
        private static readonly Dictionary<long, GameCache> Caches = new();
        private static string _cacheDirectory;

        public static Dictionary<string, string> LoadCompletedTexts(long gameId) {
            EnsureCacheDirectoryInitialized();

            lock (Lock) {
                var cache = CacheFor(gameId);
                EnsureLoaded(gameId, cache);
                return new Dictionary<string, string>(cache.entries);
            }
        }

        public static void SaveCompletedText(long gameId, string llmId, string text) {
            if (String.IsNullOrEmpty(llmId) || String.IsNullOrEmpty(text)) { return; }

            lock (Lock) {
                if (_cacheDirectory == null) {
                    Debug.LogWarning(
                            "Cannot save client text cache before the cache directory has " +
                            "been initialized on the main thread.");
                    return;
                }

                var cache = CacheFor(gameId);
                EnsureLoaded(gameId, cache);

                var normalizedText = text ?? "";
                if (cache.entries.TryGetValue(llmId, out var existing) &&
                    existing == normalizedText) {
                    return;
                }

                cache.entries[llmId] = normalizedText;
                cache.dirtyEntries++;
                if (ShouldFlush(cache)) { FlushLocked(gameId, cache); }
            }
        }

        private static void FlushAll() {
            lock (Lock) {
                if (_cacheDirectory == null) { return; }

                foreach (var (gameId, cache) in Caches) {
                    if (cache.dirtyEntries > 0) { FlushLocked(gameId, cache); }
                }
            }
        }

        private static GameCache CacheFor(long gameId) {
            if (!Caches.TryGetValue(gameId, out var cache)) {
                cache = new GameCache();
                Caches[gameId] = cache;
            }

            return cache;
        }

        private static void EnsureLoaded(long gameId, GameCache gameCache) {
            if (gameCache.loaded) { return; }

            gameCache.loaded = true;
            var path = CachePath(gameId);
            if (!File.Exists(path)) { return; }

            try {
                var cache = JsonUtility.FromJson<CacheFile>(File.ReadAllText(path));
                if (cache?.entries == null) { return; }

                foreach (var entry in cache.entries) {
                    if (!String.IsNullOrEmpty(entry.llmId) && !String.IsNullOrEmpty(entry.text)) {
                        gameCache.entries[entry.llmId] = entry.text;
                    }
                }
            } catch (Exception e) {
                Debug.LogWarning($"Failed to load client text cache for game {gameId}: {e}");
                gameCache.entries.Clear();
            }
        }

        private static void EnsureCacheDirectoryInitialized() {
            if (_cacheDirectory != null) { return; }

            _cacheDirectory = Path.Combine(Application.persistentDataPath, "eagle0", "client_text");
            Application.quitting -= FlushAll;
            Application.quitting += FlushAll;
        }

        private static bool ShouldFlush(GameCache cache) => cache.dirtyEntries
                                                                    >= MaxDirtyEntriesBeforeFlush
                                                            || DateTime.UtcNow - cache.lastFlushUtc
                                                                       >= MaxTimeBetweenFlushes;

        private static void FlushLocked(long gameId, GameCache gameCache) {
            try {
                Directory.CreateDirectory(CacheDirectory);
                var cache = new CacheFile();
                foreach (var (id, cachedText) in gameCache.entries) {
                    cache.entries.Add(new CacheEntry { llmId = id, text = cachedText });
                }

                File.WriteAllText(CachePath(gameId), JsonUtility.ToJson(cache));
                gameCache.dirtyEntries = 0;
                gameCache.lastFlushUtc = DateTime.UtcNow;
            } catch (Exception e) {
                Debug.LogWarning($"Failed to save client text cache for game {gameId}: {e}");
            }
        }

        private static string CacheDirectory => _cacheDirectory;

        private static string CachePath(long gameId) =>
                Path.Combine(CacheDirectory, $"{gameId:X}.json");
    }
}
