using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
#if DAVANCI_GA
using GameAnalyticsSDK;
#endif
#if DAVANCI_FIREBASE
using Firebase.Analytics;
#endif

namespace Davanci.Nexus
{
    // Analytics facade. Events are forwarded to every installed provider (GameAnalytics,
    // Firebase) and kept in an in-memory buffer for the editor Event Monitor. Provider
    // calls are guarded by scripting defines, so this compiles with no SDKs installed.
    // Tutorial and progress events live in DavanciNexus.Funnel.cs.
    public static partial class DavanciNexus
    {
        public struct Logged
        {
            public string Name;
            public string Parameters;
            public string Providers;
            public float Time;
            public DateTime Stamp;
            // Already sent once for this install, so it was not sent again (shown in the monitor only).
            public bool Skipped;
        }

        const int MaxBuffer = 200;
        static readonly List<Logged> _recent = new List<Logged>();

        public static IReadOnlyList<Logged> Recent => _recent;
        public static event Action<Logged> OnEvent;

        public static bool IsGameAnalyticsReady
        {
            get
            {
#if DAVANCI_GA
                return _gaReady;
#else
                return false;
#endif
            }
        }

        public static bool IsFirebaseReady
        {
            get
            {
#if DAVANCI_FIREBASE
                return _firebaseReady;
#else
                return false;
#endif
            }
        }

        public static void Clear() => _recent.Clear();

        // Providers initialize asynchronously; calls made before a provider is ready are
        // queued and flushed by Mark*Ready(). The in-memory buffer is never gated.
#if DAVANCI_GA
        static bool _gaReady;
        static readonly List<Action> _gaQueue = new List<Action>();

        static void ToGA(Action call)
        {
            if (_gaReady) call();
            else _gaQueue.Add(call);
        }
#endif

#if DAVANCI_FIREBASE
        static bool _firebaseReady;
        static readonly List<Action> _firebaseQueue = new List<Action>();

        static void ToFirebase(Action call)
        {
            if (_firebaseReady) call();
            else _firebaseQueue.Add(call);
        }
#endif

        public static void MarkGameAnalyticsReady()
        {
#if DAVANCI_GA
            if (_gaReady) return;
            _gaReady = true;
            Flush(_gaQueue);
#endif
        }

        // Must be called on the main thread.
        public static void MarkFirebaseReady()
        {
#if DAVANCI_FIREBASE
            if (_firebaseReady) return;
            _firebaseReady = true;
            Flush(_firebaseQueue);
#endif
        }

#if DAVANCI_GA || DAVANCI_FIREBASE
        static void Flush(List<Action> queue)
        {
            var pending = queue.ToArray();
            queue.Clear();
            foreach (var call in pending)
            {
                try { call(); }
                catch (Exception e) { Debug.LogWarning($"[Nexus] queued event failed: {e.Message}"); }
            }
        }
#endif

        // Ads
        public static void AdWatched(string placement) => Design("ad_watched", null, ("placement", placement));
        public static void AdFailed(string placement) => Design("ad_failed", null, ("placement", placement));

        // Local retention notification opened (day = 1, 3 or 7).
        public static void RetentionNotificationOpened(int day) =>
            Design("retention_notification_opened", day, ("day", day));

        // Game-scheduled local notification opened (id matches ScheduleNotification).
        public static void NotificationOpened(int id) =>
            Design("notification_opened", id, ("id", id));

        // ---- Local notifications (requires DAVANCI_NOTIFICATIONS + permission) -----------

        public static bool ScheduleNotification(int id, string title, string body, DateTime fireTimeLocal) =>
            NexusNotifications.ScheduleNotification(id, title, body, fireTimeLocal);

        public static bool ScheduleNotificationIn(int id, string title, string body, double delaySeconds) =>
            NexusNotifications.ScheduleNotificationIn(id, title, body, delaySeconds);

        public static void CancelNotification(int id) => NexusNotifications.CancelNotification(id);

        public static void CancelAllNotifications() => NexusNotifications.CancelAllNotifications();

        public static void RefreshRetentionNotifications() =>
            NexusNotifications.RefreshRetentionNotifications();

        // Impression-level ad revenue from mediation (MAX / LevelPlay OnAdRevenuePaid / ILRD).
        // Firebase: GA4 ad_impression. AppsFlyer: af_ad_revenue. GameAnalytics: ILRD via
        // NexusGameAnalyticsIlrd.Subscribe (SubscribeMaxImpressions / SubscribeIronSourceImpressions).
        public static void AdRevenue(string adPlatform, string adNetwork, string adFormat,
                                     double revenue, string adUnit = null, string currency = "USD",
                                     string placement = null)
        {
            adPlatform ??= "unknown"; adNetwork ??= "unknown"; adFormat ??= "unknown"; adUnit ??= "unknown";
            placement ??= "unknown";
            var providers = new List<string>();
#if DAVANCI_FIREBASE
            // ad_placement = the in-game trigger the game passed to ShowRewardedAd (e.g.
            // "MinigameMultiplier"). Register it as a GA4 custom dimension to break revenue
            // down by placement. Rewarded/interstitial only; banners report "unknown".
            ToFirebase(() => SendFirebase("ad_impression", new (string key, object value)[]
            {
                ("value", revenue),
                ("currency", currency),
                ("ad_platform", adPlatform),
                ("ad_source", adNetwork),
                ("ad_format", adFormat),
                ("ad_unit_name", adUnit),
                ("ad_placement", placement),
            }));
            providers.Add("Firebase");
#endif
#if DAVANCI_GA
            providers.Add("GA(ILRD)");
#endif
#if DAVANCI_APPSFLYER
            ToAppsFlyerRevenue(adNetwork, adFormat, revenue, currency);
            providers.Add("AppsFlyer");
#endif
            Record("ad_revenue", new (string key, object value)[]
            {
                ("platform", adPlatform), ("network", adNetwork), ("format", adFormat),
                ("revenue", revenue), ("placement", placement),
            }, providers, trackAsLastEvent: false);
        }

#if DAVANCI_APPSFLYER
        // Ad revenue as an AppsFlyer event (af_revenue), so the data reaches AppsFlyer for ROAS.
        // The dedicated AppsFlyer Ad Revenue connector isn't compatible with the bundled AppsFlyer
        // 6.18 SDK (the connector tops out at 6.14.3), so this is the supported path for now.
        static void ToAppsFlyerRevenue(string adNetwork, string adFormat, double revenue, string currency)
        {
            var values = new Dictionary<string, string>
            {
                { "af_revenue", revenue.ToString(System.Globalization.CultureInfo.InvariantCulture) },
                { "af_currency", currency },
                { "ad_network", adNetwork },
                { "ad_format", adFormat },
            };
            try { AppsFlyerSDK.AppsFlyer.sendEvent("af_ad_revenue", values); }
            catch (Exception e) { Debug.LogWarning("[Nexus] AppsFlyer ad revenue failed: " + e.Message); }
        }

        // Generic AppsFlyer rich event — used for af_purchase and (when enabled) mirrored custom events.
        static void SendAppsFlyer(string eventName, (string key, object value)[] parameters)
        {
            var values = new Dictionary<string, string>();
            if (parameters != null)
                foreach (var (k, v) in parameters)
                    values[k] = v == null
                        ? ""
                        : Convert.ToString(v, System.Globalization.CultureInfo.InvariantCulture);
            try { AppsFlyerSDK.AppsFlyer.sendEvent(eventName, values); }
            catch (Exception e) { Debug.LogWarning("[Nexus] AppsFlyer event failed: " + e.Message); }
        }

        // Cached so the per-event toggle check doesn't re-load the config asset each call.
        static NexusConfig _afCfg;
        static bool _afCfgLoaded;
        static bool AppsFlyerMirrorEnabled()
        {
            if (!_afCfgLoaded) { _afCfg = NexusConfig.Load(); _afCfgLoaded = true; }
            return _afCfg != null && _afCfg.appsFlyerMirrorEvents;
        }
#endif

        // Economy, currency flows go through GameAnalytics resource events.
        public static void CurrencyEarned(string currency, double amount, string source, string itemId = null)
            => Resource(ResourceFlow.Source, currency, amount, source, itemId ?? source,
                        "currency_earned", ("currency", currency), ("amount", amount), ("source", source));

        public static void CurrencySpent(string currency, double amount, string sink, string itemId = null)
            => Resource(ResourceFlow.Sink, currency, amount, sink, itemId ?? sink,
                        "currency_spent", ("currency", currency), ("amount", amount), ("sink", sink));

        // Progression, `type` is the kind of thing, `id` the specific one.
        public static void Unlocked(string type, string id)
            => Design("unlocked", null, GaName("unlocked", type),
                      ("type", type), ("id", id));

        public static void Acquired(string type, string id, int count = 1)
            => Design("acquired", count, GaName("acquired", type),
                      ("type", type), ("id", id), ("count", count));

        public static void Upgraded(string type, string id, int level)
            => Design("upgraded", level, GaName("upgraded", type),
                      ("type", type), ("id", id), ("level", level));

        public static void Milestone(string id, double value = 0)
            => Design("milestone", (float)value, GaName("milestone", id),
                      ("id", id), ("value", value));

        public static void Reset(string type, int newLevel, double reward = 0)
            => Design("reset", newLevel, GaName("reset", type),
                      ("type", type), ("level", newLevel), ("reward", reward));

        // Idle / boosts
        public static void OfflineEarningsCollected(double amount, double offlineSeconds)
            => Design("offline_earnings_collected", (float)amount, null,
                      ("amount", amount), ("offline_seconds", offlineSeconds));

        public static void BoostActivated(string boostId, double multiplier, double durationSeconds)
            => Design("boost_activated", (float)multiplier, GaName("boost_activated", boostId),
                      ("boost_id", boostId), ("multiplier", multiplier), ("duration_seconds", durationSeconds));

        // Player state, Checkpoint records where the player is for churn segmentation.
        public static void Checkpoint(string id)
        {
            LastCheckpoint = id;
            SetUserProperty("last_checkpoint", id);
            Design("checkpoint", null, ("id", id));
        }

        public static string LastCheckpoint { get; private set; }

        // Firebase user property, durable cross-session segmentation. Firebase-only.
        public static void SetUserProperty(string name, string value)
        {
            value = FitUserProperty(value);
            var providers = new List<string>();
#if DAVANCI_FIREBASE
            ToFirebase(() => FirebaseAnalytics.SetUserProperty(name, value));
            providers.Add("Firebase");
#endif
            Record($"user_property:{name}", new[] { (name, (object)value) }, providers, trackAsLastEvent: false);
        }

        // Monetization — IAP revenue to every analytics provider that is installed.
        public static void Purchase(string productId, double amount, string currency, string itemType = "iap")
        {
            if (string.IsNullOrEmpty(currency)) currency = "USD";
            var providers = new List<string>();

#if DAVANCI_APPSFLYER
            // af_purchase — standard AppsFlyer ROAS event (always sent, not behind the mirror toggle).
            SendAppsFlyer("af_purchase", new (string key, object value)[]
            {
                ("af_revenue", amount),
                ("af_currency", currency),
                ("af_content_id", productId),
                ("af_content_type", itemType),
                ("af_quantity", 1),
            });
            providers.Add("AppsFlyer");
#endif

#if DAVANCI_GA
            // GameAnalytics business event (proper IAP / revenue tracking — not a design event).
            int cents = amount > 0 ? (int)Math.Round(amount * 100.0) : 0;
            ToGA(() => GameAnalytics.NewBusinessEvent(currency, cents, itemType, productId, "nexus_iap"));
            providers.Add("GA");
#endif

#if DAVANCI_FIREBASE
            // GA4-standard purchase event (value + currency + item id).
            ToFirebase(() =>
            {
                FirebaseAnalytics.LogEvent(FirebaseAnalytics.EventPurchase, new Parameter[]
                {
                    new Parameter(FirebaseAnalytics.ParameterValue, amount),
                    new Parameter(FirebaseAnalytics.ParameterCurrency, currency),
                    new Parameter("item_id", productId),
                    new Parameter("item_category", itemType),
                });
            });
            providers.Add("Firebase");
#endif

            Record("purchase", new (string key, object value)[]
            {
                ("product_id", productId), ("amount", amount), ("currency", currency), ("item_type", itemType),
            }, providers, trackAsLastEvent: false);
        }

        public static void Custom(string name, params (string key, object value)[] parameters)
            => Design(name, FirstNumeric(parameters), null, parameters);

        // Cloud save — syncs the player's save to their own iCloud (iOS). Save/Load take a
        // string (your JSON); onDone(true) means it reached the cloud, load gives null when
        // there's no save yet. See NexusCloudSave for bytes / advanced result.
        public static void Save(string text, Action<bool> onDone = null) => NexusCloudSave.Save(text, onDone);
        public static void Load(Action<string> onLoaded) => NexusCloudSave.Load(onLoaded);
        public static bool IsCloudAvailable => NexusCloudSave.IsCloudAvailable;

        // Auto-sync hookup (with Cloud Save → Auto-sync on): give a save-to-string and
        // load-from-string function once and Nexus loads on launch / saves on pause+quit.
        //   DavanciNexus.BindCloudSave(() => MySave.ToJson(), json => MySave.FromJson(json));
        public static void BindCloudSave(Func<string> save, Action<string> load)
            => NexusCloudSaveRunner.Bind(save, load);

        // Remote config — pass the value you'd ship if the network is down.
        //   int cooldown = DavanciNexus.GetInt("interstitial_cooldown", 45);
        public static string GetString(string key, string fallback = "") => NexusRemoteConfig.GetString(key, fallback);
        public static bool GetBool(string key, bool fallback = false) => NexusRemoteConfig.GetBool(key, fallback);
        public static int GetInt(string key, int fallback = 0) => NexusRemoteConfig.GetInt(key, fallback);
        public static float GetFloat(string key, float fallback = 0f) => NexusRemoteConfig.GetFloat(key, fallback);
        public static string GetVariant(string experimentKey) => NexusRemoteConfig.GetVariant(experimentKey);
        public static bool IsRemoteConfigReady => NexusRemoteConfig.IsReady;

        // In-app purchases — cross-platform (App Store + Google Play) via Unity IAP. With IAP
        // enabled and auto-init on, Nexus initializes at launch; otherwise call InitializeIAP.
        // A verified purchase is logged automatically through Purchase() above.
        //   DavanciNexus.Buy("coins_100", r => { if (r.Success) Grant(); });
        //   if (DavanciNexus.OwnsProduct("remove_ads")) HideAds();
        public static void InitializeIAP(Action<bool> onReady = null) => NexusIAP.Initialize(onReady);
        public static bool IsIapReady => NexusIAP.IsReady;
        /// <summary>Fires when IAP init completes (true = store ready, false = failed).</summary>
        public static event Action<bool> IapInitialized
        {
            add => NexusIAP.Initialized += value;
            remove => NexusIAP.Initialized -= value;
        }
        public static void Buy(string productId, Action<NexusPurchaseResult> onResult) => NexusIAP.Buy(productId, onResult);
        public static void RestorePurchases(Action<bool> onDone = null) => NexusIAP.Restore(onDone);
        public static bool OwnsProduct(string productId) => NexusIAP.OwnsProduct(productId);
        /// <summary>Localized store price. Wait for <see cref="IsIapReady"/> or <see cref="IapInitialized"/> before calling.</summary>
        public static NexusIapPrice GetPrice(string productId) => NexusIAP.GetPrice(productId);
        /// <summary>Store-formatted price string (e.g. "$0.99"). Shorthand for <see cref="GetPrice"/>.Display.</summary>
        public static string PriceString(string productId) => NexusIAP.PriceString(productId);

        // Controls whether a design event is also mirrored to AppsFlyer.
        //   WhenEnabled = mirror only if appsFlyerMirrorEvents is on (general custom events)
        //   Off         = never mirror here (the caller already sent a dedicated AppsFlyer event, e.g. purchase)
        enum AfMirror { WhenEnabled, Off }

        // Design events
        static void Design(string name, params (string key, object value)[] parameters)
            => Design(name, FirstNumeric(parameters), null, AfMirror.WhenEnabled, parameters);

        static void Design(string name, float? gaValue, params (string key, object value)[] parameters)
            => Design(name, gaValue, null, AfMirror.WhenEnabled, parameters);

        static void Design(string name, float? gaValue, string gaEventName,
                           params (string key, object value)[] parameters)
            => Design(name, gaValue, gaEventName, AfMirror.WhenEnabled, parameters);

        static void Design(string name, float? gaValue, string gaEventName, AfMirror af,
                           params (string key, object value)[] parameters)
        {
            var providers = new List<string>();

#if DAVANCI_GA
            string gaName = string.IsNullOrEmpty(gaEventName) ? name : gaEventName;
            float? gaVal = gaValue;
            ToGA(() =>
            {
                if (gaVal.HasValue) GameAnalytics.NewDesignEvent(gaName, gaVal.Value);
                else GameAnalytics.NewDesignEvent(gaName);
            });
            providers.Add("GA");
#endif
#if DAVANCI_FIREBASE
            ToFirebase(() => SendFirebase(name, parameters));
            providers.Add("Firebase");
#endif
#if DAVANCI_APPSFLYER
            if (af == AfMirror.WhenEnabled && AppsFlyerMirrorEnabled())
            {
                SendAppsFlyer(name, parameters);
                providers.Add("AppsFlyer");
            }
#endif
            Record(name, parameters, providers);
        }

        enum ResourceFlow { Source, Sink }

        static void Resource(ResourceFlow flow, string currency, double amount,
                             string itemType, string itemId, string fbName,
                             params (string key, object value)[] fbParams)
        {
            var providers = new List<string>();

#if DAVANCI_GA
            // GA requires currency + itemType registered in its settings or it drops the event.
            var gaFlow = flow == ResourceFlow.Source
                ? GAResourceFlowType.Source
                : GAResourceFlowType.Sink;
            float gaAmount = Convert.ToSingle(amount);
            string gaItemType = Sanitize(itemType);
            string gaItemId = Sanitize(itemId);
            ToGA(() => GameAnalytics.NewResourceEvent(gaFlow, currency, gaAmount, gaItemType, gaItemId));
            providers.Add("GA");
#endif
#if DAVANCI_FIREBASE
            ToFirebase(() => SendFirebase(fbName, fbParams));
            providers.Add("Firebase");
#endif
            Record(fbName, fbParams, providers);
        }

        static void Record(string name, (string key, object value)[] parameters, List<string> providers,
                           bool trackAsLastEvent = true, bool skipped = false)
        {
#if DAVANCI_FIREBASE
            if (trackAsLastEvent && !skipped)
            {
                string lastEvent = FitUserProperty(name);
                ToFirebase(() => FirebaseAnalytics.SetUserProperty("last_event", lastEvent));
            }
#endif

            var logged = new Logged
            {
                Name = name,
                Parameters = FormatParams(parameters),
                Providers = skipped ? "(skipped: already sent)"
                    : providers == null || providers.Count == 0 ? "(none)" : string.Join("+", providers),
                Time = Application.isPlaying ? Time.realtimeSinceStartup : 0f,
                Stamp = DateTime.Now,
                Skipped = skipped,
            };

            _recent.Add(logged);
            if (_recent.Count > MaxBuffer)
                _recent.RemoveAt(0);

            OnEvent?.Invoke(logged);
            Debug.Log($"[Nexus] {name} {logged.Parameters} → {logged.Providers}");
        }

        static float? FirstNumeric((string key, object value)[] parameters)
        {
            if (parameters == null) return null;
            foreach (var p in parameters)
                if (p.value is int or long or float or double)
                    return Convert.ToSingle(p.value);
            return null;
        }

        // Firebase drops a user property whose value is longer than 36 characters.
        const int MaxUserPropertyLength = 36;

        static string FitUserProperty(string value)
            => value != null && value.Length > MaxUserPropertyLength ? value.Substring(0, MaxUserPropertyLength) : value;

        static string GaName(string category, string id)
            => string.IsNullOrEmpty(id) ? category : $"{category}:{Sanitize(id)}";

        static string Sanitize(string s)
        {
            if (string.IsNullOrEmpty(s)) return "unknown";
            var sb = new StringBuilder(s.Length);
            foreach (char c in s)
                sb.Append(char.IsLetterOrDigit(c) || c is '_' or '-' or '.' or ' ' ? c : '_');
            return sb.ToString();
        }

#if DAVANCI_FIREBASE
        static void SendFirebase(string name, (string key, object value)[] parameters)
        {
            if (parameters == null || parameters.Length == 0)
            {
                FirebaseAnalytics.LogEvent(name);
                return;
            }

            var fbParams = new Parameter[parameters.Length];
            for (int i = 0; i < parameters.Length; i++)
            {
                var (key, val) = parameters[i];
                switch (val)
                {
                    case int or long:
                        fbParams[i] = new Parameter(key, Convert.ToInt64(val));
                        break;
                    case float or double:
                        fbParams[i] = new Parameter(key, Convert.ToDouble(val));
                        break;
                    default:
                        fbParams[i] = new Parameter(key, val?.ToString() ?? "");
                        break;
                }
            }
            FirebaseAnalytics.LogEvent(name, fbParams);
        }
#endif

        static string FormatParams((string key, object value)[] parameters)
        {
            if (parameters == null || parameters.Length == 0) return "";
            var sb = new StringBuilder("{ ");
            for (int i = 0; i < parameters.Length; i++)
            {
                if (i > 0) sb.Append(", ");
                sb.Append(parameters[i].key).Append(": ").Append(parameters[i].value);
            }
            sb.Append(" }");
            return sb.ToString();
        }
    }
}
