using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using UnityEngine;
#if DAVANCI_GA
using GameAnalyticsSDK;
#endif

namespace Davanci.Nexus
{
    // Tutorial and progress events (1.4.0): every game sends the same names and parameters, so
    // the Firebase definitions are registered once and the dashboard reads all games the same
    // way. Tutorial events are sent once per install; a completed (kind, index) is sent once and
    // replays of it (start, complete, fail) are skipped. Skipped calls still show in Recent.
    //
    //   DavanciNexus.TutorialStep(0, "tap_to_start");      DavanciNexus.TutorialStepDone(0);
    //   DavanciNexus.Progress.Start(5);                     DavanciNexus.Progress.Complete(5);
    //   DavanciNexus.Progress.Fail(3, "out_of_moves");      DavanciNexus.Progress.Start(2, "world", "jungle");
    public static partial class DavanciNexus
    {
        // Lowercase id parts stay within Firebase and GameAnalytics limits.
        const int MaxIdLength = 40;

        // Ids passed with TutorialStep, reused when TutorialStepDone / FtueStall omit theirs.
        static readonly Dictionary<int, string> _stepIds = new Dictionary<int, string>();

        enum Stage { Start, Complete, Fail }

        // ---- Tutorial ------------------------------------------------------------------

        public static void TutorialBegin()
        {
            if (!NexusEventGuards.MarkOnce("t.begin")) { RecordSkipped("tutorial_begin", null); return; }
            GaDesign("tutorial:begin");
            GaProgression(Stage.Start, "tutorial");
            SendFunnel("tutorial_begin", null);
        }

        // step is 0-based; id is a stable name for the step (e.g. "buy_ride").
        public static void TutorialStep(int step, string id)
        {
            id = EventId(id, StepPart(step));
            _stepIds[step] = id;
            var parameters = TutorialParams(step, id);
            if (!NexusEventGuards.MarkStep(done: false, step)) { RecordSkipped("tutorial_step", parameters); return; }

            GaProgression(Stage.Start, "tutorial", StepPart(step));
            GaDesign($"tutorial:{StepPart(step)}:{id}");
            SendFunnel("tutorial_step", parameters);
        }

        [Obsolete("Pass a stable step name: DavanciNexus.TutorialStep(step, \"buy_ride\").")]
        public static void TutorialStep(int step) => TutorialStep(step, StepPart(step));

        public static void TutorialStepDone(int step, string id = null)
        {
            id = EventId(id, StepId(step));
            var parameters = TutorialParams(step, id);
            if (!NexusEventGuards.MarkStep(done: true, step)) { RecordSkipped("tutorial_step_done", parameters); return; }

            GaProgression(Stage.Complete, "tutorial", StepPart(step));
            SendFunnel("tutorial_step_done", parameters);
        }

        public static void TutorialComplete()
        {
            var parameters = new (string key, object value)[] { ("t_step", NexusEventGuards.LastTutorialStep) };
            if (!NexusEventGuards.MarkOnce("t.complete")) { RecordSkipped("tutorial_complete", parameters); return; }

            GaProgression(Stage.Complete, "tutorial");
            GaDesign("tutorial:complete");
            AppsFlyerEvent("af_tutorial_completion", ("af_success", "true"), ("af_content_id", "tutorial"));
            SendFunnel("tutorial_complete", parameters, appsFlyer: true);
        }

        // The player sat on a tutorial step without acting for idleSeconds.
        public static void FtueStall(int step, string id, double idleSeconds)
        {
            id = EventId(id, StepId(step));
            int idle = (int)Math.Max(0, Math.Round(idleSeconds));
            GaDesign($"tutorial:stall:{StepPart(step)}", idle);
            SendFunnel("ftue_stall", new (string key, object value)[] { ("t_step", step), ("t_id", id), ("idle_s", idle) });
        }

        [Obsolete("Use DavanciNexus.Progress.Start(index, kind).")]
        public static void LevelStart(int level) => Progress.Start(level, "level");

        // Clears the once-per-install memory (debug menus, tutorial replay features).
        public static void ResetEventGuards()
        {
            NexusEventGuards.Reset();
            _stepIds.Clear();
            Progress.ClearTimers();
        }

        static (string key, object value)[] TutorialParams(int step, string id)
            => new (string key, object value)[] { ("t_step", step), ("t_id", id) };

        static string StepPart(int step) => "step_" + step.ToString("00", CultureInfo.InvariantCulture);

        static string StepId(int step) => _stepIds.TryGetValue(step, out var id) ? id : StepPart(step);

        // ---- Progress (levels, stages, milestones: anything ordered) -------------------

        public static class Progress
        {
            // Start time of the attempt in play, per (kind, index); unscaled real time.
            static readonly Dictionary<string, double> _startedAt = new Dictionary<string, double>();

            // index is the player's ordered step number (1, 2, 3, ...); kind labels what a step is.
            public static void Start(int index, string kind = "level", string id = null)
            {
                kind = Kind(kind);
                id = EventId(id, IndexText(index));
                if (IsReplay(kind, index))
                {
                    RecordSkipped("progress_start", Params(index, kind, id, NexusEventGuards.CurrentAttempt(kind, index)));
                    return;
                }

                int attempt = NexusEventGuards.StartAttempt(kind, index);
                _startedAt[TimerKey(kind, index)] = Now;
                GaProgression(Stage.Start, kind, IndexPart(index), GaIdPart(id, index));
                GaDesign($"progress:start:{kind}");
                SendFunnel("progress_start", Params(index, kind, id, attempt));
            }

            public static void Complete(int index, string kind = "level", string id = null)
            {
                kind = Kind(kind);
                id = EventId(id, IndexText(index));
                int attempt = NexusEventGuards.CurrentAttempt(kind, index);
                if (IsReplay(kind, index))
                {
                    RecordSkipped("progress_complete", Params(index, kind, id, attempt));
                    return;
                }

                int seconds = TakeSeconds(kind, index);
                NexusEventGuards.Completed(kind, index);
                GaProgression(Stage.Complete, kind, IndexPart(index), GaIdPart(id, index), seconds);
                GaDesign($"progress:complete:{kind}");
                AppsFlyerEvent("af_level_achieved", ("af_level", index), ("af_content_type", kind));
                SendFunnel("progress_complete", Params(index, kind, id, attempt, ("p_seconds", seconds)), appsFlyer: true);

                SetUserProperty("progress_index", index.ToString(CultureInfo.InvariantCulture));
                SetUserProperty("progress_kind", kind);
            }

            public static void Fail(int index, string cause = null, string kind = "level", string id = null)
            {
                kind = Kind(kind);
                id = EventId(id, IndexText(index));
                cause = EventId(cause, "unknown");
                int attempt = NexusEventGuards.CurrentAttempt(kind, index);
                if (IsReplay(kind, index))
                {
                    RecordSkipped("progress_fail", Params(index, kind, id, attempt, ("p_cause", cause)));
                    return;
                }

                int seconds = TakeSeconds(kind, index);
                GaProgression(Stage.Fail, kind, IndexPart(index), GaIdPart(id, index));
                GaDesign($"progress:fail:{kind}");
                SendFunnel("progress_fail",
                    Params(index, kind, id, attempt, ("p_seconds", seconds), ("p_cause", cause)));
            }

            internal static void ClearTimers() => _startedAt.Clear();

            static double Now => Time.realtimeSinceStartupAsDouble;

            static bool IsReplay(string kind, int index) => index <= NexusEventGuards.HighestCompleted(kind);

            // Seconds since the matching Start, then forgotten; 0 without one (e.g. after a restart).
            static int TakeSeconds(string kind, int index)
            {
                string key = TimerKey(kind, index);
                if (!_startedAt.TryGetValue(key, out double startedAt)) return 0;
                _startedAt.Remove(key);
                return (int)Math.Max(0, Math.Round(Now - startedAt));
            }

            static string TimerKey(string kind, int index) => kind + ":" + index.ToString(CultureInfo.InvariantCulture);

            // "tutorial" is the tutorial's own GA progression; a game kind of that name would mix into it.
            static string Kind(string kind)
            {
                kind = EventId(kind, "level");
                if (kind != "tutorial") return kind;
                Debug.LogWarning("[Nexus] Progress kind \"tutorial\" is reserved for the tutorial events; using \"tutorial_progress\".");
                return "tutorial_progress";
            }

            static string IndexText(int index) => index.ToString(CultureInfo.InvariantCulture);

            static string IndexPart(int index) => index.ToString("0000", CultureInfo.InvariantCulture);

            // The id is a third GA progression level only when the game named the step.
            static string GaIdPart(string id, int index) => id == IndexText(index) ? null : id;

            static (string key, object value)[] Params(int index, string kind, string id, int attempt,
                params (string key, object value)[] extra)
            {
                var list = new List<(string key, object value)>
                {
                    ("p_index", index), ("p_kind", kind), ("p_id", id), ("p_attempt", attempt),
                };
                list.AddRange(extra);
                return list.ToArray();
            }
        }

        // ---- Routing -------------------------------------------------------------------

        // Firebase gets the event exactly as named; GameAnalytics and AppsFlyer calls are made by the caller.
        static void SendFunnel(string name, (string key, object value)[] parameters, bool appsFlyer = false)
        {
            var providers = new List<string>();
#if DAVANCI_GA
            providers.Add("GA");
#endif
#if DAVANCI_FIREBASE
            ToFirebase(() => SendFirebase(name, parameters));
            providers.Add("Firebase");
#endif
#if DAVANCI_APPSFLYER
            if (appsFlyer) providers.Add("AppsFlyer");
#endif
            Record(name, parameters, providers);
        }

        static void RecordSkipped(string name, (string key, object value)[] parameters)
            => Record(name, parameters, null, trackAsLastEvent: false, skipped: true);

        static void GaDesign(string id, float? value = null)
        {
#if DAVANCI_GA
            ToGA(() =>
            {
                if (value.HasValue) GameAnalytics.NewDesignEvent(id, value.Value);
                else GameAnalytics.NewDesignEvent(id);
            });
#endif
        }

        static void GaProgression(Stage stage, string p1, string p2 = null, string p3 = null, int? score = null)
        {
#if DAVANCI_GA
            var status = stage == Stage.Start ? GAProgressionStatus.Start
                : stage == Stage.Complete ? GAProgressionStatus.Complete
                : GAProgressionStatus.Fail;
            ToGA(() =>
            {
                if (p2 == null)
                {
                    if (score.HasValue) GameAnalytics.NewProgressionEvent(status, p1, score.Value);
                    else GameAnalytics.NewProgressionEvent(status, p1);
                }
                else if (p3 == null)
                {
                    if (score.HasValue) GameAnalytics.NewProgressionEvent(status, p1, p2, score.Value);
                    else GameAnalytics.NewProgressionEvent(status, p1, p2);
                }
                else
                {
                    if (score.HasValue) GameAnalytics.NewProgressionEvent(status, p1, p2, p3, score.Value);
                    else GameAnalytics.NewProgressionEvent(status, p1, p2, p3);
                }
            });
#endif
        }

        // Marketing events only (tutorial completion, level achieved), never mirrored a second time.
        static void AppsFlyerEvent(string name, params (string key, object value)[] values)
        {
#if DAVANCI_APPSFLYER
            SendAppsFlyer(name, values);
#endif
        }

        // Lowercase letters, digits and underscores (anything else becomes _), at most 40 characters.
        static string EventId(string s, string fallback)
        {
            if (string.IsNullOrWhiteSpace(s)) return fallback;
            s = s.Trim().ToLowerInvariant();
            var sb = new StringBuilder(Math.Min(s.Length, MaxIdLength));
            foreach (char c in s)
            {
                if (sb.Length == MaxIdLength) break;
                sb.Append(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_' ? c : '_');
            }
            return sb.ToString();
        }
    }
}
