using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using UnityEngine;

namespace Davanci.Nexus
{
    // Game-agnostic cloud save. The game serializes its own save however it likes and hands
    // Nexus an opaque byte[]; Nexus wraps it with metadata, GZips it, mirrors it locally and
    // syncs it to the player's own cloud (iCloud Key-Value Store on iOS, Play Games Saved
    // Games on Android). Divergence is resolved by last-write-wins on timestamp, with a
    // monotonic counter as a clock-skew tiebreaker.

    public enum NexusSaveSource { None, Local, Cloud }

    public struct NexusSaveResult
    {
        public bool HasData;
        public byte[] Payload;          // the game's opaque save, or null
        public NexusSaveSource Source;  // where the winning save came from
        public long TimestampUtcMs;
    }

    // The metadata wrapper around the game's opaque payload. JsonUtility-friendly (payload
    // kept as base64 since JsonUtility serializes byte[] poorly).
    [Serializable]
    public class NexusSaveEnvelope
    {
        public int schema = 1;
        public string payloadB64;
        public long timestampUtcMs;
        public long counter;     // monotonic per origin device; tiebreaker under equal timestamps
        public string deviceId;  // device that produced this save

        public byte[] Payload
        {
            get => string.IsNullOrEmpty(payloadB64) ? Array.Empty<byte>() : Convert.FromBase64String(payloadB64);
            set => payloadB64 = value == null ? null : Convert.ToBase64String(value);
        }
    }

    // GZip(JSON(envelope)) <-> envelope. Decode is corruption-tolerant: bad bytes yield null.
    public static class NexusSaveCodec
    {
        public static byte[] Encode(NexusSaveEnvelope env)
        {
            byte[] raw = Encoding.UTF8.GetBytes(JsonUtility.ToJson(env));
            using var ms = new MemoryStream();
            using (var gz = new GZipStream(ms, System.IO.Compression.CompressionLevel.Optimal, leaveOpen: true))
                gz.Write(raw, 0, raw.Length);
            return ms.ToArray();
        }

        public static NexusSaveEnvelope Decode(byte[] data)
        {
            if (data == null || data.Length == 0) return null;
            try
            {
                using var ms = new MemoryStream(data);
                using var gz = new GZipStream(ms, CompressionMode.Decompress);
                using var outMs = new MemoryStream();
                gz.CopyTo(outMs);
                string json = Encoding.UTF8.GetString(outMs.ToArray());
                var env = JsonUtility.FromJson<NexusSaveEnvelope>(json);
                return string.IsNullOrEmpty(env?.payloadB64) && env?.timestampUtcMs == 0 ? null : env;
            }
            catch (Exception e)
            {
                Debug.LogWarning($"[Nexus] Save decode failed: {e.Message}");
                return null;
            }
        }
    }

    // Pure last-write-wins resolution. Deterministic so two endpoints converge identically.
    public static class NexusSaveMerge
    {
        public static NexusSaveEnvelope PickWinner(NexusSaveEnvelope a, NexusSaveEnvelope b)
        {
            if (a == null) return b;
            if (b == null) return a;
            if (a.timestampUtcMs != b.timestampUtcMs)
                return a.timestampUtcMs > b.timestampUtcMs ? a : b;
            if (a.counter != b.counter)
                return a.counter > b.counter ? a : b;
            // Final deterministic tiebreak: both endpoints pick the same winner regardless of order.
            return string.CompareOrdinal(a.deviceId ?? "", b.deviceId ?? "") >= 0 ? a : b;
        }
    }

    // Last-write-wins save orchestration (provider + local mirror are injectable for tests).
    public sealed class NexusCloudSaveCore
    {
        readonly INexusCloudProvider _cloud;
        readonly INexusLocalStore _local;
        readonly Func<long> _clock;
        readonly string _deviceId;
        long _counter;

        public NexusCloudSaveCore(INexusCloudProvider cloud, INexusLocalStore local,
                                  Func<long> clock = null, string deviceId = null)
        {
            _cloud = cloud ?? new LocalOnlyProvider();
            _local = local ?? new InMemoryLocalStore();
            _clock = clock ?? (() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
            _deviceId = string.IsNullOrEmpty(deviceId) ? SystemInfo.deviceUniqueIdentifier : deviceId;

            // Resume the monotonic counter from whatever is already on disk.
            var existing = NexusSaveCodec.Decode(_local.Read());
            if (existing != null) _counter = existing.counter;
        }

        public bool IsCloudAvailable => _cloud.Available;

        public void Save(byte[] payload, Action<bool> onDone = null)
        {
            var env = new NexusSaveEnvelope
            {
                Payload = payload,
                timestampUtcMs = _clock(),
                counter = ++_counter,
                deviceId = _deviceId,
            };
            byte[] bytes = NexusSaveCodec.Encode(env);
            _local.Write(bytes);

            if (_cloud.Available) _cloud.Push(bytes, ok => onDone?.Invoke(ok));
            else onDone?.Invoke(false);
        }

        public void Load(Action<NexusSaveResult> onDone)
        {
            var localEnv = NexusSaveCodec.Decode(_local.Read());
            if (!_cloud.Available) { Resolve(localEnv, null, cloudOk: false, onDone); return; }

            _cloud.Pull((ok, data) =>
            {
                var cloudEnv = ok ? NexusSaveCodec.Decode(data) : null;
                Resolve(localEnv, cloudEnv, ok, onDone);
            });
        }

        void Resolve(NexusSaveEnvelope local, NexusSaveEnvelope cloud, bool cloudOk,
                     Action<NexusSaveResult> onDone)
        {
            var winner = NexusSaveMerge.PickWinner(local, cloud);
            if (winner == null)
            {
                onDone?.Invoke(new NexusSaveResult { HasData = false, Source = NexusSaveSource.None });
                return;
            }

            if (winner.counter > _counter) _counter = winner.counter;
            byte[] bytes = NexusSaveCodec.Encode(winner);

            bool fromCloud = ReferenceEquals(winner, cloud);
            if (fromCloud)
            {
                // Cloud is newer: refresh the local mirror.
                _local.Write(bytes);
            }
            else if (cloudOk && !ReferenceEquals(cloud, winner))
            {
                // Local is newer (or cloud was empty): push it up so the cloud catches up.
                _cloud.Push(bytes, null);
            }

            onDone?.Invoke(new NexusSaveResult
            {
                HasData = true,
                Payload = winner.Payload,
                Source = fromCloud ? NexusSaveSource.Cloud : NexusSaveSource.Local,
                TimestampUtcMs = winner.timestampUtcMs,
            });
        }
    }

    // Static facade. iOS → iCloud KVS, Android → Play Games Saved Games, editor → local mirror.
    public static class NexusCloudSave
    {
        static NexusCloudSaveCore _core;

        public static NexusCloudSaveCore Core => _core ??= BuildDefault();

        static NexusCloudSaveCore BuildDefault()
            => new NexusCloudSaveCore(SelectProvider(), new FileLocalStore());

        static INexusCloudProvider SelectProvider()
        {
            var cfg = NexusConfig.Load();
            if (cfg == null || !cfg.enableCloudSave)
                return new LocalOnlyProvider();

#if UNITY_IOS && !UNITY_EDITOR
            return new NexusICloudProvider();
#elif UNITY_ANDROID && DAVANCI_GPGS && !UNITY_EDITOR
            return new NexusGpgsProvider(cfg.gpgsSnapshotName);
#else
            return new LocalOnlyProvider();
#endif
        }

        public static bool IsCloudAvailable => Core.IsCloudAvailable;

        public static void Save(string text, Action<bool> onDone = null)
            => Save(text == null ? null : Encoding.UTF8.GetBytes(text), onDone);

        public static void Load(Action<string> onLoaded)
            => LoadResult(res => onLoaded?.Invoke(res.HasData ? Encoding.UTF8.GetString(res.Payload) : null));

        public static void Save(byte[] payload, Action<bool> onDone = null) => Core.Save(payload, onDone);
        public static void LoadBytes(Action<byte[]> onLoaded)
            => LoadResult(res => onLoaded?.Invoke(res.HasData ? res.Payload : null));

        public static void LoadResult(Action<NexusSaveResult> onDone) => Core.Load(onDone);

        public static void OverrideCoreForTesting(NexusCloudSaveCore core) => _core = core;
    }
}
