using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MajacloudSdk.Internal;

namespace MajacloudSdk
{
    /// <summary>Per-player JSON documents with revisions. Objects go through <see cref="Majacloud.JsonSerializer"/> (JsonUtility by default).</summary>
    public sealed class MajacloudCloudSave
    {
        public const int MaxBytesPerKey = 262144;
        private readonly MajacloudHttp _http;

        internal MajacloudCloudSave(MajacloudHttp http)
        {
            _http = http;
        }

        public Task<MajacloudResult<MajacloudSaveReceipt>> SaveAsync<T>(string key, T data, long? expectedRevision = null, CancellationToken cancellationToken = default)
        {
            string json;
            try
            {
                json = Majacloud.JsonSerializer.ToJson(data);
            }
            catch (Exception e)
            {
                return Task.FromResult(MajacloudResult<MajacloudSaveReceipt>.Fail(MajacloudErrorCode.Validation, "Could not serialise the save: " + e.Message));
            }
            return SaveJsonAsync(key, json, expectedRevision, cancellationToken);
        }

        /// <summary>Save a raw JSON document (object, array or scalar).</summary>
        public async Task<MajacloudResult<MajacloudSaveReceipt>> SaveJsonAsync(string key, string json, long? expectedRevision = null, CancellationToken cancellationToken = default)
        {
            if (!MajacloudEndpoints.IsValidIdentifier(key)) return MajacloudResult<MajacloudSaveReceipt>.Fail(MajacloudErrorCode.Validation, "Save keys may contain letters, numbers, dots, dashes and underscores (up to 80).");
            if (string.IsNullOrWhiteSpace(json)) return MajacloudResult<MajacloudSaveReceipt>.Fail(MajacloudErrorCode.Validation, "Nothing to save.");
            if (Encoding.UTF8.GetByteCount(json) > MaxBytesPerKey) return MajacloudResult<MajacloudSaveReceipt>.Fail(MajacloudErrorCode.Validation, "Saves can be up to 256 KB each.");
            JsonNode document;
            try
            {
                document = JsonNode.Raw(json);
            }
            catch (JsonParseException e)
            {
                return MajacloudResult<MajacloudSaveReceipt>.Fail(MajacloudErrorCode.Validation, "The save is not valid JSON: " + e.Message);
            }
            var body = JsonNode.Object().Set("data", document);
            if (expectedRevision.HasValue) body.Set("revision", expectedRevision.Value);
            var result = await _http.SendJsonAsync(new HttpRequest { Method = "PUT", Path = MajacloudEndpoints.CloudKey(key), Auth = HttpAuth.Bearer, JsonBody = body.ToJson(), Idempotent = !expectedRevision.HasValue }, cancellationToken);
            if (!result.Ok) return MajacloudResult<MajacloudSaveReceipt>.Fail(result.Error);
            return MajacloudResult<MajacloudSaveReceipt>.Success(Receipt(result.Value));
        }

        public async Task<MajacloudResult<MajacloudCloudSaveItem<T>>> LoadAsync<T>(string key, CancellationToken cancellationToken = default)
        {
            var raw = await LoadJsonAsync(key, cancellationToken);
            if (!raw.Ok) return MajacloudResult<MajacloudCloudSaveItem<T>>.Fail(raw.Error);
            T data;
            try
            {
                data = Majacloud.JsonSerializer.FromJson<T>(raw.Value.Json);
            }
            catch (Exception e)
            {
                return MajacloudResult<MajacloudCloudSaveItem<T>>.Fail(MajacloudErrorCode.ParseError, "Could not read the save as " + typeof(T).Name + ": " + e.Message);
            }
            return MajacloudResult<MajacloudCloudSaveItem<T>>.Success(new MajacloudCloudSaveItem<T> { Key = raw.Value.Key, Data = data, Json = raw.Value.Json, Revision = raw.Value.Revision, UpdatedAt = raw.Value.UpdatedAt });
        }

        /// <summary>Load the raw JSON document. A missing key is a result with <c>IsNotFound</c>.</summary>
        public async Task<MajacloudResult<MajacloudCloudSaveItem<string>>> LoadJsonAsync(string key, CancellationToken cancellationToken = default)
        {
            if (!MajacloudEndpoints.IsValidIdentifier(key)) return MajacloudResult<MajacloudCloudSaveItem<string>>.Fail(MajacloudErrorCode.Validation, "Invalid save key.");
            var result = await _http.SendJsonAsync(new HttpRequest { Method = "GET", Path = MajacloudEndpoints.CloudKey(key), Auth = HttpAuth.Bearer, Idempotent = true }, cancellationToken);
            if (!result.Ok) return MajacloudResult<MajacloudCloudSaveItem<string>>.Fail(result.Error);
            var dataNode = result.Value["data"];
            var json = dataNode.IsNull ? "null" : dataNode.ToJson();
            return MajacloudResult<MajacloudCloudSaveItem<string>>.Success(new MajacloudCloudSaveItem<string> { Key = result.Value.GetString("key") ?? key, Data = json, Json = json, Revision = result.Value.GetLong("revision"), UpdatedAt = result.Value.GetDateTime("updated_at") });
        }

        public async Task<MajacloudResult<bool>> DeleteAsync(string key, CancellationToken cancellationToken = default)
        {
            if (!MajacloudEndpoints.IsValidIdentifier(key)) return MajacloudResult<bool>.Fail(MajacloudErrorCode.Validation, "Invalid save key.");
            var result = await _http.SendJsonAsync(new HttpRequest { Method = "DELETE", Path = MajacloudEndpoints.CloudKey(key), Auth = HttpAuth.Bearer, Idempotent = true }, cancellationToken);
            if (!result.Ok) return MajacloudResult<bool>.Fail(result.Error);
            return MajacloudResult<bool>.Success(result.Value.GetBool("deleted"));
        }

        public async Task<MajacloudResult<IReadOnlyList<MajacloudCloudSaveEntry>>> ListAsync(CancellationToken cancellationToken = default)
        {
            var result = await _http.SendJsonAsync(new HttpRequest { Method = "GET", Path = MajacloudEndpoints.Cloud, Auth = HttpAuth.Bearer, Idempotent = true }, cancellationToken);
            if (!result.Ok) return MajacloudResult<IReadOnlyList<MajacloudCloudSaveEntry>>.Fail(result.Error);
            var list = new List<MajacloudCloudSaveEntry>();
            foreach (var node in result.Value.GetArray("keys").Elements())
            {
                list.Add(new MajacloudCloudSaveEntry { Key = node.GetString("key"), Revision = node.GetLong("revision"), SizeBytes = node.GetLong("size_bytes"), UpdatedAt = node.GetDateTime("updated_at") });
            }
            return MajacloudResult<IReadOnlyList<MajacloudCloudSaveEntry>>.Success(list);
        }

        private static MajacloudSaveReceipt Receipt(JsonNode json) => new MajacloudSaveReceipt { Key = json.GetString("key"), Revision = json.GetLong("revision"), SizeBytes = json.GetLong("size_bytes"), UpdatedAt = json.GetDateTime("updated_at"), Created = json.GetBool("created") };

        // --- Coroutine variants -------------------------------------------------------------------------

        public IEnumerator Save<T>(string key, T data, Action<MajacloudResult<MajacloudSaveReceipt>> callback, long? expectedRevision = null) => MajacloudCoroutine.Run(SaveAsync(key, data, expectedRevision), callback);
        public IEnumerator Load<T>(string key, Action<MajacloudResult<MajacloudCloudSaveItem<T>>> callback) => MajacloudCoroutine.Run(LoadAsync<T>(key), callback);
        public IEnumerator Delete(string key, Action<MajacloudResult<bool>> callback) => MajacloudCoroutine.Run(DeleteAsync(key), callback);
        public IEnumerator List(Action<MajacloudResult<IReadOnlyList<MajacloudCloudSaveEntry>>> callback) => MajacloudCoroutine.Run(ListAsync(), callback);
    }
}
