using System;
using MajacloudSdk.Internal;

namespace MajacloudSdk
{
    public enum MajacloudErrorCode
    {
        Unknown,
        NotConfigured,
        NotSignedIn,
        Validation,
        Unauthorized,
        Forbidden,
        NotFound,
        Conflict,
        RateLimited,
        QuotaExceeded,
        PlanLimit,
        RevisionMismatch,
        ServerError,
        Network,
        Timeout,
        Cancelled,
        ParseError,
        SignatureInvalid,
        LinkExpired,
        LinkCancelled,
    }

    /// <summary>Structured error. <see cref="Message"/> is safe to show to players.</summary>
    public sealed class MajacloudError
    {
        public MajacloudErrorCode Code { get; }
        public string RawCode { get; }
        public string Message { get; }
        public int HttpStatus { get; }
        public string RequestId { get; }
        public float? RetryAfterSeconds { get; }
        internal JsonNode Body { get; }

        internal MajacloudError(MajacloudErrorCode code, string message, int httpStatus = 0, string rawCode = null, string requestId = null, float? retryAfter = null, JsonNode body = null)
        {
            Code = code;
            Message = message ?? code.ToString();
            HttpStatus = httpStatus;
            RawCode = rawCode ?? code.ToString().ToLowerInvariant();
            RequestId = requestId;
            RetryAfterSeconds = retryAfter;
            Body = body;
        }

        public bool IsRetryable => Code == MajacloudErrorCode.Network || Code == MajacloudErrorCode.Timeout || Code == MajacloudErrorCode.ServerError || Code == MajacloudErrorCode.RateLimited;

        /// <summary>Read a top-level or details field from the error body, e.g. "next_claim_at".</summary>
        public string GetDetailString(string key)
        {
            if (Body == null) return null;
            var details = Body["error"].GetObject("details");
            var value = details.GetString(key);
            return value ?? Body.GetString(key);
        }

        public double? GetDetailNumber(string key)
        {
            if (Body == null) return null;
            var details = Body["error"].GetObject("details");
            return details.GetNullableDouble(key) ?? Body.GetNullableDouble(key);
        }

        public DateTime? GetDetailDateTime(string key)
        {
            if (Body == null) return null;
            return Body["error"].GetObject("details").GetDateTime(key) ?? Body.GetDateTime(key);
        }

        public override string ToString()
        {
            var text = "[" + RawCode + "] " + Message;
            if (HttpStatus > 0) text += " (HTTP " + HttpStatus + ")";
            if (!string.IsNullOrEmpty(RequestId)) text += " request " + RequestId;
            return text;
        }
    }

    /// <summary>Result of an SDK call. Never throws; check <see cref="Ok"/>.</summary>
    public class MajacloudResult
    {
        public bool Ok { get; }
        public MajacloudError Error { get; }

        protected MajacloudResult(bool ok, MajacloudError error)
        {
            Ok = ok;
            Error = error;
        }

        public bool IsNotFound => !Ok && Error != null && Error.Code == MajacloudErrorCode.NotFound;
        public bool IsRetryable => !Ok && Error != null && Error.IsRetryable;

        public static MajacloudResult Success() => new MajacloudResult(true, null);
        public static MajacloudResult Fail(MajacloudError error) => new MajacloudResult(false, error ?? new MajacloudError(MajacloudErrorCode.Unknown, "Unknown error"));
        public static MajacloudResult Fail(MajacloudErrorCode code, string message) => Fail(new MajacloudError(code, message));

        public static implicit operator bool(MajacloudResult result) => result != null && result.Ok;

        public void ThrowIfFailed()
        {
            if (!Ok) throw new MajacloudException(Error);
        }
    }

    public sealed class MajacloudResult<T> : MajacloudResult
    {
        public T Value { get; }

        private MajacloudResult(bool ok, T value, MajacloudError error) : base(ok, error)
        {
            Value = value;
        }

        public static MajacloudResult<T> Success(T value) => new MajacloudResult<T>(true, value, null);
        public new static MajacloudResult<T> Fail(MajacloudError error) => new MajacloudResult<T>(false, default, error ?? new MajacloudError(MajacloudErrorCode.Unknown, "Unknown error"));
        public new static MajacloudResult<T> Fail(MajacloudErrorCode code, string message) => Fail(new MajacloudError(code, message));

        public T ValueOr(T fallback) => Ok ? Value : fallback;

        public T ValueOrThrow()
        {
            if (!Ok) throw new MajacloudException(Error);
            return Value;
        }
    }

    public sealed class MajacloudException : Exception
    {
        public MajacloudError Error { get; }

        public MajacloudException(MajacloudError error) : base(error?.ToString() ?? "Majacloud error")
        {
            Error = error;
        }
    }
}
