// ============================================================================
// Platform Configuration
// ============================================================================
// Comment out these defines to disable iOS or Android support
#if UNITY_IOS && !UNITY_EDITOR
#define CONTEXTSDK_IOS
#endif

#if UNITY_ANDROID && !UNITY_EDITOR
#define CONTEXTSDK_ANDROID
#endif
// ============================================================================

using System.Collections.Generic;
using System.Runtime.InteropServices;
using AOT;
using UnityEngine;
using System;

#if UNITY_ANDROID
using UnityEngine.Android;
#endif

#nullable enable

public static class ContextSDKBinding
{
  #region Configuration

  /// <summary>
  /// Enable this to see debug logs from ContextSDK binding
  /// </summary>
  public static bool EnableDebugLogs = false;

#if CONTEXTSDK_ANDROID
    // Change this to "com.contextsdk" if using the core distribution instead of adtech
    private const string ANDROID_PACKAGE_NAME = "com.contextsdk.adtech";
#endif

  #endregion

  #region Debug Logging

  private static void DebugLog(string message)
  {
    if (EnableDebugLogs)
    {
      Debug.Log($"[ContextSDK] {message}");
    }
  }

  #endregion

  #region Android Helpers

#if CONTEXTSDK_ANDROID
    private static bool SafeAndroidCall(System.Action action, string errorMessage)
    {
        try
        {
            action();
            return true;
        }
        catch (Exception e)
        {
            Debug.LogError($"ContextSDK: {errorMessage} - {e.Message}");
            return false;
        }
    }

    private static T SafeAndroidCall<T>(System.Func<T> func, string errorMessage, T defaultValue)
    {
        try
        {
            return func();
        }
        catch (Exception e)
        {
            Debug.LogError($"ContextSDK: {errorMessage} - {e.Message}");
            return defaultValue;
        }
    }
#endif

  #endregion

  #region iOS P/Invoke Declarations

#if CONTEXTSDK_IOS
    [DllImport("__Internal")]
    private static extern void contextSDK_setupContextManagerWithAPIBackend(string licenseKey);

    [DllImport("__Internal")]
    private static extern void contextSDK_setControlMode(bool enabled);

    [DllImport("__Internal")]
    private static extern bool contextSDK_setGlobalCustomSignalInt(string id, int value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_setGlobalCustomSignalBool(string id, bool value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_setGlobalCustomSignalFloat(string id, float value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_setGlobalCustomSignalString(string id, string value);

    [DllImport("__Internal")]
    private static extern int contextSDK_getNextContextID();
#endif

#if CONTEXTSDK_ANDROID
        private static AndroidJavaClass? contextSDKClass;
        private static AndroidJavaClass ContextSDKClass
        {
            get
            {
                if (contextSDKClass == null)
                {
                    contextSDKClass = new AndroidJavaClass($"{ANDROID_PACKAGE_NAME}.ContextSDK");
                }
                return contextSDKClass;
            }
        }
#endif

  private static Dictionary<int, ContextDelegate> pendingContextDelegates = new Dictionary<int, ContextDelegate>();
  private delegate void InternalContextDelegate(int contextID);
  public delegate void ContextDelegate(Context context);

#if CONTEXTSDK_ANDROID
    private class ContextCallback : AndroidJavaProxy
    {
        private readonly ContextDelegate callback;

        public ContextCallback(ContextDelegate callback) : base("kotlin.jvm.functions.Function1")
        {
            this.callback = callback;
        }

        public AndroidJavaObject? invoke(AndroidJavaObject contextObj)
        {
            Context context = new Context(-1, contextObj);
            // Marshal the callback to Unity's main thread
            UnityMainThreadDispatcher.Enqueue(() => callback(context));
            return null;
        }
    }

    private class UnityMainThreadDispatcher : MonoBehaviour
    {
        private static readonly System.Collections.Queue actions = new System.Collections.Queue();
        private static UnityMainThreadDispatcher? instance = null;

        public static void Initialize()
        {
            if (instance == null)
            {
                var go = new GameObject("ContextSDKMainThreadDispatcher");
                instance = go.AddComponent<UnityMainThreadDispatcher>();
                DontDestroyOnLoad(go);
            }
        }

        public static void Enqueue(System.Action action)
        {
            lock (actions)
            {
                actions.Enqueue(action);
            }
        }

        void Update()
        {
            lock (actions)
            {
                while (actions.Count > 0)
                {
                    var action = (System.Action)actions.Dequeue();
                    action?.Invoke();
                }
            }
        }
    }
#endif

#if UNITY_IOS
    [DllImport("__Internal")]
    private static extern int contextSDK_instantContext(string flowName, int customSignalsID, int duration);
    [DllImport("__Internal")]
    private static extern void contextSDK_calibrate(int contextID, int customSignalsID, string flowName, int maxDelay, InternalContextDelegate callback);
    [DllImport("__Internal")]
    private static extern void contextSDK_fetchContext_string(int contextID, int customSignalsID, string flowName, int duration, InternalContextDelegate callback);
    [DllImport("__Internal")]
    private static extern void contextSDK_optimize_string(int contextID, int customSignalsID, string flowName, int maxDelay, InternalContextDelegate callback);
    [DllImport("__Internal")]
    private static extern int contextSDK_recentContext(string flowName);

    [DllImport("__Internal")]
    private static extern int contextSDK_trackEvent(string eventName, int customSignalsID);
    [DllImport("__Internal")]
    private static extern int contextSDK_trackPageView(string pageName, int customSignalsID);
    [DllImport("__Internal")]
    private static extern int contextSDK_trackUserAction(string userActionName, int customSignalsID);

    [DllImport("__Internal")]
    private static extern bool contextSDK_context_shouldUpsell(int contextID);
    [DllImport("__Internal")]
    private static extern string contextSDK_context_validate(int contextID);

    [DllImport("__Internal")]
    private static extern bool contextSDK_context_log(int contextID, int outcome);
    [DllImport("__Internal")]
    private static extern bool contextSDK_context_logIfNotLoggedYet(int contextID, int outcome);

    [DllImport("__Internal")]
    private static extern bool contextSDK_context_appendOutcomeMetadataInt(int contextID, string id, int value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_context_appendOutcomeMetadataBool(int contextID, string id, bool value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_context_appendOutcomeMetadataFloat(int contextID, string id, float value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_context_appendOutcomeMetadataString(int contextID, string id, string value);

    [DllImport("__Internal")]
    private static extern bool contextSDK_releaseContext(int contextID);

    [DllImport("__Internal")]
    private static extern bool contextSDK_context_addCustomSignalInt(int contextID, string id, int value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_context_addCustomSignalBool(int contextID, string id, bool value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_context_addCustomSignalFloat(int contextID, string id, float value);
    [DllImport("__Internal")]
    private static extern bool contextSDK_context_addCustomSignalString(int contextID, string id, string value);

    [DllImport("__Internal")]
    private static extern bool contextSDK_releaseCustomSignals(int contextID);
#endif

  #endregion

  #region Setup Methods

  public static void SetupWithAPIBackend(string licenseKey)
  {
#if CONTEXTSDK_IOS
        contextSDK_setupContextManagerWithAPIBackend(licenseKey);
#endif
  }

  public static void SetupWithAPIKey(string apiKey)
  {
#if CONTEXTSDK_ANDROID
        // Initialize the main thread dispatcher first
        UnityMainThreadDispatcher.Initialize();

        DebugLog($"Setting up with API key");
        SafeAndroidCall(() => {
            var activity = AndroidApplication.currentActivity;
            using (AndroidJavaObject application = activity.Call<AndroidJavaObject>("getApplication"))
            {
                ContextSDKClass.CallStatic("setupWithApiKeyFromUnity", application, apiKey);
            }
        }, "Failed to setup");
#endif
  }

  #endregion

  #region Lifecycle Methods

  /// <summary>
  /// Call this when your application resumes from the background.
  /// This should be called from OnApplicationPause(false) in your MonoBehaviour.
  /// </summary>
  public static void OnApplicationResume()
  {
#if CONTEXTSDK_ANDROID
        DebugLog("OnApplicationResume called");
        SafeAndroidCall(() => {
            var activity = AndroidApplication.currentActivity;
            ContextSDKClass.CallStatic("startWithPlainActivity", activity);
        }, "Failed to start with activity");
#endif
  }

  /// <summary>
  /// Call this when your application pauses (goes to background).
  /// This should be called from OnApplicationPause(true) in your MonoBehaviour.
  /// </summary>
  public static void OnApplicationPause()
  {
#if CONTEXTSDK_ANDROID
        DebugLog("OnApplicationPause called");
        SafeAndroidCall(() => {
            var activity = AndroidApplication.currentActivity;
            ContextSDKClass.CallStatic("stopWithPlainActivity", activity);
        }, "Failed to stop with activity");
#endif
  }

  /// <summary>
  /// Enable or disable control mode. When enabled the SDK will always return true for <see cref="Context.shouldUpsell"/>
  /// and skip all internal decision making.
  /// </summary>
  /// <param name="enabled">Whether control mode should be enabled.</param>
  public static void SetControlMode(bool enabled)
  {
#if CONTEXTSDK_IOS
        contextSDK_setControlMode(enabled);
#endif
  }

  #endregion

  #region Global Custom Signals

  /// <summary>
  ///  Set custom signals that will be used for all ContextSDK events on this instance. We recommend using this to provide generic information that's applicable to all calls, like any AB test information, or other data that may be relevant to calculate the likelihood of an event. Please be sure to not include any PII or other potentially sensitive information. You can overwrite values by using the same id again.
  /// </summary>
  /// <param name="id">The unique id of the custom signal. This id will be used to identify the custom signal in the ContextSDK backend.</param>
  /// <param name="value">The value of the custom signal</param>
  public static void SetGlobalCustomSignal(string id, int value)
  {
#if CONTEXTSDK_IOS
        contextSDK_setGlobalCustomSignalInt(id, value);
#elif CONTEXTSDK_ANDROID
        DebugLog($"SetGlobalCustomSignal: {id} = {value}");
        SafeAndroidCall(() => {
            AndroidJavaObject globalSignals = ContextSDKClass.CallStatic<AndroidJavaObject>("getGlobalCustomSignals");
            globalSignals.Call("set", id, value);
        }, "Failed to set global custom signal");
#endif
  }

  /// <summary>
  ///  Set custom signals that will be used for all ContextSDK events on this instance. We recommend using this to provide generic information that's applicable to all calls, like any AB test information, or other data that may be relevant to calculate the likelihood of an event. Please be sure to not include any PII or other potentially sensitive information. You can overwrite values by using the same id again.
  /// </summary>
  /// <param name="id">The unique id of the custom signal. This id will be used to identify the custom signal in the ContextSDK backend.</param>
  /// <param name="value">The value of the custom signal</param>
  public static void SetGlobalCustomSignal(string id, float value)
  {
#if CONTEXTSDK_IOS
        contextSDK_setGlobalCustomSignalFloat(id, value);
#elif CONTEXTSDK_ANDROID
        DebugLog($"SetGlobalCustomSignal: {id} = {value}");
        SafeAndroidCall(() => {
            AndroidJavaObject globalSignals = ContextSDKClass.CallStatic<AndroidJavaObject>("getGlobalCustomSignals");
            globalSignals.Call("set", id, value);
        }, "Failed to set global custom signal");
#endif
  }

  /// <summary>
  ///  Set custom signals that will be used for all ContextSDK events on this instance. We recommend using this to provide generic information that's applicable to all calls, like any AB test information, or other data that may be relevant to calculate the likelihood of an event. Please be sure to not include any PII or other potentially sensitive information. You can overwrite values by using the same id again.
  /// </summary>
  /// <param name="id">The unique id of the custom signal. This id will be used to identify the custom signal in the ContextSDK backend.</param>
  /// <param name="value">The value of the custom signal</param>
  public static void SetGlobalCustomSignal(string id, bool value)
  {
#if CONTEXTSDK_IOS
        contextSDK_setGlobalCustomSignalBool(id, value);
#elif CONTEXTSDK_ANDROID
        DebugLog($"SetGlobalCustomSignal: {id} = {value}");
        SafeAndroidCall(() => {
            AndroidJavaObject globalSignals = ContextSDKClass.CallStatic<AndroidJavaObject>("getGlobalCustomSignals");
            globalSignals.Call("set", id, value);
        }, "Failed to set global custom signal");
#endif
  }

  /// <summary>
  ///  Set custom signals that will be used for all ContextSDK events on this instance. We recommend using this to provide generic information that's applicable to all calls, like any AB test information, or other data that may be relevant to calculate the likelihood of an event. Please be sure to not include any PII or other potentially sensitive information. You can overwrite values by using the same id again.
  /// </summary>
  /// <param name="id">The unique id of the custom signal. This id will be used to identify the custom signal in the ContextSDK backend.</param>
  /// <param name="value">The value of the custom signal</param>
  public static void SetGlobalCustomSignal(string id, string value)
  {
#if CONTEXTSDK_IOS
        contextSDK_setGlobalCustomSignalString(id, value);
#elif CONTEXTSDK_ANDROID
        DebugLog($"SetGlobalCustomSignal: {id} = {value}");
        SafeAndroidCall(() => {
            AndroidJavaObject globalSignals = ContextSDKClass.CallStatic<AndroidJavaObject>("getGlobalCustomSignals");
            globalSignals.Call("set", id, value);
        }, "Failed to set global custom signal");
#endif
  }

  #endregion

  #region Context Methods

  /// <summary>
  /// Get the current context synchronously. The signal may not include all the information, if the duration wasn't reached.
  /// Be sure that the majority of the times when calling this method, the SDK has already had enough time to reach the duration you've set.
  /// </summary>
  public static Context InstantContext(string flowName, CustomSignals? customSignals = null, int duration = 3)
  {

#if CONTEXTSDK_IOS
        int customSignalsID = customSignals?.contextID ?? -1;
        int contextID = contextSDK_instantContext(flowName, customSignalsID, duration);
        return new Context(contextID);
#elif CONTEXTSDK_ANDROID
        DebugLog($"InstantContext: {flowName}, duration: {duration}");
        return SafeAndroidCall(() => {
            AndroidJavaObject customSignalsObj = customSignals?.ToAndroidCustomSignals()
                ?? new AndroidJavaObject($"{ANDROID_PACKAGE_NAME}.signals.CustomSignals");
            AndroidJavaObject contextObj = ContextSDKClass.CallStatic<AndroidJavaObject>("instantContext", flowName, duration, customSignalsObj);
            return new Context(-1, contextObj);
        }, "Failed to get InstantContext", new Context(-1));
#else
    return new Context(-1);
#endif
  }

  public static void Calibrate(string flowName, ContextDelegate callback, CustomSignals? customSignals = null, int maxDelay = 3)
  {

#if CONTEXTSDK_IOS
        int customSignalsID = customSignals?.contextID ?? -1;
        int currentContextID = contextSDK_getNextContextID();
        pendingContextDelegates[currentContextID] = callback;
        contextSDK_calibrate(currentContextID, customSignalsID, flowName, maxDelay, HandleContext);
#elif CONTEXTSDK_ANDROID
        DebugLog($"Calibrate: {flowName}, maxDelay: {maxDelay}");
        bool success = SafeAndroidCall(() => {
            AndroidJavaObject customSignalsObj = customSignals?.ToAndroidCustomSignals()
                ?? new AndroidJavaObject($"{ANDROID_PACKAGE_NAME}.signals.CustomSignals");
            ContextCallback contextCallback = new ContextCallback(callback);
            ContextSDKClass.CallStatic("calibrate", flowName, maxDelay, customSignalsObj, contextCallback);
        }, "Failed to calibrate");

        if (!success)
        {
            callback(new Context(-1));
        }
#else
    // On non iOS always instantly callback.
    callback(new Context(-1));
#endif
  }

  public static void FetchContext(string flowName, ContextDelegate callback, CustomSignals? customSignals = null, int duration = 3)
  {

#if CONTEXTSDK_IOS
        int customSignalsID = customSignals?.contextID ?? -1;
        int currentContextID = contextSDK_getNextContextID();
        pendingContextDelegates[currentContextID] = callback;
        contextSDK_fetchContext_string(currentContextID, customSignalsID, flowName, duration, HandleContext);
#elif CONTEXTSDK_ANDROID
        DebugLog($"FetchContext: {flowName}, duration: {duration}");
        bool success = SafeAndroidCall(() => {
            AndroidJavaObject customSignalsObj = customSignals?.ToAndroidCustomSignals()
                ?? new AndroidJavaObject($"{ANDROID_PACKAGE_NAME}.signals.CustomSignals");
            ContextCallback contextCallback = new ContextCallback(callback);
            ContextSDKClass.CallStatic("fetchContext", flowName, duration, customSignalsObj, contextCallback);
        }, "Failed to fetch context");

        if (!success)
        {
            callback(new Context(-1));
        }
#else
    // On non iOS always instantly callback.
    callback(new Context(-1));
#endif
  }

  public static void Optimize(string flowName, ContextDelegate callback, CustomSignals? customSignals = null, int? maxDelay = null)
  {

#if CONTEXTSDK_IOS
        int customSignalsID = customSignals?.contextID ?? -1;
        int currentContextID = contextSDK_getNextContextID();
        pendingContextDelegates[currentContextID] = callback;
        contextSDK_optimize_string(currentContextID, customSignalsID, flowName, maxDelay ?? -1000, HandleContext);
#else
    // On non iOS always instantly callback.
    callback(new Context(-1));
#endif
  }

  [MonoPInvokeCallback(typeof(InternalContextDelegate))]
  private static void HandleContext(int contextID)
  {
    ContextDelegate callback = pendingContextDelegates[contextID];
    callback.Invoke(new Context(contextID));
    pendingContextDelegates.Remove(contextID);
  }

  /// <summary>
  /// Fetch the most recently generated Context for a given flowName
  /// </summary>
  public static Context? RecentContext(string flowName)
  {
#if CONTEXTSDK_IOS
        int contextID = contextSDK_recentContext(flowName);
        if (contextID == -1)
        {
            return null;
        }

        return new Context(contextID);
#elif CONTEXTSDK_ANDROID
        DebugLog($"RecentContext: {flowName}");
        return SafeAndroidCall(() => {
            AndroidJavaObject contextObj = ContextSDKClass.CallStatic<AndroidJavaObject>("recentContext", flowName);
            if (contextObj == null)
            {
                return null;
            }
            return new Context(-1, contextObj);
        }, "Failed to get recent context", null);
#else
    return null;
#endif
  }

  #endregion

  #region Tracking Methods

  /// <summary>
  /// Call this method to track a generic event. Using this allows us to provide you with insights on how your app behaves in the real-world context
  /// </summary>
  public static void TrackEvent(string eventName, CustomSignals? customSignals = null)
  {
#if CONTEXTSDK_IOS
        int customSignalsID = customSignals?.contextID ?? -1;
        contextSDK_trackEvent(eventName, customSignalsID);
#elif CONTEXTSDK_ANDROID
        DebugLog($"TrackEvent: {eventName}");
        SafeAndroidCall(() => {
            AndroidJavaObject customSignalsObj = customSignals?.ToAndroidCustomSignals()
                ?? new AndroidJavaObject($"{ANDROID_PACKAGE_NAME}.signals.CustomSignals");
            ContextSDKClass.CallStatic("trackEvent", eventName, customSignalsObj);
        }, "Failed to track event");
#endif
  }

  /// <summary>
  /// Call this method to track a generic page view. Using this allows us to provide you with insights on how your app behaves in the real-world context
  /// </summary>
  public static void TrackPageView(string pageName, CustomSignals? customSignals = null)
  {
#if CONTEXTSDK_IOS
        int customSignalsID = customSignals?.contextID ?? -1;
        contextSDK_trackPageView(pageName, customSignalsID);
#elif CONTEXTSDK_ANDROID
        DebugLog($"TrackPageView: {pageName}");
        SafeAndroidCall(() => {
            AndroidJavaObject customSignalsObj = customSignals?.ToAndroidCustomSignals()
                ?? new AndroidJavaObject($"{ANDROID_PACKAGE_NAME}.signals.CustomSignals");
            ContextSDKClass.CallStatic("trackPageView", pageName, customSignalsObj);
        }, "Failed to track page view");
#endif
  }

  /// <summary>
  /// Call this method to track a generic user action. Using this allows us to provide you with insights on how your app behaves in the real-world context
  /// </summary>
  public static void TrackUserAction(string userActionName, CustomSignals? customSignals = null)
  {
#if CONTEXTSDK_IOS
        int customSignalsID = customSignals?.contextID ?? -1;
        contextSDK_trackUserAction(userActionName, customSignalsID);
#elif CONTEXTSDK_ANDROID
        DebugLog($"TrackUserAction: {userActionName}");
        SafeAndroidCall(() => {
            AndroidJavaObject customSignalsObj = customSignals?.ToAndroidCustomSignals()
                ?? new AndroidJavaObject($"{ANDROID_PACKAGE_NAME}.signals.CustomSignals");
            ContextSDKClass.CallStatic("trackUserAction", userActionName, customSignalsObj);
        }, "Failed to track user action");
#endif
  }

  #endregion

  #region Helper Classes

  public class CustomSignals
  {
    internal int contextID;
#if CONTEXTSDK_ANDROID
        private Dictionary<string, object> signalValues = new Dictionary<string, object>();
#endif
    public CustomSignals()
    {
#if CONTEXTSDK_IOS
            this.contextID = contextSDK_getNextContextID();
#else
      this.contextID = -1;
#endif
    }

    public void AppendCustomSignal(string id, int value)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_addCustomSignalInt(contextID, id, value);
#elif CONTEXTSDK_ANDROID
            signalValues[id] = value;
#endif
    }

    public void AppendCustomSignal(string id, float value)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_addCustomSignalFloat(contextID, id, value);
#elif CONTEXTSDK_ANDROID
            signalValues[id] = value;
#endif
    }

    public void AppendCustomSignal(string id, bool value)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_addCustomSignalBool(contextID, id, value);
#elif CONTEXTSDK_ANDROID
            signalValues[id] = value;
#endif
    }

    public void AppendCustomSignal(string id, string value)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_addCustomSignalString(contextID, id, value);
#elif CONTEXTSDK_ANDROID
            signalValues[id] = value;
#endif
    }

#if CONTEXTSDK_ANDROID
        internal AndroidJavaObject ToAndroidCustomSignals()
        {
            AndroidJavaObject customSignalsObj = new AndroidJavaObject($"{ANDROID_PACKAGE_NAME}.signals.CustomSignals");
            foreach (var kvp in signalValues)
            {
                if (kvp.Value is int intValue)
                {
                    customSignalsObj.Call("set", kvp.Key, intValue);
                }
                else if (kvp.Value is float floatValue)
                {
                    customSignalsObj.Call("set", kvp.Key, floatValue);
                }
                else if (kvp.Value is bool boolValue)
                {
                    customSignalsObj.Call("set", kvp.Key, boolValue);
                }
                else if (kvp.Value is string stringValue)
                {
                    customSignalsObj.Call("set", kvp.Key, stringValue);
                }
            }
            return customSignalsObj;
        }
#endif

    ~CustomSignals()
    {
#if CONTEXTSDK_IOS
            contextSDK_releaseCustomSignals(contextID);
#endif
    }
  }

  public class RevenueEvent
  {
    public readonly float revenue;
    public readonly Currency currency;
    public readonly RevenueSource source;

    public RevenueEvent(RevenueSource source, float revenue, Currency currency)
    {
      this.source = source;
      this.revenue = revenue;
      this.currency = currency;
    }
  }

  public class Context
  {
    internal int contextID;
    internal AndroidJavaObject? androidContextObj;
    /// <summary>
    /// Check if you now is a good time to show an upsell prompt. During calibration phase, this will always be `true`.
    /// </summary>
    public readonly bool shouldUpsell = true;

#if CONTEXTSDK_ANDROID
        private AndroidJavaObject? OutcomeMetadata
        {
            get
            {
                if (androidContextObj != null)
                {
                    return androidContextObj.Call<AndroidJavaObject>("getOutcomeMetadata");
                }
                return null;
            }
        }
#endif

    internal Context(int contextID, AndroidJavaObject? androidContextObj = null)
    {
      this.contextID = contextID;
      this.androidContextObj = androidContextObj;
#if CONTEXTSDK_IOS
            this.shouldUpsell = contextSDK_context_shouldUpsell(contextID);
#elif CONTEXTSDK_ANDROID
            if (androidContextObj != null)
            {
                this.shouldUpsell = androidContextObj.Call<bool>("getShouldUpsell");
            }
#endif
    }

    /// <summary>
    ///  Use this method to log an outcome and send the context event to the backend
    /// </summary>
    public void Log(Outcome outcome)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_log(contextID, ((int)outcome));
#elif CONTEXTSDK_ANDROID
            DebugLog($"Log: {outcome}");
            if (androidContextObj != null)
            {
                SafeAndroidCall(() => {
                    AndroidJavaObject eventOutcome = GetAndroidEventOutcome(outcome);
                    androidContextObj.Call("log", eventOutcome);
                }, "Failed to log outcome");
            }
#endif
    }

    /// <summary>
    /// Use this function to log an outcome only if this particular context object hasn't been logged yet
    /// </summary>
    public void LogIfNotLoggedYet(Outcome outcome)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_logIfNotLoggedYet(contextID, ((int)outcome));
#elif CONTEXTSDK_ANDROID
            DebugLog($"LogIfNotLoggedYet: {outcome}");
            if (androidContextObj != null)
            {
                SafeAndroidCall(() => {
                    AndroidJavaObject eventOutcome = GetAndroidEventOutcome(outcome);
                    androidContextObj.Call("logIfNotLoggedYet", eventOutcome);
                }, "Failed to log if not logged yet");
            }
#endif
    }

    /// <summary>
    /// This method allows you to append custom outcomes to that specific outcome
    /// We recommend using this method to provide information like the selected price tier / plan, or other relevant info about the type of product or action the user has selected
    /// </summary>
    public void AppendOutcomeMetadata(string id, int value)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_appendOutcomeMetadataInt(contextID, id, value);
#elif CONTEXTSDK_ANDROID
            DebugLog($"AppendOutcomeMetadata: {id} = {value}");
            if (OutcomeMetadata != null)
            {
                SafeAndroidCall(() => {
                    OutcomeMetadata.Call("set", id, value);
                }, "Failed to append outcome metadata");
            }
#endif
    }

    /// <summary>
    /// This method allows you to append custom outcomes to that specific outcome
    /// We recommend using this method to provide information like the selected price tier / plan, or other relevant info about the type of product or action the user has selected
    /// </summary>
    public void AppendOutcomeMetadata(string id, bool value)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_appendOutcomeMetadataBool(contextID, id, value);
#elif CONTEXTSDK_ANDROID
            DebugLog($"AppendOutcomeMetadata: {id} = {value}");
            if (OutcomeMetadata != null)
            {
                SafeAndroidCall(() => {
                    OutcomeMetadata.Call("set", id, value);
                }, "Failed to append outcome metadata");
            }
#endif
    }

    /// <summary>
    /// This method allows you to append custom outcomes to that specific outcome
    /// We recommend using this method to provide information like the selected price tier / plan, or other relevant info about the type of product or action the user has selected
    /// </summary>
    public void AppendOutcomeMetadata(string id, float value)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_appendOutcomeMetadataFloat(contextID, id, value);
#elif CONTEXTSDK_ANDROID
            DebugLog($"AppendOutcomeMetadata: {id} = {value}");
            if (OutcomeMetadata != null)
            {
                SafeAndroidCall(() => {
                    OutcomeMetadata.Call("set", id, value);
                }, "Failed to append outcome metadata");
            }
#endif
    }

    /// <summary>
    /// This method allows you to append custom outcomes to that specific outcome
    /// We recommend using this method to provide information like the selected price tier / plan, or other relevant info about the type of product or action the user has selected
    /// </summary>
    public void AppendOutcomeMetadata(string id, string value)
    {
#if CONTEXTSDK_IOS
            contextSDK_context_appendOutcomeMetadataString(contextID, id, value);
#elif CONTEXTSDK_ANDROID
            DebugLog($"AppendOutcomeMetadata: {id} = {value}");
            if (OutcomeMetadata != null)
            {
                SafeAndroidCall(() => {
                    OutcomeMetadata.Call("set", id, value);
                }, "Failed to append outcome metadata");
            }
#endif
    }

    /// <summary>
    /// Tracks a revenue event and logs the given outcome.
    /// </summary>
    /// <param name="revenueEvent">The revenue event information.</param>
    /// <param name="outcome">The outcome to log. Defaults to <see cref="Outcome.Positive"/>.</param>
    public void LogRevenueOutcome(RevenueEvent revenueEvent, Outcome outcome = Outcome.Positive)
    {
      AppendOutcomeMetadata("ctx_revenue", revenueEvent.revenue);
      AppendOutcomeMetadata("ctx_currency", currencyCode(revenueEvent.currency));
      AppendOutcomeMetadata("ctx_revenue_source", revenueSourceIdentifier(revenueEvent.source));
      Log(outcome);
    }

#if CONTEXTSDK_ANDROID
        private AndroidJavaObject GetAndroidEventOutcome(Outcome outcome)
        {
            string eventOutcomeName = outcome switch
            {
                Outcome.PositiveAdTapped => "POSITIVE_AD_TAPPED",
                Outcome.PositiveConverted => "POSITIVE_CONVERTED",
                Outcome.PositiveInteracted => "POSITIVE_INTERACTED",
                Outcome.Positive => "POSITIVE",
                Outcome.Skipped => "SKIPPED",
                Outcome.Negative => "NEGATIVE",
                Outcome.NegativeNotInteracted => "NEGATIVE_NOT_INTERACTED",
                Outcome.NegativeDismissed => "NEGATIVE_DISMISSED",
                _ => throw new ArgumentOutOfRangeException(nameof(outcome), outcome, "Unknown outcome value")
            };

            AndroidJavaClass eventOutcomeClass = new AndroidJavaClass($"{ANDROID_PACKAGE_NAME}.enums.EventOutcome");
            return eventOutcomeClass.GetStatic<AndroidJavaObject>(eventOutcomeName);
        }
#endif

    /// <summary>
    /// Call this method to do a local validation of your ContextSDK setup
    /// </summary>
    /// <returns>A string with diagnostic information.</returns>
    public string Validate()
    {
#if CONTEXTSDK_IOS
            return contextSDK_context_validate(contextID);
#elif CONTEXTSDK_ANDROID
            DebugLog("Validate called");
            if (androidContextObj != null)
            {
                return SafeAndroidCall(() => {
                    return androidContextObj.Call<string>("validate");
                }, "Failed to validate context", "Validation failed.");
            }
            return "No Context object available for validation.";
#else
      return "Calling Validate from outside of iOS is not supported.";
#endif
    }

    ~Context()
    {
#if CONTEXTSDK_IOS
            contextSDK_releaseContext(contextID);
#endif
    }
  }

  /// <summary>
  /// Describes the outcome of a flow.
  /// </summary>
  public enum Outcome
  {
    /// <summary>
    /// Optional outcome: The user has tapped on the ad, and followed any external link provided.
    /// </summary>

    PositiveAdTapped = 4,

    /// <summary>
    /// Optional outcome: The user ended up successfully purchasing the product (all the way through the payment flow)
    /// </summary>
    PositiveConverted = 3,

    /// <summary>
    ///  Optional outcome: The user has tapped on the banner, and started the purchase flow, or read more about the offer
    /// </summary>
    PositiveInteracted = 2,

    /// <summary>
    ///  A generic, positive signal. Use this for the basic ContextSDK integration, e.g. when showing an upsell prompt.
    /// </summary>
    Positive = 1,

    /// <summary>
    /// Log this when ContextSDK has recommended to skip showing an upsell prompt (`.shouldUpsell` is false). Logging this explicitly is not required if you use `ContextSDKBinding.Calibrate()` as it will be handled by ContextSDK automatically.
    /// </summary>
    Skipped = 0,

    /// <summary>
    ///  A generic, negative signal. Use this for the basic ContextSDK integration, on a user e.g. declining or dismissing an upsell prompt
    /// </summary>
    Negative = -1,

    /// <summary>
    ///  Optional outcome: Use this as a negative signal of a user not interacting with e.g. a banner. Depending on your app, we may recommend to log this when the app is put into the background, and hasn't interacted with a banner in any way. This can be done using the `LogIfNotLoggedYet` method
    /// </summary>
    NegativeNotInteracted = -2,

    /// <summary>
    /// Optional outcome: The user has actively dismissed the banner (e.g. using a close button)
    /// </summary>
    NegativeDismissed = -3
  }

  public enum RevenueSource
  {
    AdImpression,
    AdClick,
    AdClickThrough,
    AdConversion,
    AdEngagement,
    PurchaseOutsideAppStore,
    SubscriptionOutsideAppStore,
    PurchaseAppStore,
    SubscriptionAppStore,
    Affiliate,
    Tip,
    Donation,
  }

  private static string revenueSourceIdentifier(RevenueSource source)
  {
    return source switch
    {
      RevenueSource.AdImpression => "ad_impression",
      RevenueSource.AdClick => "ad_click",
      RevenueSource.AdClickThrough => "ad_click_through",
      RevenueSource.AdConversion => "ad_conversion",
      RevenueSource.AdEngagement => "ad_engagement",
      RevenueSource.PurchaseOutsideAppStore => "purchase_outside_app_store",
      RevenueSource.SubscriptionOutsideAppStore => "subscription_outside_app_store",
      RevenueSource.PurchaseAppStore => "purchase_app_store",
      RevenueSource.SubscriptionAppStore => "subscription_app_store",
      RevenueSource.Affiliate => "affiliate",
      RevenueSource.Tip => "tip",
      RevenueSource.Donation => "donation",
      _ => "",
    };
  }

  public enum Currency
  {
    aed, afn, all, amd, ang, aoa, ars, aud, awg, azn,
    bam, bbd, bdt, bgn, bhd, bif, bmd, bnd, bob, brl,
    bsd, btn, bwp, byn, bzd, cad, cdf, chf, clp, cny,
    cop, crc, cup, cve, czk, djf, dkk, dop, dzd, egp,
    ern, etb, eur, fjd, fkp, fok, gbp, gel, ggp, ghs,
    gip, gmd, gnf, gtq, gyd, hkd, hnl, hrk, htg, huf,
    idr, ils, imp, inr, iqd, irr, isk, jep, jmd, jod,
    jpy, kes, kgs, khr, kid, kmf, krw, kwd, kyd, kzt,
    lak, lbp, lkr, lrd, lsl, lyd, mad, mdl, mga, mkd,
    mmk, mnt, mop, mru, mur, mvr, mwk, mxn, myr, mzn,
    nad, ngn, nio, nok, npr, nzd, omr, pab, pen, pgk,
    php, pkr, pln, pyg, qar, ron, rsd, rub, rwf, sar,
    sbd, scr, sdg, sek, sgd, shp, sll, sos, srd, ssp,
    stn, syp, szl, thb, tjs, tmt, tnd, top, @try, ttd,
    tvd, twd, tzs, uah, ugx, usd, uyu, uzs, ves, vnd,
    vuv, wst, xaf, xcd, xof, xpf, yer, zar, zmw, zwl,
  }

  private static string currencyCode(Currency currency)
  {
    return currency.ToString().ToUpper();
  }

  #endregion
}
