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

namespace MajacloudSdk
{
    /// <summary>
    /// Live, RSA-signed configuration. Offline-first: the last verified
    /// payload is cached and used until a newer one is fetched.
    /// </summary>
    public sealed class MajacloudRemoteConfig
    {
        private readonly MajacloudHttp _http;
        private readonly MajacloudSettings _settings;
        private readonly ConfigCache _cache;
        private readonly MajacloudLog _log;
        private ConfigSnapshot _snapshot;
        private string _etag;
        private Task<MajacloudResult<MajacloudConfigFetchResult>> _inFlight;
        private string _fetchedPublicKeyXml;

        public bool IsLoaded => _snapshot != null;
        public long Version => _snapshot?.Version ?? 0;
        public MajacloudConfigSource Source { get; private set; } = MajacloudConfigSource.None;
        public bool IsFromCache => Source == MajacloudConfigSource.Cache || Source == MajacloudConfigSource.CacheValidated;
        public bool IsSignatureVerified => _snapshot != null && _snapshot.SignatureVerified;
        public DateTime? LastFetchedAt { get; private set; }
        public int Count => _snapshot?.Keys.Count ?? 0;
        public IReadOnlyList<string> Keys => _snapshot != null ? (IReadOnlyList<string>)_snapshot.Keys : Array.Empty<string>();

        public IReadOnlyList<MajacloudConfigValue> Values
        {
            get
            {
                var list = new List<MajacloudConfigValue>();
                if (_snapshot == null) return list;
                foreach (var key in _snapshot.Keys) list.Add(_snapshot.Values[key]);
                return list;
            }
        }

        public event Action<MajacloudRemoteConfig> ConfigUpdated;

        internal MajacloudRemoteConfig(MajacloudHttp http, MajacloudSettings settings, ConfigCache cache, MajacloudLog log)
        {
            _http = http;
            _settings = settings;
            _cache = cache;
            _log = log;
        }

        private bool AllowUnsigned => _settings.AllowUnsignedConfigOnLocalhost && _settings.IsLocalhost;

        // The key pinned in the settings asset (Setup ▸ Test connection) wins; otherwise the one fetched on first use.
        private string PublicKeyXml => !string.IsNullOrEmpty(_settings.ConfigPublicKeyXml) ? _settings.ConfigPublicKeyXml : _fetchedPublicKeyXml;

        /// <summary>Load the cached payload synchronously (called by Majacloud.Initialize).</summary>
        internal void LoadCacheIntoMemory()
        {
            if (!_cache.TryLoad(out var json, out var etag)) return;
            if (ConfigVerifier.TryParse(json, _settings.ProjectId, PublicKeyXml, AllowUnsigned, out var snapshot, out var error, out _))
            {
                _snapshot = snapshot;
                _etag = etag;
                Source = MajacloudConfigSource.Cache;
                return;
            }
            _log.Warning("Discarding cached Remote Config: " + error);
            _cache.Clear();
            _etag = null;
        }

        /// <summary>Fetch the latest config. With a valid cache, a 304 costs one tiny request.</summary>
        public Task<MajacloudResult<MajacloudConfigFetchResult>> FetchAsync(bool force = false, CancellationToken cancellationToken = default)
        {
            if (_inFlight != null && !_inFlight.IsCompleted) return _inFlight;
            _inFlight = FetchInternalAsync(force, cancellationToken);
            return _inFlight;
        }

        private async Task<MajacloudResult<MajacloudConfigFetchResult>> FetchInternalAsync(bool force, CancellationToken cancellationToken)
        {
            if (string.IsNullOrEmpty(PublicKeyXml))
            {
                // Trust on first use: always try to verify; pin the key through Setup for production builds.
                var project = await _http.SendJsonAsync(new HttpRequest { Method = "GET", Path = MajacloudEndpoints.Project, Idempotent = true }, cancellationToken);
                if (project.Ok)
                {
                    _fetchedPublicKeyXml = project.Value.GetObject("project").GetString("config_public_key_xml") ?? string.Empty;
                    if (!string.IsNullOrEmpty(_fetchedPublicKeyXml)) _log.Warning("Remote Config public key fetched from the server (trust on first use). Store it via Tools ▸ Majacloud ▸ Setup for production builds.");
                }
            }
            var response = await _http.SendAsync(new HttpRequest
            {
                Method = "GET",
                Path = MajacloudEndpoints.Config,
                Idempotent = true,
                IfNoneMatch = !force && _snapshot != null ? _etag : null,
            }, cancellationToken);

            if (response.Status == 304 && _snapshot != null && response.IsSuccess)
            {
                Source = MajacloudConfigSource.CacheValidated;
                LastFetchedAt = DateTime.UtcNow;
                return MajacloudResult<MajacloudConfigFetchResult>.Success(Describe(false));
            }
            if (!response.IsSuccess)
            {
                var error = MajacloudHttp.MapError(response);
                if (_snapshot != null && error.IsRetryable)
                {
                    _log.Warning("Remote Config fetch failed (" + error.Message + "); using the cached copy.");
                    return MajacloudResult<MajacloudConfigFetchResult>.Success(Describe(false));
                }
                return MajacloudResult<MajacloudConfigFetchResult>.Fail(error);
            }
            if (!ConfigVerifier.TryParse(response.Text, _settings.ProjectId, PublicKeyXml, AllowUnsigned, out var snapshot, out var reason, out var code))
            {
                _log.Error("Remote Config rejected: " + reason);
                return MajacloudResult<MajacloudConfigFetchResult>.Fail(new MajacloudError(code, reason, (int)response.Status, null, response.RequestId));
            }
            if (_snapshot != null && snapshot.Version < _snapshot.Version)
            {
                _log.Error("Remote Config rejected: version " + snapshot.Version + " is older than cached version " + _snapshot.Version + ".");
                return MajacloudResult<MajacloudConfigFetchResult>.Fail(new MajacloudError(MajacloudErrorCode.Validation, "Server config is older than the cached copy.", (int)response.Status, "config_downgrade", response.RequestId));
            }
            var changed = _snapshot == null || _snapshot.Checksum != snapshot.Checksum;
            _snapshot = snapshot;
            _etag = response.ETag;
            Source = MajacloudConfigSource.Network;
            LastFetchedAt = DateTime.UtcNow;
            if (!_cache.Save(response.Text, response.ETag)) _log.Warning("Remote Config payload is too large for the local cache; it will be fetched on every start.");
            if (changed) ConfigUpdated?.Invoke(this);
            return MajacloudResult<MajacloudConfigFetchResult>.Success(Describe(changed));
        }

        private MajacloudConfigFetchResult Describe(bool changed) => new MajacloudConfigFetchResult { Version = Version, Source = Source, Changed = changed, SignatureVerified = IsSignatureVerified, Count = Count };

        public void ClearCache()
        {
            _cache.Clear();
            _etag = null;
        }

        // --- Getters (lenient: strings, numbers and booleans convert sensibly) -----------------------

        public bool Has(string key) => _snapshot != null && key != null && _snapshot.Values.ContainsKey(key);

        public bool TryGet(string key, out MajacloudConfigValue value)
        {
            value = null;
            return _snapshot != null && key != null && _snapshot.Values.TryGetValue(key, out value);
        }

        public string GetString(string key, string fallback = null) => TryGet(key, out var v) ? v.AsString() : fallback;

        public int GetInt(string key, int fallback = 0)
        {
            if (!TryGet(key, out var v)) return fallback;
            var n = v.AsNumber();
            if (!n.HasValue || n.Value > int.MaxValue || n.Value < int.MinValue) return fallback;
            return (int)Math.Round(n.Value);
        }

        public long GetLong(string key, long fallback = 0)
        {
            if (!TryGet(key, out var v)) return fallback;
            var n = v.AsNumber();
            return n.HasValue ? (long)Math.Round(n.Value) : fallback;
        }

        public float GetFloat(string key, float fallback = 0f)
        {
            if (!TryGet(key, out var v)) return fallback;
            var n = v.AsNumber();
            return n.HasValue ? (float)n.Value : fallback;
        }

        public double GetDouble(string key, double fallback = 0)
        {
            if (!TryGet(key, out var v)) return fallback;
            return v.AsNumber() ?? fallback;
        }

        public bool GetBool(string key, bool fallback = false)
        {
            if (!TryGet(key, out var v)) return fallback;
            return v.AsBool() ?? fallback;
        }

        public string GetJsonString(string key, string fallback = null) => TryGet(key, out var v) ? v.Raw : fallback;

        public T GetJson<T>(string key, T fallback = default) => TryGet(key, out var v) ? v.As(fallback) : fallback;

        // --- Coroutine variant -----------------------------------------------------------------------------

        public IEnumerator Fetch(Action<MajacloudResult<MajacloudConfigFetchResult>> callback, bool force = false) => MajacloudCoroutine.Run(FetchAsync(force), callback);
    }
}
