// =============================================================================
// TudadaSDK Unity Interface
//
// Unity WebGL에서 TudadaSDK의 모든 API를 사용할 수 있게 해주는 메인 싱글톤입니다.
// WX(WeChat SDK)를 우회하지 않고 TudadaSDK와 직접 통신합니다.
//
// 사용법:
// 1. 씬에 빈 GameObject를 생성하고 이 스크립트를 추가합니다.
// 2. 또는 TudadaSDK.Instance를 호출하면 자동으로 생성됩니다.
//
// 예시:
// TudadaSDK.Instance.Login(
//     onSuccess: (res) => Debug.Log("Login code: " + res.code),
//     onFail: (err) => Debug.LogError("Login failed: " + err)
// );
// =============================================================================

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using Tudada;
using Tudada.Internal;

/// <summary>
/// TudadaSDK Unity 인터페이스 (메인 클래스)
/// </summary>
public partial class TudadaSDK : MonoBehaviour
{
    #region Singleton

    private static TudadaSDK _instance;
    private static readonly object _lock = new object();
    private static bool _applicationIsQuitting = false;

    /// <summary>
    /// 싱글톤 인스턴스 (스레드 안전, 자동 생성)
    /// </summary>
    public static TudadaSDK Instance
    {
        get
        {
            if (_applicationIsQuitting)
            {
                Debug.LogWarning($"{Constants.LOG_PREFIX} Instance requested after application quit");
                return null;
            }

            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = FindObjectOfType<TudadaSDK>();

                    if (_instance == null)
                    {
                        var go = new GameObject(Constants.GAME_OBJECT_NAME);
                        _instance = go.AddComponent<TudadaSDK>();
                        DontDestroyOnLoad(go);
                    }
                }
                return _instance;
            }
        }
    }

    #endregion

    #region DllImport - Core

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern int TudadaSDK_IsAvailable();

    [DllImport("__Internal")]
    private static extern void TudadaSDK_Initialize();
#endif

    #endregion

    #region DllImport - Storage

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void TudadaSDK_SetStorage(string key, string data, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_SetStorageSync(string key, string data);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_GetStorage(string key, int callbackId);

    [DllImport("__Internal")]
    private static extern string TudadaSDK_GetStorageSync(string key);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_RemoveStorage(string key, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_RemoveStorageSync(string key);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_ClearStorage(int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_ClearStorageSync();

    [DllImport("__Internal")]
    private static extern string TudadaSDK_GetStorageInfoSync();

    [DllImport("__Internal")]
    private static extern void TudadaSDK_TudadaStoreSave(string key, string value, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_TudadaStoreGet(string key, int callbackId);
#endif

    #endregion

    #region DllImport - System

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern string TudadaSDK_GetSystemInfoSync();

    [DllImport("__Internal")]
    private static extern void TudadaSDK_GetSystemInfo(int callbackId);

    [DllImport("__Internal")]
    private static extern string TudadaSDK_GetWindowInfo();

    [DllImport("__Internal")]
    private static extern string TudadaSDK_GetAppBaseInfo();

    [DllImport("__Internal")]
    private static extern string TudadaSDK_GetDeviceInfo();

    [DllImport("__Internal")]
    private static extern string TudadaSDK_GetMenuButtonBoundingClientRect();
#endif

    #endregion

    #region DllImport - Auth

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void TudadaSDK_Login(int timeout, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_CheckSession(int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_GetUserInfo(int callbackId);
#endif

    #endregion

    #region DllImport - Device

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void TudadaSDK_VibrateShort(string type, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_VibrateLong(int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_ShowKeyboard(string defaultValue, int maxLength, int multiple, int confirmHold, string confirmType, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_HideKeyboard(int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_SetClipboardData(string data, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_GetClipboardData(int callbackId);
#endif

    #endregion

    #region DllImport - Lifecycle

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern string TudadaSDK_GetLaunchOptions();

    [DllImport("__Internal")]
    private static extern void TudadaSDK_ExitMiniProgram(int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_RestartMiniProgram(int callbackId);
#endif

    #endregion

    #region DllImport - Accelerometer

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void TudadaSDK_StartAccelerometerSensor(string sensitivity, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_StopAccelerometerSensor(int callbackId);
#endif

    #endregion

    #region DllImport - CheckFeature

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void TudadaSDK_CheckFeature(string apiName, int callbackId);
#endif

    #endregion

    #region DllImport - Ad

#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void TudadaSDK_CreateRewardedVideoAd(string adUnitId, int adInstanceId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_ShowRewardedAd(string slotId, string adUnitId, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_GetRewardedAdSlots(int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_GetRewardedAdSlot(string slotId, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_ShareForReward(string url, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_GetAvailableBannerIds(int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_GetBanner(string bannerId, int callbackId);

    [DllImport("__Internal")]
    private static extern void TudadaSDK_RunBannerAction(string bannerId, int callbackId);
#endif

    #endregion

    #region Callback Management

    private readonly Dictionary<int, Action<string>> _successCallbacks = new Dictionary<int, Action<string>>();
    private readonly Dictionary<int, Action<string>> _failCallbacks = new Dictionary<int, Action<string>>();
    private int _callbackId;

    /// <summary>
    /// 새로운 콜백 ID 생성 및 콜백 등록
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    /// <returns>콜백 ID</returns>
    protected int RegisterCallbacks(Action<string> onSuccess, Action<string> onFail)
    {
        int id = _callbackId++;

        if (onSuccess != null)
        {
            _successCallbacks[id] = onSuccess;
        }
        if (onFail != null)
        {
            _failCallbacks[id] = onFail;
        }

        return id;
    }

    /// <summary>
    /// 제네릭 콜백 등록 (JSON 자동 파싱)
    /// </summary>
    /// <typeparam name="T">결과 타입</typeparam>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    /// <param name="defaultValue">파싱 실패 시 기본값</param>
    /// <returns>콜백 ID</returns>
    protected int RegisterCallbacks<T>(Action<T> onSuccess, Action<string> onFail, T defaultValue) where T : class
    {
        return RegisterCallbacks(
            json => InvokeTypedCallback(json, onSuccess, defaultValue),
            onFail
        );
    }

    /// <summary>
    /// 타입 콜백 실행 (JSON 파싱 포함)
    /// </summary>
    private void InvokeTypedCallback<T>(string json, Action<T> onSuccess, T defaultValue) where T : class
    {
        if (onSuccess == null) return;

        try
        {
            var result = JsonUtility.FromJson<T>(json);
            onSuccess.Invoke(result ?? defaultValue);
        }
        catch
        {
            onSuccess.Invoke(defaultValue);
        }
    }

    /// <summary>
    /// 성공 콜백 실행
    /// </summary>
    protected void ExecuteSuccessCallback(int callbackId, string data)
    {
        if (_successCallbacks.TryGetValue(callbackId, out var callback))
        {
            try
            {
                callback?.Invoke(data);
            }
            catch (Exception e)
            {
                Debug.LogError($"{Constants.LOG_PREFIX} Success callback error: {e.Message}");
            }
        }

        CleanupCallbacks(callbackId);
    }

    /// <summary>
    /// 실패 콜백 실행
    /// </summary>
    protected void ExecuteFailCallback(int callbackId, string errMsg)
    {
        if (_failCallbacks.TryGetValue(callbackId, out var callback))
        {
            try
            {
                callback?.Invoke(errMsg);
            }
            catch (Exception e)
            {
                Debug.LogError($"{Constants.LOG_PREFIX} Fail callback error: {e.Message}");
            }
        }

        CleanupCallbacks(callbackId);
    }

    /// <summary>
    /// 콜백 정리
    /// </summary>
    protected void CleanupCallbacks(int callbackId)
    {
        _successCallbacks.Remove(callbackId);
        _failCallbacks.Remove(callbackId);
        _paymentFailCallbacks.Remove(callbackId);
    }

    /// <summary>
    /// jslib에서 전달받은 데이터 파싱 및 콜백 실행
    /// </summary>
    protected void HandleCallback(string rawData, bool isSuccess)
    {
        var data = CallbackData.Parse(rawData);

        if (data.callbackId < 0)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} {Messages.INVALID_CALLBACK_DATA}: {rawData}");
            return;
        }

        if (isSuccess)
        {
            ExecuteSuccessCallback(data.callbackId, data.data);
        }
        else
        {
            ExecuteFailCallback(data.callbackId, data.data);
        }
    }

    /// <summary>
    /// 광고용 콜백 등록 (TudadaRewardedVideoAd에서 사용)
    /// </summary>
    internal int RegisterAdCallback(Action<string> onSuccess, Action<string> onFail)
    {
        return RegisterCallbacks(onSuccess, onFail);
    }

    /// <summary>
    /// CheckFeature용 콜백 등록 (success/fail 모두 CheckFeatureResult 파싱)
    /// </summary>
    private readonly Dictionary<int, Action<CheckFeatureResult>> _checkFeatureSuccessCallbacks = new Dictionary<int, Action<CheckFeatureResult>>();
    private readonly Dictionary<int, Action<CheckFeatureResult>> _checkFeatureFailCallbacks = new Dictionary<int, Action<CheckFeatureResult>>();

    private int RegisterCheckFeatureCallbacks(Action<CheckFeatureResult> onSuccess, Action<CheckFeatureResult> onFail)
    {
        int id = _callbackId++;
        if (onSuccess != null) _checkFeatureSuccessCallbacks[id] = onSuccess;
        if (onFail != null) _checkFeatureFailCallbacks[id] = onFail;
        return id;
    }

    private void HandleCheckFeatureCallback(string rawData, bool isSuccess)
    {
        var data = CallbackData.Parse(rawData);
        if (data.callbackId < 0)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} {Messages.INVALID_CALLBACK_DATA}: {rawData}");
            return;
        }

        CheckFeatureResult result;
        try
        {
            result = JsonUtility.FromJson<CheckFeatureResult>(data.data);
        }
        catch
        {
            result = new CheckFeatureResult
            {
                errMsg = data.data,
                supported = false,
                status = "platform_unsupported"
            };
        }

        if (isSuccess && _checkFeatureSuccessCallbacks.TryGetValue(data.callbackId, out var successCb))
        {
            try { successCb.Invoke(result); } catch (Exception e)
            { Debug.LogError($"{Constants.LOG_PREFIX} CheckFeature success callback error: {e.Message}"); }
        }
        else if (!isSuccess && _checkFeatureFailCallbacks.TryGetValue(data.callbackId, out var failCb))
        {
            try { failCb.Invoke(result); } catch (Exception e)
            { Debug.LogError($"{Constants.LOG_PREFIX} CheckFeature fail callback error: {e.Message}"); }
        }

        _checkFeatureSuccessCallbacks.Remove(data.callbackId);
        _checkFeatureFailCallbacks.Remove(data.callbackId);
    }

    /// <summary>
    /// 결제용 콜백 등록 — success 는 타입(T) 파싱, fail 은 PaymentFailResult 파싱(code 분기).
    /// </summary>
    private readonly Dictionary<int, Action<PaymentFailResult>> _paymentFailCallbacks = new Dictionary<int, Action<PaymentFailResult>>();

    internal int RegisterPaymentCallbacks<T>(Action<T> onSuccess, Action<PaymentFailResult> onFail, T defaultValue) where T : class
    {
        int id = _callbackId++;
        if (onSuccess != null) _successCallbacks[id] = json => InvokeTypedCallback(json, onSuccess, defaultValue);
        if (onFail != null) _paymentFailCallbacks[id] = onFail;
        return id;
    }

    /// <summary>
    /// 결제 실패 콜백 처리 — fail JSON({ code, errMsg, txnKey, failReason })을 PaymentFailResult 로 파싱.
    /// 평문(파싱 실패)·빈 code 는 보수적으로 보정한다(PAYMENT_UNAVAILABLE).
    /// </summary>
    private void HandlePaymentFailCallback(string rawData)
    {
        var data = CallbackData.Parse(rawData);
        if (data.callbackId < 0)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} {Messages.INVALID_CALLBACK_DATA}: {rawData}");
            return;
        }

        PaymentFailResult result = null;
        try { result = JsonUtility.FromJson<PaymentFailResult>(data.data); }
        catch { result = null; }
        if (result == null)
        {
            result = new PaymentFailResult { errMsg = data.data };
        }
        if (string.IsNullOrEmpty(result.code))
        {
            result.code = PaymentErrorCode.PaymentUnavailable;
        }

        if (_paymentFailCallbacks.TryGetValue(data.callbackId, out var failCb))
        {
            try { failCb.Invoke(result); }
            catch (Exception e)
            { Debug.LogError($"{Constants.LOG_PREFIX} Payment fail callback error: {e.Message}"); }
        }

        CleanupCallbacks(data.callbackId);
    }

    #endregion

    #region Events - Lifecycle

    /// <summary>
    /// 앱이 포그라운드로 전환될 때 발생
    /// </summary>
    public static event TudadaEventHandler<OnShowResult> OnShow;

    /// <summary>
    /// 앱이 백그라운드로 전환될 때 발생
    /// </summary>
    public static event TudadaEventHandler<OnHideResult> OnHide;

    #endregion

    #region Events - Keyboard

    /// <summary>
    /// 키보드 입력 시 발생
    /// </summary>
    public static event TudadaEventHandler<KeyboardInputResult> OnKeyboardInput;

    /// <summary>
    /// 키보드에서 확인 버튼 클릭 시 발생
    /// </summary>
    public static event TudadaEventHandler<KeyboardConfirmResult> OnKeyboardConfirm;

    /// <summary>
    /// 키보드가 닫힐 때 발생
    /// </summary>
    public static event TudadaEventHandler<KeyboardCompleteResult> OnKeyboardComplete;

    #endregion

    #region Unity Lifecycle

    private void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);

            gameObject.name = Constants.GAME_OBJECT_NAME;

            Debug.Log($"{Constants.LOG_PREFIX} Initializing...");

#if UNITY_WEBGL && !UNITY_EDITOR
            TudadaSDK_Initialize();
#endif

            Debug.Log($"{Constants.LOG_PREFIX} Initialized");
        }
        else if (_instance != this)
        {
            Destroy(gameObject);
        }
    }

    private void OnDestroy()
    {
        if (_instance == this)
        {
            _applicationIsQuitting = true;
        }
    }

    #endregion

    #region Public API - Core

    /// <summary>
    /// TudadaSDK가 사용 가능한지 확인
    /// </summary>
    /// <returns>사용 가능 여부</returns>
    public bool IsAvailable()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        return TudadaSDK_IsAvailable() == 1;
#else
        return true;
#endif
    }

    #endregion

    #region Public API - Storage

    /// <summary>
    /// 로컬 스토리지에 데이터 저장 (비동기)
    /// </summary>
    /// <param name="key">저장할 키</param>
    /// <param name="data">저장할 데이터 (문자열)</param>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void SetStorage(string key, string data, Action<SetStorageResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"SetStorage - key: {key}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new SetStorageResult { errMsg = Messages.SET_STORAGE_OK }
        );
        TudadaSDK_SetStorage(key, data, callbackId);
#else
        ExecuteEditorStorage(
            () =>
            {
                PlayerPrefs.SetString(Constants.STORAGE_KEY_PREFIX + key, data);
                PlayerPrefs.Save();
            },
            () => onSuccess?.Invoke(new SetStorageResult { errMsg = Messages.SET_STORAGE_OK }),
            onFail,
            "SetStorage",
            key
        );
#endif
    }

    /// <summary>
    /// 로컬 스토리지에 데이터 저장 (동기)
    /// </summary>
    /// <param name="key">저장할 키</param>
    /// <param name="data">저장할 데이터 (문자열)</param>
    public void SetStorageSync(string key, string data)
    {
        LogDebug($"SetStorageSync - key: {key}");

#if UNITY_WEBGL && !UNITY_EDITOR
        TudadaSDK_SetStorageSync(key, data);
#else
        PlayerPrefs.SetString(Constants.STORAGE_KEY_PREFIX + key, data);
        PlayerPrefs.Save();
        LogEditor("SetStorageSync", key);
#endif
    }

    /// <summary>
    /// 로컬 스토리지에서 데이터 조회 (비동기)
    /// </summary>
    /// <param name="key">조회할 키</param>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void GetStorage(string key, Action<GetStorageResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"GetStorage - key: {key}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new GetStorageResult { data = "", errMsg = Messages.GET_STORAGE_OK }
        );
        TudadaSDK_GetStorage(key, callbackId);
#else
        ExecuteEditorStorage(
            () =>
            {
                string value = PlayerPrefs.GetString(Constants.STORAGE_KEY_PREFIX + key, "");
                LogEditor("GetStorage", $"{key} = {value}");
                onSuccess?.Invoke(new GetStorageResult { data = value, errMsg = Messages.GET_STORAGE_OK });
            },
            null,
            onFail,
            "GetStorage",
            key
        );
#endif
    }

    /// <summary>
    /// 로컬 스토리지에서 데이터 조회 (동기)
    /// </summary>
    /// <param name="key">조회할 키</param>
    /// <returns>저장된 데이터 (문자열)</returns>
    public string GetStorageSync(string key)
    {
        LogDebug($"GetStorageSync - key: {key}");

#if UNITY_WEBGL && !UNITY_EDITOR
        return TudadaSDK_GetStorageSync(key);
#else
        string value = PlayerPrefs.GetString(Constants.STORAGE_KEY_PREFIX + key, "");
        LogEditor("GetStorageSync", $"{key} = {value}");
        return value;
#endif
    }

    /// <summary>
    /// 로컬 스토리지에서 데이터 삭제 (비동기)
    /// </summary>
    /// <param name="key">삭제할 키</param>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void RemoveStorage(string key, Action<RemoveStorageResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"RemoveStorage - key: {key}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new RemoveStorageResult { errMsg = Messages.REMOVE_STORAGE_OK }
        );
        TudadaSDK_RemoveStorage(key, callbackId);
#else
        ExecuteEditorStorage(
            () =>
            {
                PlayerPrefs.DeleteKey(Constants.STORAGE_KEY_PREFIX + key);
                PlayerPrefs.Save();
            },
            () => onSuccess?.Invoke(new RemoveStorageResult { errMsg = Messages.REMOVE_STORAGE_OK }),
            onFail,
            "RemoveStorage",
            key
        );
#endif
    }

    /// <summary>
    /// 로컬 스토리지에서 데이터 삭제 (동기)
    /// </summary>
    /// <param name="key">삭제할 키</param>
    public void RemoveStorageSync(string key)
    {
        LogDebug($"RemoveStorageSync - key: {key}");

#if UNITY_WEBGL && !UNITY_EDITOR
        TudadaSDK_RemoveStorageSync(key);
#else
        PlayerPrefs.DeleteKey(Constants.STORAGE_KEY_PREFIX + key);
        PlayerPrefs.Save();
        LogEditor("RemoveStorageSync", key);
#endif
    }

    /// <summary>
    /// 로컬 스토리지 전체 삭제 (비동기)
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void ClearStorage(Action<ClearStorageResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("ClearStorage");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new ClearStorageResult { errMsg = Messages.CLEAR_STORAGE_OK }
        );
        TudadaSDK_ClearStorage(callbackId);
#else
        ExecuteEditorStorage(
            () =>
            {
                PlayerPrefs.DeleteAll();
                PlayerPrefs.Save();
            },
            () => onSuccess?.Invoke(new ClearStorageResult { errMsg = Messages.CLEAR_STORAGE_OK }),
            onFail,
            "ClearStorage"
        );
#endif
    }

    /// <summary>
    /// 로컬 스토리지 전체 삭제 (동기)
    /// </summary>
    public void ClearStorageSync()
    {
        LogDebug("ClearStorageSync");

#if UNITY_WEBGL && !UNITY_EDITOR
        TudadaSDK_ClearStorageSync();
#else
        PlayerPrefs.DeleteAll();
        PlayerPrefs.Save();
        LogEditor("ClearStorageSync");
#endif
    }

    /// <summary>
    /// 스토리지 정보 조회 (동기)
    /// </summary>
    /// <returns>스토리지 정보</returns>
    public StorageInfo GetStorageInfoSync()
    {
        LogDebug("GetStorageInfoSync");

#if UNITY_WEBGL && !UNITY_EDITOR
        string json = TudadaSDK_GetStorageInfoSync();
        return ParseJsonOrDefault(json, CreateDefaultStorageInfo());
#else
        LogEditor("GetStorageInfoSync");
        return CreateDefaultStorageInfo();
#endif
    }

    /// <summary>
    /// TudadaStore에 값 저장 (클라우드 스토리지)
    /// </summary>
    /// <remarks>
    /// <para><b>용량 제한:</b> value는 최대 2KB까지 저장 가능합니다.</para>
    /// </remarks>
    /// <param name="key">저장할 키</param>
    /// <param name="value">저장할 값 (문자열, 최대 2KB)</param>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void TudadaStoreSave(string key, string value, Action<TudadaStoreSaveResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"TudadaStoreSave - key: {key}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new TudadaStoreSaveResult { errMsg = Messages.TUDADA_STORE_SAVE_OK }
        );
        TudadaSDK_TudadaStoreSave(key, value, callbackId);
#else
        ExecuteEditorStorage(
            () =>
            {
                PlayerPrefs.SetString(Constants.TUDADA_STORE_KEY_PREFIX + key, value);
                PlayerPrefs.Save();
            },
            () => onSuccess?.Invoke(new TudadaStoreSaveResult { errMsg = Messages.TUDADA_STORE_SAVE_OK }),
            onFail,
            "TudadaStoreSave",
            key
        );
#endif
    }

    /// <summary>
    /// TudadaStore에서 값 조회 (클라우드 스토리지)
    /// </summary>
    /// <param name="key">조회할 키</param>
    /// <param name="onSuccess">성공 콜백 (조회된 값)</param>
    /// <param name="onFail">실패 콜백</param>
    public void TudadaStoreGet(string key, Action<TudadaStoreGetResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"TudadaStoreGet - key: {key}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new TudadaStoreGetResult { value = "", errMsg = Messages.TUDADA_STORE_GET_OK }
        );
        TudadaSDK_TudadaStoreGet(key, callbackId);
#else
        ExecuteEditorStorage(
            () =>
            {
                string value = PlayerPrefs.GetString(Constants.TUDADA_STORE_KEY_PREFIX + key, "");
                LogEditor("TudadaStoreGet", $"{key} = {value}");
                onSuccess?.Invoke(new TudadaStoreGetResult { value = value, errMsg = Messages.TUDADA_STORE_GET_OK });
            },
            null,
            onFail,
            "TudadaStoreGet",
            key
        );
#endif
    }

    #endregion

    #region Public API - System

    /// <summary>
    /// 시스템 정보 조회 (동기)
    /// </summary>
    /// <returns>시스템 정보</returns>
    public Tudada.SystemInfo GetSystemInfoSync()
    {
        LogDebug("GetSystemInfoSync");

#if UNITY_WEBGL && !UNITY_EDITOR
        string json = TudadaSDK_GetSystemInfoSync();
        return ParseJsonOrDefault(json, CreateDefaultSystemInfo());
#else
        LogEditor("GetSystemInfoSync");
        return CreateDefaultSystemInfo();
#endif
    }

    /// <summary>
    /// 시스템 정보 조회 (비동기)
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void GetSystemInfo(Action<Tudada.SystemInfo> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("GetSystemInfo");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            CreateDefaultSystemInfo()
        );
        TudadaSDK_GetSystemInfo(callbackId);
#else
        LogEditor("GetSystemInfo");
        onSuccess?.Invoke(CreateDefaultSystemInfo());
#endif
    }

    /// <summary>
    /// 창 정보 조회 (동기)
    /// </summary>
    /// <returns>창 정보</returns>
    public WindowInfo GetWindowInfo()
    {
        LogDebug("GetWindowInfo");

#if UNITY_WEBGL && !UNITY_EDITOR
        string json = TudadaSDK_GetWindowInfo();
        return ParseJsonOrDefault(json, CreateDefaultWindowInfo());
#else
        LogEditor("GetWindowInfo");
        return CreateDefaultWindowInfo();
#endif
    }

    /// <summary>
    /// 앱 기본 정보 조회 (동기)
    /// </summary>
    /// <returns>앱 기본 정보</returns>
    public AppBaseInfo GetAppBaseInfo()
    {
        LogDebug("GetAppBaseInfo");

#if UNITY_WEBGL && !UNITY_EDITOR
        string json = TudadaSDK_GetAppBaseInfo();
        return ParseJsonOrDefault(json, CreateDefaultAppBaseInfo());
#else
        LogEditor("GetAppBaseInfo");
        return CreateDefaultAppBaseInfo();
#endif
    }

    /// <summary>
    /// 디바이스 정보 조회 (동기)
    /// </summary>
    /// <returns>디바이스 정보</returns>
    public DeviceInfo GetDeviceInfo()
    {
        LogDebug("GetDeviceInfo");

#if UNITY_WEBGL && !UNITY_EDITOR
        string json = TudadaSDK_GetDeviceInfo();
        return ParseJsonOrDefault(json, CreateDefaultDeviceInfo());
#else
        LogEditor("GetDeviceInfo");
        return CreateDefaultDeviceInfo();
#endif
    }

    /// <summary>
    /// 메뉴 버튼 위치 정보 조회
    /// </summary>
    /// <returns>메뉴 버튼 위치 정보</returns>
    public MenuButtonRect GetMenuButtonBoundingClientRect()
    {
        LogDebug("GetMenuButtonBoundingClientRect");

#if UNITY_WEBGL && !UNITY_EDITOR
        string json = TudadaSDK_GetMenuButtonBoundingClientRect();
        return ParseJsonOrDefault(json, CreateDefaultMenuButtonRect());
#else
        LogEditor("GetMenuButtonBoundingClientRect");
        return CreateDefaultMenuButtonRect();
#endif
    }

    #endregion

    #region Public API - Auth

    /// <summary>
    /// 사용자 로그인
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    /// <param name="timeout">타임아웃 (ms, 기본값 0 = 타임아웃 없음)</param>
    public void Login(Action<LoginResult> onSuccess = null, Action<string> onFail = null, int timeout = 0)
    {
        LogDebug($"Login - timeout: {timeout}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            CreateMockLoginResult()
        );
        TudadaSDK_Login(timeout, callbackId);
#else
        LogEditor("Login");
        onSuccess?.Invoke(CreateMockLoginResult());
#endif
    }

    /// <summary>
    /// 세션 유효성 확인
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void CheckSession(Action<CheckSessionResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("CheckSession");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new CheckSessionResult { errMsg = Messages.CHECK_SESSION_OK }
        );
        TudadaSDK_CheckSession(callbackId);
#else
        LogEditor("CheckSession - session valid");
        onSuccess?.Invoke(new CheckSessionResult { errMsg = Messages.CHECK_SESSION_OK });
#endif
    }

    /// <summary>
    /// 사용자 정보 조회
    ///
    /// 사용자 프로필과 게임 서버 검증용 자격증명
    /// (userInfoPayload/userInfoSignature)을 반환한다.
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void GetUserInfo(Action<GetUserInfoResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("GetUserInfo");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            CreateMockGetUserInfoResult()
        );
        TudadaSDK_GetUserInfo(callbackId);
#else
        LogEditor("GetUserInfo");
        onSuccess?.Invoke(CreateMockGetUserInfoResult());
#endif
    }

    #endregion

    #region Public API - Device

    /// <summary>
    /// 짧은 진동 (15ms)
    /// </summary>
    /// <param name="type">진동 강도 (heavy, medium, light)</param>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void VibrateShort(VibrateType type = VibrateType.medium, Action<VibrateResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"VibrateShort - type: {type}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new VibrateResult { errMsg = Messages.VIBRATE_SHORT_OK }
        );
        TudadaSDK_VibrateShort(type.ToString(), callbackId);
#else
        LogEditor("VibrateShort", type.ToString());
        onSuccess?.Invoke(new VibrateResult { errMsg = Messages.VIBRATE_SHORT_OK });
#endif
    }

    /// <summary>
    /// 긴 진동 (400ms)
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void VibrateLong(Action<VibrateResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("VibrateLong");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new VibrateResult { errMsg = Messages.VIBRATE_LONG_OK }
        );
        TudadaSDK_VibrateLong(callbackId);
#else
        LogEditor("VibrateLong");
        onSuccess?.Invoke(new VibrateResult { errMsg = Messages.VIBRATE_LONG_OK });
#endif
    }

    /// <summary>
    /// 키보드 표시
    /// </summary>
    /// <param name="defaultValue">기본값</param>
    /// <param name="maxLength">최대 길이 (0 = 무제한)</param>
    /// <param name="multiple">여러 줄 입력 여부</param>
    /// <param name="confirmHold">완료 버튼 클릭 시 키보드 유지 여부</param>
    /// <param name="confirmType">확인 버튼 타입</param>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void ShowKeyboard(
        string defaultValue = "",
        int maxLength = 0,
        bool multiple = false,
        bool confirmHold = false,
        KeyboardConfirmType confirmType = KeyboardConfirmType.done,
        Action<ShowKeyboardResult> onSuccess = null,
        Action<string> onFail = null)
    {
        LogDebug($"ShowKeyboard - defaultValue: {defaultValue}, maxLength: {maxLength}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new ShowKeyboardResult { errMsg = Messages.SHOW_KEYBOARD_OK }
        );
        TudadaSDK_ShowKeyboard(defaultValue, maxLength, multiple ? 1 : 0, confirmHold ? 1 : 0, confirmType.ToString(), callbackId);
#else
        LogEditor("ShowKeyboard");
        onSuccess?.Invoke(new ShowKeyboardResult { errMsg = Messages.SHOW_KEYBOARD_OK });
#endif
    }

    /// <summary>
    /// 키보드 숨김
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void HideKeyboard(Action<HideKeyboardResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("HideKeyboard");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new HideKeyboardResult { errMsg = Messages.HIDE_KEYBOARD_OK }
        );
        TudadaSDK_HideKeyboard(callbackId);
#else
        LogEditor("HideKeyboard");
        onSuccess?.Invoke(new HideKeyboardResult { errMsg = Messages.HIDE_KEYBOARD_OK });
#endif
    }

    /// <summary>
    /// 클립보드에 데이터 저장
    /// </summary>
    /// <param name="data">저장할 데이터</param>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void SetClipboardData(string data, Action<SetClipboardDataResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("SetClipboardData");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new SetClipboardDataResult { errMsg = Messages.SET_CLIPBOARD_DATA_OK }
        );
        TudadaSDK_SetClipboardData(data, callbackId);
#else
        LogEditor("SetClipboardData");
        GUIUtility.systemCopyBuffer = data;
        onSuccess?.Invoke(new SetClipboardDataResult { errMsg = Messages.SET_CLIPBOARD_DATA_OK });
#endif
    }

    /// <summary>
    /// 클립보드에서 데이터 조회
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void GetClipboardData(Action<GetClipboardDataResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("GetClipboardData");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new GetClipboardDataResult { errMsg = Messages.GET_CLIPBOARD_DATA_OK, data = "" }
        );
        TudadaSDK_GetClipboardData(callbackId);
#else
        LogEditor("GetClipboardData");
        onSuccess?.Invoke(new GetClipboardDataResult { errMsg = Messages.GET_CLIPBOARD_DATA_OK, data = GUIUtility.systemCopyBuffer });
#endif
    }

    #endregion

    #region Public API - Lifecycle

    /// <summary>
    /// 런치 옵션 조회 (동기)
    /// </summary>
    /// <returns>런치 옵션 정보</returns>
    public Tudada.TudadaLaunchOptions GetLaunchOptions()
    {
        LogDebug("GetLaunchOptions");

#if UNITY_WEBGL && !UNITY_EDITOR
        string json = TudadaSDK_GetLaunchOptions();
        return ParseJsonOrDefault(json, CreateDefaultLaunchOptions());
#else
        LogEditor("GetLaunchOptions");
        return CreateDefaultLaunchOptions();
#endif
    }

    /// <summary>
    /// 미니프로그램 종료
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void ExitMiniProgram(Action<ExitMiniProgramResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("ExitMiniProgram");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new ExitMiniProgramResult { errMsg = Messages.EXIT_MINI_PROGRAM_OK }
        );
        TudadaSDK_ExitMiniProgram(callbackId);
#else
        LogEditor("ExitMiniProgram - 에디터에서는 종료되지 않습니다");
        onSuccess?.Invoke(new ExitMiniProgramResult { errMsg = Messages.EXIT_MINI_PROGRAM_OK });
#endif
    }

    /// <summary>
    /// 미니프로그램 재시작
    /// </summary>
    /// <param name="onSuccess">성공 콜백</param>
    /// <param name="onFail">실패 콜백</param>
    public void RestartMiniProgram(Action<RestartMiniProgramResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("RestartMiniProgram");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new RestartMiniProgramResult { errMsg = Messages.RESTART_MINI_PROGRAM_OK }
        );
        TudadaSDK_RestartMiniProgram(callbackId);
#else
        LogEditor("RestartMiniProgram - 에디터에서는 재시작되지 않습니다");
        onSuccess?.Invoke(new RestartMiniProgramResult { errMsg = Messages.RESTART_MINI_PROGRAM_OK });
#endif
    }

    #endregion

    #region Public API - Accelerometer

    /// <summary>
    /// 가속도계 변화 이벤트
    ///
    /// 센싱이 시작되면 가속도 변화 데이터를 수신합니다.
    /// StartAccelerometerSensor() 호출 전에 등록해도 됩니다.
    /// </summary>
    public event Action<AccelerometerChangeResult> OnAccelerometerChange
    {
        add { TudadaAccelerometerSensor.GetOrCreateInstance().OnChange += value; }
        remove { TudadaAccelerometerSensor.GetOrCreateInstance().OnChange -= value; }
    }

    /// <summary>
    /// 가속도계 센싱 시작
    /// </summary>
    /// <param name="sensitivity">민감도 (기본값: normal)</param>
    /// <param name="onSuccess">센싱 시작 성공 콜백</param>
    /// <param name="onFail">센싱 시작 실패 콜백</param>
    public void StartAccelerometerSensor(
        AccelerometerSensitivity sensitivity = AccelerometerSensitivity.normal,
        Action<StartAccelerometerSensorResult> onSuccess = null,
        Action<string> onFail = null)
    {
        LogDebug($"StartAccelerometerSensor - sensitivity: {sensitivity}");

        var sensor = TudadaAccelerometerSensor.GetOrCreateInstance();

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new StartAccelerometerSensorResult { errMsg = Messages.ACCELEROMETER_START_OK }
        );
        TudadaSDK_StartAccelerometerSensor(sensitivity.ToString(), callbackId);
#else
        LogEditor("AccelerometerSensor started", $"sensitivity: {sensitivity}");
        sensor.SetListening(true, sensitivity);
        onSuccess?.Invoke(new StartAccelerometerSensorResult { errMsg = Messages.ACCELEROMETER_START_OK });
#endif
    }

    /// <summary>
    /// 가속도계 센싱 중지
    /// </summary>
    /// <param name="onSuccess">센싱 중지 성공 콜백</param>
    /// <param name="onFail">센싱 중지 실패 콜백</param>
    public void StopAccelerometerSensor(
        Action<StopAccelerometerSensorResult> onSuccess = null,
        Action<string> onFail = null)
    {
        LogDebug("StopAccelerometerSensor");

        var sensor = TudadaAccelerometerSensor.GetInstance();
        if (sensor == null || !sensor.IsListening)
        {
            LogDebug("Accelerometer is not listening");
            onSuccess?.Invoke(new StopAccelerometerSensorResult { errMsg = Messages.ACCELEROMETER_STOP_OK });
            return;
        }

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new StopAccelerometerSensorResult { errMsg = Messages.ACCELEROMETER_STOP_OK }
        );
        TudadaSDK_StopAccelerometerSensor(callbackId);
#else
        LogEditor("AccelerometerSensor stopped");
        sensor.SetListening(false);
        onSuccess?.Invoke(new StopAccelerometerSensorResult { errMsg = Messages.ACCELEROMETER_STOP_OK });
#endif
    }

    #endregion

    #region Public API - CheckFeature

    /// <summary>
    /// Unity PascalCase 메서드명 → JS SDK camelCase API명 매핑
    /// CheckFeature에서 Unity 개발자가 PascalCase로 API 이름을 넘겨도 올바르게 동작하도록 변환합니다.
    /// </summary>
    private static readonly Dictionary<string, string> _apiNameMap = new Dictionary<string, string>
    {
        // Auth
        { "Login", "login" },
        { "CheckSession", "checkSession" },
        { "GetUserInfo", "getUserInfo" },
        // Storage
        { "SetStorage", "setStorage" },
        { "SetStorageSync", "setStorageSync" },
        { "GetStorage", "getStorage" },
        { "GetStorageSync", "getStorageSync" },
        { "RemoveStorage", "removeStorage" },
        { "RemoveStorageSync", "removeStorageSync" },
        { "ClearStorage", "clearStorage" },
        { "ClearStorageSync", "clearStorageSync" },
        { "GetStorageInfoSync", "getStorageInfoSync" },
        { "TudadaStoreSave", "tudadaStoreSave" },
        { "TudadaStoreGet", "tudadaStoreGet" },
        // System
        { "GetSystemInfo", "getSystemInfo" },
        { "GetWindowInfo", "getWindowInfo" },
        { "GetAppBaseInfo", "getAppBaseInfo" },
        { "GetDeviceInfo", "getDeviceInfo" },
        { "GetMenuButtonBoundingClientRect", "getMenuButtonBoundingClientRect" },
        // Device
        { "VibrateShort", "vibrateShort" },
        { "VibrateLong", "vibrateLong" },
        { "ShowKeyboard", "showKeyboard" },
        { "HideKeyboard", "hideKeyboard" },
        { "SetClipboardData", "setClipboardData" },
        { "GetClipboardData", "getClipboardData" },
        // Lifecycle
        { "ExitMiniProgram", "exitMiniProgram" },
        { "RestartMiniProgram", "restartMiniProgram" },
        // Accelerometer
        { "StartAccelerometerSensor", "startAccelerometerSensor" },
        { "StopAccelerometerSensor", "stopAccelerometerSensor" },
        // Ad
        { "CreateRewardedVideoAd", "createRewardedVideoAd" },
        { "ShowRewardedAd", "showRewardedAd" },
        { "GetAvailableBannerIds", "getAvailableBannerIds" },
        { "GetBanner", "getBanner" },
        { "RunBannerAction", "runBannerAction" },
        { "ShareForReward", "shareForReward" },
        // Launch
        { "GetLaunchOptions", "getLaunchOptions" },
        // CheckFeature
        { "CheckFeature", "checkFeature" },
    };

    /// <summary>
    /// Unity API 이름을 JS SDK API 이름으로 변환합니다.
    /// 매핑에 없으면 원본을 그대로 반환합니다 (이미 camelCase인 경우).
    /// </summary>
    private static string ResolveJsApiName(string apiName)
    {
        return _apiNameMap.TryGetValue(apiName, out var jsName) ? jsName : apiName;
    }

    /// <summary>
    /// 기능 지원 여부 확인
    ///
    /// SDK에 API가 존재하는지 먼저 확인하고,
    /// 존재하면 플랫폼에 실제 지원 가능 여부를 질의합니다.
    /// Unity PascalCase 이름과 JS SDK camelCase 이름 모두 사용 가능합니다.
    /// </summary>
    /// <param name="apiName">확인할 API 이름 (PascalCase 또는 camelCase)</param>
    /// <param name="onSuccess">기능 지원됨</param>
    /// <param name="onFail">기능 미지원 또는 오류</param>
    public void CheckFeature(
        string apiName,
        Action<CheckFeatureResult> onSuccess = null,
        Action<CheckFeatureResult> onFail = null)
    {
        string jsApiName = ResolveJsApiName(apiName);
        LogDebug($"CheckFeature - apiName: {apiName} → jsApiName: {jsApiName}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCheckFeatureCallbacks(onSuccess, onFail);
        TudadaSDK_CheckFeature(jsApiName, callbackId);
#else
        LogEditor("CheckFeature", $"apiName: {apiName} → jsApiName: {jsApiName}");
        // 에디터에서는 항상 지원됨으로 응답
        onSuccess?.Invoke(new CheckFeatureResult
        {
            errMsg = Messages.CHECK_FEATURE_OK,
            supported = true,
            status = "supported",
            apiName = jsApiName
        });
#endif
    }

    #endregion

    #region Public API - Ad

    /// <summary>
    /// 보상형 비디오 광고 인스턴스 생성
    /// </summary>
    /// <param name="adUnitId">광고 단위 ID</param>
    /// <returns>광고 인스턴스</returns>
    [System.Obsolete("wx 호환 레거시 API입니다. ShowRewardedAd() 를 사용하세요. 다음 버전에서 제거될 예정입니다.")]
    public TudadaRewardedVideoAd CreateRewardedVideoAd(string adUnitId)
    {
        LogDebug($"CreateRewardedVideoAd - adUnitId: {adUnitId}");

        var ad = new TudadaRewardedVideoAd(adUnitId);

#if UNITY_WEBGL && !UNITY_EDITOR
        TudadaSDK_CreateRewardedVideoAd(adUnitId, ad.InstanceId);
#else
        LogEditor("RewardedVideoAd created", $"instanceId: {ad.InstanceId}");
#endif

        return ad;
    }

    /// <summary>
    /// 보상형 광고 통합 표시 (load + show 일괄 처리, adUnitId 기반 video 폴백)
    /// </summary>
    /// <param name="adUnitId">광고 단위 ID</param>
    /// <param name="onSuccess">성공 콜백 (isEnded: 광고 시청 완료 여부)</param>
    /// <param name="onFail">실패 콜백</param>
    public void ShowRewardedAd(string adUnitId, Action<ShowRewardedAdResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"ShowRewardedAd - adUnitId: {adUnitId}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new ShowRewardedAdResult { isEnded = false }
        );
        TudadaSDK_ShowRewardedAd("", adUnitId, callbackId);
#else
        LogEditor("ShowRewardedAd", $"adUnitId: {adUnitId}");
        onSuccess?.Invoke(new ShowRewardedAdResult { isEnded = true });
#endif
    }

    /// <summary>
    /// 보상형 리워드 통합 실행 (슬롯 기반).
    /// 슬롯의 action(REWARD_VIDEO/BANNER/SHARE)에 따라 플랫폼이 디스패치합니다.
    /// 슬롯은 GetRewardedAdSlots / GetRewardedAdSlot 로 획득합니다.
    /// </summary>
    /// <param name="slot">실행할 리워드 슬롯</param>
    /// <param name="onSuccess">성공 콜백 (isEnded: 리워드 완료 여부)</param>
    /// <param name="onFail">실패 콜백</param>
    public void ShowRewardedAd(RewardedAdSlot slot, Action<ShowRewardedAdResult> onSuccess = null, Action<string> onFail = null)
    {
        string slotId = slot?.slotId ?? "";
        LogDebug($"ShowRewardedAd - slotId: {slotId}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new ShowRewardedAdResult { isEnded = false }
        );
        TudadaSDK_ShowRewardedAd(slotId, "", callbackId);
#else
        LogEditor("ShowRewardedAd", $"slotId: {slotId}");
        onSuccess?.Invoke(new ShowRewardedAdResult { isEnded = true });
#endif
    }

    // ==========================================================================
    // 통합 리워드 슬롯 (GetRewardedAdSlots / GetRewardedAdSlot)
    //
    // 각 슬롯은 slotId, action, slotData(렌더용)로 구성. 게임은 slotData로 렌더링하고
    // 사용자 탭 시 ShowRewardedAd(slot)으로 실행합니다.
    // ==========================================================================

    /// <summary>
    /// 활성 리워드 슬롯 전체 목록 조회
    /// </summary>
    /// <param name="onSuccess">성공 콜백 (slots: 리워드 슬롯 배열)</param>
    /// <param name="onFail">실패 콜백</param>
    public void GetRewardedAdSlots(Action<GetRewardedAdSlotsResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("GetRewardedAdSlots called");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new GetRewardedAdSlotsResult { slots = new RewardedAdSlot[0] }
        );
        TudadaSDK_GetRewardedAdSlots(callbackId);
#else
        LogEditor("GetRewardedAdSlots", "");
        onSuccess?.Invoke(new GetRewardedAdSlotsResult { slots = new RewardedAdSlot[0] });
#endif
    }

    /// <summary>
    /// slotId로 단일 리워드 슬롯 조회
    /// </summary>
    /// <param name="slotId">조회할 슬롯 ID</param>
    /// <param name="onSuccess">성공 콜백 (slot: 리워드 슬롯)</param>
    /// <param name="onFail">실패 콜백. 슬롯이 없을 때도 호출</param>
    public void GetRewardedAdSlot(string slotId, Action<GetRewardedAdSlotResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"GetRewardedAdSlot - slotId: {slotId}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new GetRewardedAdSlotResult { slot = null }
        );
        TudadaSDK_GetRewardedAdSlot(slotId, callbackId);
#else
        LogEditor("GetRewardedAdSlot", $"slotId: {slotId}");
        onSuccess?.Invoke(new GetRewardedAdSlotResult
        {
            slot = new RewardedAdSlot { slotId = slotId, action = "REWARD_VIDEO", slotData = null }
        });
#endif
    }

    // ==========================================================================
    // Banner Ad (슬롯 모델 — bannerId == 슬롯 이름) — [Obsolete] 통합 리워드 슬롯으로 대체
    //
    // 게임이 정의한 슬롯에 광고가 매칭되면 imageUrl을 받아 자체 렌더링하고,
    // 사용자 탭 시 RunBannerAction을 호출해 액션 처리를 SDK에 위임합니다.
    // 보상 등 후처리는 게임 내부에서 수행합니다.
    // ==========================================================================

    /// <summary>
    /// 활성 배너 ID(=슬롯 이름) 목록 조회
    /// </summary>
    /// <param name="onSuccess">성공 콜백 (bannerIds: 활성 슬롯 이름 배열)</param>
    /// <param name="onFail">실패 콜백</param>
    [Obsolete("GetRewardedAdSlots 를 사용하세요.")]
    public void GetAvailableBannerIds(Action<GetAvailableBannerIdsResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug("GetAvailableBannerIds called");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new GetAvailableBannerIdsResult { bannerIds = new string[0] }
        );
        TudadaSDK_GetAvailableBannerIds(callbackId);
#else
        LogEditor("GetAvailableBannerIds", "");
        onSuccess?.Invoke(new GetAvailableBannerIdsResult { bannerIds = new string[0] });
#endif
    }

    /// <summary>
    /// 슬롯 이름(bannerId)으로 배너 데이터 조회
    /// </summary>
    /// <param name="bannerId">슬롯 이름을 겸하는 배너 ID</param>
    /// <param name="onSuccess">성공 콜백 (banner: imageUrl 등)</param>
    /// <param name="onFail">실패 콜백. 슬롯에 매칭된 광고 없을 때도 호출</param>
    [Obsolete("GetRewardedAdSlot 를 사용하세요.")]
    public void GetBanner(string bannerId, Action<GetBannerResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"GetBanner - bannerId: {bannerId}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new GetBannerResult { banner = null }
        );
        TudadaSDK_GetBanner(bannerId, callbackId);
#else
        LogEditor("GetBanner", $"bannerId: {bannerId}");
        onSuccess?.Invoke(new GetBannerResult
        {
            banner = new Banner { bannerId = bannerId, imageUrl = "" }
        });
#endif
    }

    /// <summary>
    /// 배너 액션 실행 (사용자 탭 시 호출).
    /// SDK가 액션을 처리하고 success/fail 신호만 통지합니다.
    /// </summary>
    /// <param name="bannerId">슬롯 이름을 겸하는 배너 ID</param>
    /// <param name="onSuccess">성공 콜백 — 게임이 보상 등 후처리 수행</param>
    /// <param name="onFail">실패 콜백</param>
    [Obsolete("ShowRewardedAd(slot) (action=BANNER) 를 사용하세요.")]
    public void RunBannerAction(string bannerId, Action onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"RunBannerAction - bannerId: {bannerId}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            _ => onSuccess?.Invoke(),
            onFail
        );
        TudadaSDK_RunBannerAction(bannerId, callbackId);
#else
        LogEditor("RunBannerAction", $"bannerId: {bannerId}");
        onSuccess?.Invoke();
#endif
    }

    /// <summary>
    /// 공유를 통한 보상 요청
    /// </summary>
    /// <param name="url">공유 URL (선택)</param>
    /// <param name="onSuccess">성공 콜백 (rewarded: 보상 지급 여부)</param>
    /// <param name="onFail">실패 콜백</param>
    [Obsolete("ShowRewardedAd(slot) (action=SHARE) 를 사용하세요.")]
    public void ShareForReward(string url = null, Action<ShareForRewardResult> onSuccess = null, Action<string> onFail = null)
    {
        LogDebug($"ShareForReward - url: {url ?? "(null)"}");

#if UNITY_WEBGL && !UNITY_EDITOR
        int callbackId = RegisterCallbacks(
            onSuccess,
            onFail,
            new ShareForRewardResult { rewarded = false }
        );
        TudadaSDK_ShareForReward(url ?? "", callbackId);
#else
        LogEditor("ShareForReward", $"url: {url ?? "(null)"}");
        onSuccess?.Invoke(new ShareForRewardResult { rewarded = true });
#endif
    }

    #endregion

    #region jslib Callbacks - Lifecycle Events

    /// <summary>
    /// jslib에서 onShow 이벤트 수신
    /// </summary>
    public void OnShowCallback(string data)
    {
        LogDebug($"OnShowCallback: {data}");
        InvokeEventSafe(OnShow, data, new OnShowResult(), "OnShowCallback");
    }

    /// <summary>
    /// jslib에서 onHide 이벤트 수신
    /// </summary>
    public void OnHideCallback(string data)
    {
        LogDebug($"OnHideCallback: {data}");
        InvokeEventSafe(OnHide, data, new OnHideResult(), "OnHideCallback");
    }

    #endregion

    #region jslib Callbacks - Keyboard Events

    /// <summary>
    /// jslib에서 onKeyboardInput 이벤트 수신
    /// </summary>
    public void OnKeyboardInputCallback(string data)
    {
        LogDebug($"OnKeyboardInputCallback: {data}");
        InvokeEventSafe(OnKeyboardInput, data, new KeyboardInputResult(), "OnKeyboardInputCallback");
    }

    /// <summary>
    /// jslib에서 onKeyboardConfirm 이벤트 수신
    /// </summary>
    public void OnKeyboardConfirmCallback(string data)
    {
        LogDebug($"OnKeyboardConfirmCallback: {data}");
        InvokeEventSafe(OnKeyboardConfirm, data, new KeyboardConfirmResult(), "OnKeyboardConfirmCallback");
    }

    /// <summary>
    /// jslib에서 onKeyboardComplete 이벤트 수신
    /// </summary>
    public void OnKeyboardCompleteCallback(string data)
    {
        LogDebug($"OnKeyboardCompleteCallback: {data}");
        InvokeEventSafe(OnKeyboardComplete, data, new KeyboardCompleteResult(), "OnKeyboardCompleteCallback");
    }

    #endregion

    #region jslib Callbacks - Storage

    public void OnSetStorageSuccess(string data) => HandleCallback(data, true);
    public void OnSetStorageFail(string data) => HandleCallback(data, false);
    public void OnGetStorageSuccess(string data) => HandleCallback(data, true);
    public void OnGetStorageFail(string data) => HandleCallback(data, false);
    public void OnRemoveStorageSuccess(string data) => HandleCallback(data, true);
    public void OnRemoveStorageFail(string data) => HandleCallback(data, false);
    public void OnClearStorageSuccess(string data) => HandleCallback(data, true);
    public void OnClearStorageFail(string data) => HandleCallback(data, false);
    public void OnTudadaStoreSaveSuccess(string data) => HandleCallback(data, true);
    public void OnTudadaStoreSaveFail(string data) => HandleCallback(data, false);
    public void OnTudadaStoreGetSuccess(string data) => HandleCallback(data, true);
    public void OnTudadaStoreGetFail(string data) => HandleCallback(data, false);

    #endregion

    #region jslib Callbacks - System

    public void OnGetSystemInfoSuccess(string data) => HandleCallback(data, true);
    public void OnGetSystemInfoFail(string data) => HandleCallback(data, false);

    #endregion

    #region jslib Callbacks - Auth

    public void OnLoginSuccess(string data) => HandleCallback(data, true);
    public void OnLoginFail(string data) => HandleCallback(data, false);
    public void OnCheckSessionSuccess(string data) => HandleCallback(data, true);
    public void OnCheckSessionFail(string data) => HandleCallback(data, false);
    public void OnGetUserInfoSuccess(string data) => HandleCallback(data, true);
    public void OnGetUserInfoFail(string data) => HandleCallback(data, false);

    #endregion

    #region jslib Callbacks - Device

    public void OnVibrateShortSuccess(string data) => HandleCallback(data, true);
    public void OnVibrateShortFail(string data) => HandleCallback(data, false);
    public void OnVibrateLongSuccess(string data) => HandleCallback(data, true);
    public void OnVibrateLongFail(string data) => HandleCallback(data, false);
    public void OnShowKeyboardSuccess(string data) => HandleCallback(data, true);
    public void OnShowKeyboardFail(string data) => HandleCallback(data, false);
    public void OnHideKeyboardSuccess(string data) => HandleCallback(data, true);
    public void OnHideKeyboardFail(string data) => HandleCallback(data, false);
    public void OnSetClipboardDataSuccess(string data) => HandleCallback(data, true);
    public void OnSetClipboardDataFail(string data) => HandleCallback(data, false);
    public void OnGetClipboardDataSuccess(string data) => HandleCallback(data, true);
    public void OnGetClipboardDataFail(string data) => HandleCallback(data, false);

    #endregion

    #region jslib Callbacks - Lifecycle

    public void OnExitMiniProgramSuccess(string data) => HandleCallback(data, true);
    public void OnExitMiniProgramFail(string data) => HandleCallback(data, false);
    public void OnRestartMiniProgramSuccess(string data) => HandleCallback(data, true);
    public void OnRestartMiniProgramFail(string data) => HandleCallback(data, false);

    #endregion

    #region jslib Callbacks - Accelerometer

    public void OnStartAccelerometerSensorSuccess(string data)
    {
        HandleCallback(data, true);
        var sensor = TudadaAccelerometerSensor.GetInstance();
        sensor?.SetListening(true);
    }

    public void OnStartAccelerometerSensorFail(string data) => HandleCallback(data, false);

    public void OnStopAccelerometerSensorSuccess(string data)
    {
        HandleCallback(data, true);
        var sensor = TudadaAccelerometerSensor.GetInstance();
        sensor?.SetListening(false);
    }

    public void OnStopAccelerometerSensorFail(string data) => HandleCallback(data, false);

    /// <summary>
    /// jslib에서 가속도 변화 이벤트 수신
    /// </summary>
    public void OnAccelerometerChangeCallback(string data)
    {
        try
        {
            var result = JsonUtility.FromJson<AccelerometerChangeResult>(data);
            var sensor = TudadaAccelerometerSensor.GetInstance();
            sensor?.TriggerOnChange(result ?? new AccelerometerChangeResult());
        }
        catch (Exception e)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} OnAccelerometerChangeCallback parse error: {e.Message}");
        }
    }

    #endregion

    #region jslib Callbacks - CheckFeature

    public void OnCheckFeatureSuccess(string data)
    {
        LogDebug($"OnCheckFeatureSuccess: {data}");
        HandleCheckFeatureCallback(data, true);
    }

    public void OnCheckFeatureFail(string data)
    {
        LogDebug($"OnCheckFeatureFail: {data}");
        HandleCheckFeatureCallback(data, false);
    }

    #endregion

    #region jslib Callbacks - Ad

    public void OnAdLoadSuccess(string data) => HandleCallback(data, true);
    public void OnAdLoadFail(string data) => HandleCallback(data, false);
    public void OnAdShowSuccess(string data) => HandleCallback(data, true);
    public void OnAdShowFail(string data) => HandleCallback(data, false);
    public void OnShowRewardedAdSuccess(string data) => HandleCallback(data, true);
    public void OnShowRewardedAdFail(string data) => HandleCallback(data, false);
    public void OnGetRewardedAdSlotsSuccess(string data) => HandleCallback(data, true);
    public void OnGetRewardedAdSlotsFail(string data) => HandleCallback(data, false);
    public void OnGetRewardedAdSlotSuccess(string data) => HandleCallback(data, true);
    public void OnGetRewardedAdSlotFail(string data) => HandleCallback(data, false);
    public void OnGetAvailableBannerIdsSuccess(string data) => HandleCallback(data, true);
    public void OnGetAvailableBannerIdsFail(string data) => HandleCallback(data, false);
    public void OnGetBannerSuccess(string data) => HandleCallback(data, true);
    public void OnGetBannerFail(string data) => HandleCallback(data, false);
    public void OnRunBannerActionSuccess(string data) => HandleCallback(data, true);
    public void OnRunBannerActionFail(string data) => HandleCallback(data, false);
    public void OnShareForRewardSuccess(string data) => HandleCallback(data, true);
    public void OnShareForRewardFail(string data) => HandleCallback(data, false);

    /// <summary>
    /// 광고 이벤트 데이터 (JSON 파싱용)
    /// </summary>
    [Serializable]
    private class AdEventData
    {
        public int adInstanceId;
        public string errMsg;
        public int errCode;
        public bool isEnded;
    }

    /// <summary>
    /// jslib에서 광고 로드 완료 이벤트 수신
    /// </summary>
    public void OnAdLoad(string data)
    {
        LogDebug($"OnAdLoad: {data}");

        try
        {
            var eventData = JsonUtility.FromJson<AdEventData>(data);
#pragma warning disable CS0618 // TudadaRewardedVideoAd는 @deprecated이나 SDK 내부 이벤트 디스패치에 필요
            var ad = TudadaRewardedVideoAd.GetInstance(eventData.adInstanceId);
            ad?.TriggerOnLoad();
#pragma warning restore CS0618
        }
        catch (Exception e)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} OnAdLoad parse error: {e.Message}");
        }
    }

    /// <summary>
    /// jslib에서 광고 에러 이벤트 수신
    /// </summary>
    public void OnAdError(string data)
    {
        LogDebug($"OnAdError: {data}");

        try
        {
            var eventData = JsonUtility.FromJson<AdEventData>(data);
#pragma warning disable CS0618 // TudadaRewardedVideoAd는 @deprecated이나 SDK 내부 이벤트 디스패치에 필요
            var ad = TudadaRewardedVideoAd.GetInstance(eventData.adInstanceId);
            ad?.TriggerOnError(new AdErrorResult
            {
                errMsg = eventData.errMsg,
                errCode = eventData.errCode
            });
#pragma warning restore CS0618
        }
        catch (Exception e)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} OnAdError parse error: {e.Message}");
        }
    }

    /// <summary>
    /// jslib에서 광고 닫기 이벤트 수신
    /// </summary>
    public void OnAdClose(string data)
    {
        LogDebug($"OnAdClose: {data}");

        try
        {
            var eventData = JsonUtility.FromJson<AdEventData>(data);
#pragma warning disable CS0618 // TudadaRewardedVideoAd는 @deprecated이나 SDK 내부 이벤트 디스패치에 필요
            var ad = TudadaRewardedVideoAd.GetInstance(eventData.adInstanceId);
            ad?.TriggerOnClose(new AdCloseResult
            {
                isEnded = eventData.isEnded
            });
#pragma warning restore CS0618
        }
        catch (Exception e)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} OnAdClose parse error: {e.Message}");
        }
    }

    #endregion

    #region Helper Methods - Logging

    private void LogDebug(string message)
    {
        Debug.Log($"{Constants.LOG_PREFIX} {message}");
    }

    private void LogEditor(string methodName, string detail = null)
    {
        var message = string.IsNullOrEmpty(detail)
            ? $"{Constants.LOG_PREFIX} {Constants.EDITOR_LOG_SUFFIX} {methodName}"
            : $"{Constants.LOG_PREFIX} {Constants.EDITOR_LOG_SUFFIX} {methodName}: {detail}";
        Debug.Log(message);
    }

    #endregion

    #region Helper Methods - JSON Parsing

    private T ParseJsonOrDefault<T>(string json, T defaultValue) where T : class
    {
        try
        {
            return JsonUtility.FromJson<T>(json) ?? defaultValue;
        }
        catch
        {
            return defaultValue;
        }
    }

    #endregion

    #region Helper Methods - Event Invocation

    private void InvokeEventSafe<T>(TudadaEventHandler<T> eventHandler, string data, T defaultValue, string eventName) where T : class
    {
        try
        {
            var result = JsonUtility.FromJson<T>(data);
            eventHandler?.Invoke(result ?? defaultValue);
        }
        catch (Exception e)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} {eventName} parse error: {e.Message}");
            eventHandler?.Invoke(defaultValue);
        }
    }

    #endregion

    #region Helper Methods - Editor Storage

    private void ExecuteEditorStorage(Action operation, Action onSuccess, Action<string> onFail, string methodName, string key = null)
    {
        try
        {
            operation?.Invoke();
            var logDetail = key != null ? key : null;
            LogEditor(methodName, logDetail);
            onSuccess?.Invoke();
        }
        catch (Exception e)
        {
            Debug.LogError($"{Constants.LOG_PREFIX} {Constants.EDITOR_LOG_SUFFIX} {methodName} error: {e.Message}");
            onFail?.Invoke(e.Message);
        }
    }

    #endregion

    #region Helper Methods - Default Values

    private StorageInfo CreateDefaultStorageInfo()
    {
        return new StorageInfo
        {
            keys = new string[0],
            currentSize = 0,
            limitSize = Constants.DEFAULT_STORAGE_LIMIT_KB
        };
    }

    private Tudada.SystemInfo CreateDefaultSystemInfo()
    {
        return new Tudada.SystemInfo
        {
            SDKVersion = Constants.DEFAULT_SDK_VERSION,
            version = Constants.DEFAULT_SDK_VERSION,
            enableDebug = true,
            fontSizeSetting = Constants.DEFAULT_FONT_SIZE,
            host = new HostInfo { appId = "" },
            environment = "prod",
            servicePlatform = "house",
            pixelRatio = 1,
            screenWidth = Screen.width,
            screenHeight = Screen.height,
            windowWidth = Screen.width,
            windowHeight = Screen.height,
            statusBarHeight = 0,
            safeArea = CreateDefaultSafeArea(),
            brand = "Unity",
            model = "Editor",
            system = Application.platform.ToString(),
            benchmarkLevel = 0,
            language = Application.systemLanguage.ToString(),
            theme = "light",
            deviceOrientation = Screen.orientation.ToString(),
            platform = "devtools"
        };
    }

    private WindowInfo CreateDefaultWindowInfo()
    {
        return new WindowInfo
        {
            pixelRatio = 1,
            screenWidth = Screen.width,
            screenHeight = Screen.height,
            windowWidth = Screen.width,
            windowHeight = Screen.height,
            statusBarHeight = 0,
            safeArea = CreateDefaultSafeArea(),
            screenTop = 0
        };
    }

    private AppBaseInfo CreateDefaultAppBaseInfo()
    {
        return new AppBaseInfo
        {
            SDKVersion = Constants.DEFAULT_SDK_VERSION,
            version = Constants.DEFAULT_SDK_VERSION,
            enableDebug = true,
            fontSizeSetting = Constants.DEFAULT_FONT_SIZE,
            host = new HostInfo { appId = "" },
            environment = "prod",
            servicePlatform = "house",
            theme = "light",
            language = Application.systemLanguage.ToString()
        };
    }

    private DeviceInfo CreateDefaultDeviceInfo()
    {
        return new DeviceInfo
        {
            brand = "Unity",
            model = "Editor",
            system = Application.platform.ToString(),
            benchmarkLevel = 0,
            platform = "devtools"
        };
    }

    private MenuButtonRect CreateDefaultMenuButtonRect()
    {
        return new MenuButtonRect
        {
            width = Constants.DEFAULT_MENU_BUTTON_WIDTH,
            height = Constants.DEFAULT_MENU_BUTTON_HEIGHT,
            top = Constants.DEFAULT_MENU_BUTTON_TOP,
            right = Screen.width - Constants.DEFAULT_MENU_BUTTON_RIGHT_MARGIN,
            bottom = Constants.DEFAULT_MENU_BUTTON_TOP + Constants.DEFAULT_MENU_BUTTON_HEIGHT,
            left = Screen.width - Constants.DEFAULT_MENU_BUTTON_LEFT_MARGIN
        };
    }

    private SafeArea CreateDefaultSafeArea()
    {
        return new SafeArea
        {
            left = 0,
            right = Screen.width,
            top = 0,
            bottom = Screen.height,
            width = Screen.width,
            height = Screen.height
        };
    }

    private Tudada.TudadaLaunchOptions CreateDefaultLaunchOptions()
    {
        return new Tudada.TudadaLaunchOptions
        {
            query = "{}",
            referrerInfo = null
        };
    }

    private LoginResult CreateMockLoginResult()
    {
        return new LoginResult
        {
            errMsg = Messages.LOGIN_OK,
            code = "mock_login_code_" + Guid.NewGuid().ToString().Substring(0, 8),
            userId = "mock_user_" + Guid.NewGuid().ToString().Substring(0, 8),
            userInfoPayload = "attribute=%7B%7D&authDate=" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
                + "&gameUserId=mock_user&name=mock_name&profileImageUrl=",
            userInfoSignature = "mock_signature_" + Guid.NewGuid().ToString("N").Substring(0, 16)
        };
    }

    private GetUserInfoResult CreateMockGetUserInfoResult()
    {
        return new GetUserInfoResult
        {
            errMsg = Messages.GET_USER_INFO_OK,
            userInfo = new UserInfo
            {
                nickName = "TestUser",
                avatarUrl = "",
                language = "KO",
                gender = 0,
                country = "",
                city = "",
                province = ""
            },
            userInfoPayload = "attribute=%7B%7D&authDate=" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
                + "&gameUserId=mock_user&name=TestUser&profileImageUrl=",
            userInfoSignature = "mock_signature_" + Guid.NewGuid().ToString("N").Substring(0, 16)
        };
    }

    #endregion
}

#region Event Result Types

namespace Tudada
{
    /// <summary>
    /// onShow 이벤트 결과
    /// </summary>
    [Serializable]
    public class OnShowResult
    {
        /// <summary>
        /// 앱 실행 경로
        /// </summary>
        public string path;

        /// <summary>
        /// 앱 실행 씬
        /// </summary>
        public int scene;

        /// <summary>
        /// 쿼리 파라미터
        /// </summary>
        public string query;

        /// <summary>
        /// referrerInfo
        /// </summary>
        public string referrerInfo;
    }

    /// <summary>
    /// onHide 이벤트 결과
    /// </summary>
    [Serializable]
    public class OnHideResult
    {
    }

    /// <summary>
    /// 키보드 입력 결과
    /// </summary>
    [Serializable]
    public class KeyboardInputResult
    {
        /// <summary>
        /// 입력된 값
        /// </summary>
        public string value;
    }

    /// <summary>
    /// 키보드 확인 결과
    /// </summary>
    [Serializable]
    public class KeyboardConfirmResult
    {
        /// <summary>
        /// 확인된 값
        /// </summary>
        public string value;
    }

    /// <summary>
    /// 키보드 완료 결과
    /// </summary>
    [Serializable]
    public class KeyboardCompleteResult
    {
        /// <summary>
        /// 최종 입력 값
        /// </summary>
        public string value;
    }
}

#endregion
