using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using RudderSdk.Core;
using RudderSdk.Core.Models.Inventory;
using RudderSdk.Core.Models.Leaderboards;
using RudderSdk.Core.Models.Player;
using RudderSdk.Core.Models.Storage;
using RudderSdk.Core.Models.Stores;
using RudderSdk.Unity;
using UnityEngine;

namespace LiveOpsExamples
{
  [Serializable]
  public class CozyIdentity
  {
    public string PlayerId;
    public string Nickname;
    public string DeviceId;
    public string ProjectKey;
    public string Region;
  }

  [Serializable]
  public class CozyWallet
  {
    public string Currency;
    public double Balance;
  }

  [Serializable]
  public class CozyTuning
  {
    public float RoundSeconds;
    public float PlayerSpeed;
    public float SpawnIntervalMs;
    public float CollectibleScore;
  }

  [Serializable]
  public class CozySave
  {
    public int BestScore;
    public int LastScore;
    public int RoundsPlayed;
    public int TotalCollected;
  }

  [Serializable]
  public class CozyLogEntry
  {
    public string Capability;
    public string Message;
    public string Detail;
    public DateTime Timestamp;
  }

  [Serializable]
  public class CozyShopOffer
  {
    public string StoreSlug;
    public string OfferId;
    public string Name;
    public double Price;
    public string Currency;
  }

  [Serializable]
  public class CozyBackpackItem
  {
    public string Slug;
    public string Name;
    public int Amount;
  }

  [Serializable]
  public class CozyLeaderboardEntry
  {
    public string PlayerName;
    public int Rank;
    public int Score;
  }

  [Serializable]
  public class CozyRoundResult
  {
    public int Score;
    public int Best;
    public int Collected;
    public List<CozyLeaderboardEntry> TopEntries;
  }

  [Serializable]
  public class CozyOfferInfo
  {
    public string StoreSlug;
    public string StoreName;
    public List<CozyShopOffer> Offers;
  }

  public enum CozyPhase
  {
    Booting,
    Connecting,
    Ready,
    Setup
  }

  public enum CozyModal
  {
    None,
    Shop,
    Backpack,
    Offer,
    RoundResult
  }

  public enum CozyToastTone
  {
    Info,
    Success,
    Error
  }

  /// <summary>
  /// Session/bootstrap hub for the Cozy Collector demo — the Unity port of the
  /// Phaser demo's liveops.ts + store.ts. Owns every SDK call: login, remote
  /// config (initial load, 10 s poll and scenario overrides), storage save,
  /// profile/wallets, store catalog, inventory, leaderboard and scenario effects.
  /// The UI layer binds to the public properties/events.
  /// </summary>
  public class CozyCollectorController : MonoBehaviour
  {
    private const int MaxLogEntries = 200;
    private const float ConfigPollSeconds = 10f;
    private const float NotificationAutoCompleteSeconds = 2.6f;

    [SerializeField] private GameWorld world;

    private readonly List<CozyWallet> _wallets = new List<CozyWallet>();
    private readonly List<CozyLogEntry> _logEntries = new List<CozyLogEntry>();
    private readonly List<CozyShopOffer> _shopOffers = new List<CozyShopOffer>();
    private readonly List<CozyBackpackItem> _backpackItems = new List<CozyBackpackItem>();
    private readonly CozyTuning _tuning = new CozyTuning
    {
      RoundSeconds = CozyCollectorConsts.DefaultRoundSeconds,
      PlayerSpeed = CozyCollectorConsts.DefaultPlayerSpeed,
      SpawnIntervalMs = CozyCollectorConsts.DefaultSpawnIntervalMs,
      CollectibleScore = CozyCollectorConsts.DefaultCollectibleScore
    };
    private readonly CozySave _save = new CozySave();

    private CozyPhase _phase = CozyPhase.Booting;
    private string _statusMessage = "Starting…";
    private string _setupError;
    private CozyIdentity _identity;
    private CozyModal _activeModal = CozyModal.None;
    private CozyRoundResult _roundResult;
    private CozyOfferInfo _currentOffer;
    private string _deviceId;
    private string _nickname;
    private StoreOfferSession _storeSession;
    private LeaderboardSession _leaderboardSession;
    private bool _scenarioHandlersRegistered;

    public CozyPhase Phase => _phase;
    public string StatusMessage => _statusMessage;
    public string SetupError => _setupError;
    public CozyIdentity Identity => _identity;
    public IReadOnlyList<CozyWallet> Wallets => _wallets;
    public CozyTuning Tuning => _tuning;
    public CozySave Save => _save;
    public IReadOnlyList<CozyLogEntry> LogEntries => _logEntries;
    public CozyModal ActiveModal => _activeModal;
    public IReadOnlyList<CozyShopOffer> ShopOffers => _shopOffers;
    public IReadOnlyList<CozyBackpackItem> BackpackItems => _backpackItems;
    public CozyRoundResult RoundResult => _roundResult;
    public CozyOfferInfo CurrentOffer => _currentOffer;

    public event Action PhaseChanged;
    public event Action ModalChanged;
    public event Action WalletsChanged;
    public event Action<string> TuningChanged;
    public event Action LogAdded;
    public event Action ShopChanged;
    public event Action BackpackChanged;
    public event Action RoundResultChanged;
    public event Action OfferChanged;
    public event Action<string, string, CozyToastTone> ToastRequested;

    // ---------------------------------------------------------------
    // Boot phase machine
    // ---------------------------------------------------------------

    private void Awake()
    {
      world.RoundFinished += OnRoundFinished;
    }

    private void Start()
    {
      _ = BootAsync();
    }

    private void OnDestroy()
    {
      world.RoundFinished -= OnRoundFinished;
      UnregisterScenarioHandlers();
    }

    private async Task BootAsync()
    {
      SetPhase(CozyPhase.Booting, "Starting…");

      try
      {
        await Rudder.Ready;
      }
      catch (Exception exception)
      {
        _setupError = "Rudder is not initialized. Add the Rudder component to the scene " +
                      "and assign a RudderConfiguration asset with a valid ProjectKey. " +
                      exception.Message;
        SetPhase(CozyPhase.Setup, "Setup required");
        Log(CozyCollectorConsts.CapabilitySystem, "Setup required", _setupError);
        return;
      }

      SetPhase(CozyPhase.Connecting, "Connecting to LiveOps…");
      RegisterScenarioHandlers();

      try
      {
        _deviceId = SystemInfo.deviceUniqueIdentifier;
        _nickname = GetOrCreateNickname(_deviceId);
        Log(CozyCollectorConsts.CapabilityAuth, "loginWithDevice", _nickname + " · " + ShortId(_deviceId));
        await Rudder.Client.Auth.LoginWithDeviceAsync(
          CozyCollectorConsts.Region,
          CozyCollectorConsts.Language,
          _nickname);

        await Rudder.Client.RemoteConfig.LoadAsync();
        RefreshTuning(false);

        await LoadSaveAsync();
        await RefreshProfileAsync();
        await RefreshStoresAsync();
        await RefreshInventoryAsync();

        try
        {
          await Rudder.Client.Scenario.TriggerAsync("player_login");
        }
        catch (Exception exception)
        {
          Log(CozyCollectorConsts.CapabilityScenario, "Trigger failed", exception.Message);
        }

        SetPhase(CozyPhase.Ready, "Connected");
        world.RestartRound();
        StartCoroutine(PollConfigLoop());
      }
      catch (Exception exception)
      {
        _setupError = exception.Message;
        SetPhase(CozyPhase.Setup, "Setup required");
        Log(CozyCollectorConsts.CapabilitySystem, "Login failed", exception.Message);
      }
    }

    // ---------------------------------------------------------------
    // Remote config
    // ---------------------------------------------------------------

    private IEnumerator PollConfigLoop()
    {
      var wait = new WaitForSeconds(ConfigPollSeconds);
      while (true)
      {
        yield return wait;
        _ = PollConfigAsync();
      }
    }

    private async Task PollConfigAsync()
    {
      try
      {
        await Rudder.Client.RemoteConfig.LoadAsync();
        RefreshTuning(true);
      }
      catch (Exception exception)
      {
        Log(CozyCollectorConsts.CapabilityConfig, "Config poll failed", exception.Message);
      }
    }

    private void RefreshTuning(bool announce)
    {
      var next = new CozyTuning
      {
        RoundSeconds = ReadConfigFloat(CozyCollectorConsts.ConfigRoundSeconds, CozyCollectorConsts.DefaultRoundSeconds),
        PlayerSpeed = ReadConfigFloat(CozyCollectorConsts.ConfigPlayerSpeed, CozyCollectorConsts.DefaultPlayerSpeed),
        SpawnIntervalMs = ReadConfigFloat(CozyCollectorConsts.ConfigSpawnIntervalMs, CozyCollectorConsts.DefaultSpawnIntervalMs),
        CollectibleScore = ReadConfigFloat(CozyCollectorConsts.ConfigCollectibleScore, CozyCollectorConsts.DefaultCollectibleScore)
      };

      var changedKeys = new List<string>();
      if (!Mathf.Approximately(_tuning.RoundSeconds, next.RoundSeconds))
        changedKeys.Add(CozyCollectorConsts.ConfigRoundSeconds);
      if (!Mathf.Approximately(_tuning.PlayerSpeed, next.PlayerSpeed))
        changedKeys.Add(CozyCollectorConsts.ConfigPlayerSpeed);
      if (!Mathf.Approximately(_tuning.SpawnIntervalMs, next.SpawnIntervalMs))
        changedKeys.Add(CozyCollectorConsts.ConfigSpawnIntervalMs);
      if (!Mathf.Approximately(_tuning.CollectibleScore, next.CollectibleScore))
        changedKeys.Add(CozyCollectorConsts.ConfigCollectibleScore);

      _tuning.RoundSeconds = next.RoundSeconds;
      _tuning.PlayerSpeed = next.PlayerSpeed;
      _tuning.SpawnIntervalMs = next.SpawnIntervalMs;
      _tuning.CollectibleScore = next.CollectibleScore;

      if (!announce || changedKeys.Count > 0)
      {
        Log(
          CozyCollectorConsts.CapabilityConfig,
          "Remote config loaded",
          $"speed {Mathf.RoundToInt(_tuning.PlayerSpeed)} · spawn {Mathf.RoundToInt(_tuning.SpawnIntervalMs)}ms · " +
          $"+{Mathf.RoundToInt(_tuning.CollectibleScore)} · {Mathf.RoundToInt(_tuning.RoundSeconds)}s");
      }

      foreach (var key in changedKeys)
      {
        TuningChanged?.Invoke(key);
        if (announce)
        {
          Log(CozyCollectorConsts.CapabilityConfig, "Config changed live", key);
          Toast("Remote config updated", key + " applied live", CozyToastTone.Success);
        }
      }
    }

    private static float ReadConfigFloat(string key, float fallback)
    {
      return Rudder.Client.RemoteConfig.Get(key, fallback);
    }

    // ---------------------------------------------------------------
    // Profile / wallets / stores / inventory (pull-based freshness)
    // ---------------------------------------------------------------

    private async Task RefreshProfileAsync()
    {
      var profile = await Rudder.Client.Player.GetProfileAsync();
      var player = profile?.Player;

      _identity = new CozyIdentity
      {
        PlayerId = player?.Id ?? "unknown",
        Nickname = _nickname,
        DeviceId = _deviceId,
        ProjectKey = Rudder.Client.ProjectKey,
        Region = player?.Region
      };
      Log(
        CozyCollectorConsts.CapabilityAuth,
        "Authenticated",
        _identity.Nickname + " · " + ShortId(_identity.PlayerId));

      ApplyWallets(profile);
    }

    private async Task RefreshWalletsAsync()
    {
      var profile = await Rudder.Client.Player.GetProfileAsync();
      ApplyWallets(profile);
    }

    private void ApplyWallets(PlayerProfile profile)
    {
      _wallets.Clear();
      if (profile?.Wallets != null)
      {
        foreach (var wallet in profile.Wallets)
        {
          _wallets.Add(new CozyWallet
          {
            Currency = wallet.Currency ?? string.Empty,
            Balance = wallet.Balance
          });
        }
      }

      Log(CozyCollectorConsts.CapabilityWallet, "Wallet loaded", WalletSummary());
      WalletsChanged?.Invoke();
    }

    private async Task RefreshStoresAsync()
    {
      var stores = await Rudder.Client.Stores.ListAsync();

      _shopOffers.Clear();
      if (stores != null)
      {
        foreach (var store in stores)
        {
          if (store?.Offers == null)
            continue;

          foreach (var offer in store.Offers)
          {
            _shopOffers.Add(MapOffer(store.Slug, offer));
          }
        }
      }

      Log(CozyCollectorConsts.CapabilityStore, "Store catalog loaded", _shopOffers.Count + " offer(s)");
      ShopChanged?.Invoke();
    }

    private async Task RefreshInventoryAsync()
    {
      var items = await Rudder.Client.Inventory.GetAsync();

      _backpackItems.Clear();
      if (items != null)
      {
        foreach (var item in items)
        {
          var slug = item.Slug ?? string.Empty;
          _backpackItems.Add(new CozyBackpackItem
          {
            Slug = slug,
            Name = string.IsNullOrEmpty(item.NameOverride) ? slug : item.NameOverride,
            Amount = (int)item.Amount
          });
        }
      }

      Log(CozyCollectorConsts.CapabilityInventory, "Inventory loaded", _backpackItems.Count + " item(s)");
      BackpackChanged?.Invoke();
    }

    private async Task RefreshPurchasablesAsync()
    {
      // Purchases mutate wallets, catalog stock and inventory server-side;
      // the C# SDK has no onChange, so freshness is pull-based.
      try { await RefreshWalletsAsync(); }
      catch (Exception exception) { Log(CozyCollectorConsts.CapabilityWallet, "Wallet refresh failed", exception.Message); }

      try { await RefreshStoresAsync(); }
      catch (Exception exception) { Log(CozyCollectorConsts.CapabilityStore, "Store refresh failed", exception.Message); }

      try { await RefreshInventoryAsync(); }
      catch (Exception exception) { Log(CozyCollectorConsts.CapabilityInventory, "Inventory refresh failed", exception.Message); }
    }

    // ---------------------------------------------------------------
    // Storage save
    // ---------------------------------------------------------------

    // Storage payload mirrors the Phaser saveRound JSON shape (camelCase keys).
    [Serializable]
    private class SavePayload
    {
      public int bestScore;
      public int lastScore;
      public int roundsPlayed;
      public int totalCollected;
      public string updatedAt;
    }

    private async Task LoadSaveAsync()
    {
      var response = await Rudder.Client.Storage.GetAsync();
      var item = response?.Items?.Find(
        candidate => candidate.Type == CozyCollectorConsts.StorageType && candidate.Id == CozyCollectorConsts.StorageId);

      if (item == null || string.IsNullOrEmpty(item.Data))
      {
        Log(CozyCollectorConsts.CapabilityStorage, "No saved progress yet", CozyCollectorConsts.StorageType);
        return;
      }

      try
      {
        var payload = JsonUtility.FromJson<SavePayload>(item.Data);
        _save.BestScore = payload.bestScore;
        _save.LastScore = payload.lastScore;
        _save.RoundsPlayed = payload.roundsPlayed;
        _save.TotalCollected = payload.totalCollected;
        Log(
          CozyCollectorConsts.CapabilityStorage,
          "Loaded progress",
          $"best {_save.BestScore} · rounds {_save.RoundsPlayed}");
      }
      catch (Exception exception)
      {
        Log(CozyCollectorConsts.CapabilityStorage, "Save data unreadable", exception.Message);
      }
    }

    private async Task SaveRoundAsync(int score, int collected)
    {
      _save.BestScore = Math.Max(_save.BestScore, score);
      _save.LastScore = score;
      _save.RoundsPlayed += 1;
      _save.TotalCollected += collected;

      var payload = new SavePayload
      {
        bestScore = _save.BestScore,
        lastScore = _save.LastScore,
        roundsPlayed = _save.RoundsPlayed,
        totalCollected = _save.TotalCollected,
        updatedAt = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
      };

      await Rudder.Client.Storage.SaveAsync(new[]
      {
        new StorageItem
        {
          Type = CozyCollectorConsts.StorageType,
          Id = CozyCollectorConsts.StorageId,
          Data = JsonUtility.ToJson(payload)
        }
      });

      Log(
        CozyCollectorConsts.CapabilityStorage,
        "Saved progress",
        $"best {_save.BestScore} · rounds {_save.RoundsPlayed}");
    }

    // ---------------------------------------------------------------
    // Leaderboard
    // ---------------------------------------------------------------

    private async Task<List<CozyLeaderboardEntry>> SubmitLeaderboardAsync(int score)
    {
      var leaderboard = Rudder.Client.Leaderboards.FindBySlug(CozyCollectorConsts.LeaderboardSlug);
      await leaderboard.SubmitAsync(score);
      var entries = await leaderboard.ListAsync(10);
      Log(CozyCollectorConsts.CapabilityLeaderboard, $"Submitted score {score}", CozyCollectorConsts.LeaderboardSlug);
      return MapEntries(entries);
    }

    private async Task<List<CozyLeaderboardEntry>> ListLeaderboardAsync()
    {
      var entries = await Rudder.Client.Leaderboards.FindBySlug(CozyCollectorConsts.LeaderboardSlug).ListAsync(10);
      return MapEntries(entries);
    }

    private static List<CozyLeaderboardEntry> MapEntries(IReadOnlyList<RankEntry> entries)
    {
      var mapped = new List<CozyLeaderboardEntry>();
      if (entries == null)
        return mapped;

      foreach (var entry in entries)
      {
        mapped.Add(new CozyLeaderboardEntry
        {
          PlayerName = entry.PlayerName ?? entry.PlayerId ?? "(unknown)",
          Rank = (int)entry.Rank,
          Score = (int)entry.Score
        });
      }

      return mapped;
    }

    // ---------------------------------------------------------------
    // Round flow
    // ---------------------------------------------------------------

    private void OnRoundFinished(int score, int collected)
    {
      SyncWorldPause();
      _ = FinishRoundAsync(score, collected);
    }

    private async Task FinishRoundAsync(int score, int collected)
    {
      try
      {
        await SaveRoundAsync(score, collected);
      }
      catch (Exception exception)
      {
        Toast("Save failed", exception.Message, CozyToastTone.Error);
        Log(CozyCollectorConsts.CapabilityStorage, "Save failed", exception.Message);
      }

      var entries = new List<CozyLeaderboardEntry>();
      try
      {
        entries = await SubmitLeaderboardAsync(score);
      }
      catch (Exception exception)
      {
        Toast("Leaderboard failed", exception.Message, CozyToastTone.Error);
        Log(CozyCollectorConsts.CapabilityLeaderboard, "Submit failed", exception.Message);
      }

      _roundResult = new CozyRoundResult
      {
        Score = score,
        Best = _save.BestScore,
        Collected = collected,
        TopEntries = entries
      };
      RoundResultChanged?.Invoke();
      SetModal(CozyModal.RoundResult);


      try
      {
        await Rudder.Client.Scenario.TriggerAsync("demo_round_finished");
      }
      catch (Exception exception)
      {
        Log(CozyCollectorConsts.CapabilityScenario, "Trigger failed", exception.Message);
      }
    }

    public void PlayAgain()
    {
      SetModal(CozyModal.None);
      world.RestartRound();
      SyncWorldPause();
    }

    // ---------------------------------------------------------------
    // Modals
    // ---------------------------------------------------------------

    public void OpenModal(CozyModal modal)
    {
      SetModal(modal);
    }

    public void CloseModal()
    {
      // Scenario offers must be bought or declined, same as the Phaser store.
      if (_activeModal == CozyModal.Offer)
        return;

      SetModal(CozyModal.None);
    }

    private void SetModal(CozyModal modal)
    {
      if (_activeModal == modal)
        return;

      _activeModal = modal;
      ModalChanged?.Invoke();
      SyncWorldPause();
    }

    private void SyncWorldPause()
    {
      world.SetPaused(_activeModal != CozyModal.None || !world.RoundActive);
    }

    // ---------------------------------------------------------------
    // Shop
    // ---------------------------------------------------------------

    public void BuyShopOffer(string offerId)
    {
      _ = BuyShopOfferAsync(offerId);
    }

    private async Task BuyShopOfferAsync(string offerId)
    {
      var offer = _shopOffers.Find(candidate => candidate.OfferId == offerId);
      if (offer == null)
      {
        Toast("Purchase failed", "Unknown offer " + offerId, CozyToastTone.Error);
        return;
      }

      try
      {
        var response = await Rudder.Client.Stores.PurchaseAsync(
          offer.StoreSlug,
          offer.OfferId,
          "demo-shop-" + offer.OfferId + "-" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());

        if (response != null && !response.Success)
        {
          Toast("Purchase failed", response.Error ?? "The store rejected the purchase.", CozyToastTone.Error);
          Log(CozyCollectorConsts.CapabilityStore, "Purchase rejected", response.Error);
          return;
        }
      }
      catch (Exception exception)
      {
        Toast("Purchase failed", exception.Message, CozyToastTone.Error);
        Log(CozyCollectorConsts.CapabilityStore, "Purchase failed", exception.Message);
        return;
      }

      Toast("Purchase complete", offer.Name, CozyToastTone.Success);
      Log(CozyCollectorConsts.CapabilityStore, "Purchased offer", offer.Name + " · " + offer.StoreSlug);
      await RefreshPurchasablesAsync();
    }

    // ---------------------------------------------------------------
    // Scenario store offer
    // ---------------------------------------------------------------

    public void BuyScenarioOffer()
    {
      _ = BuyScenarioOfferAsync();
    }

    private async Task BuyScenarioOfferAsync()
    {
      var session = _storeSession;
      var offerInfo = _currentOffer;
      if (session == null || offerInfo == null || offerInfo.Offers == null || offerInfo.Offers.Count == 0)
        return;

      // Mirror the Phaser OfferModal: prefer the configured offer, fall back to the first.
      var offer = offerInfo.Offers.Find(candidate => candidate.OfferId == CozyCollectorConsts.OfferId)
                  ?? offerInfo.Offers[0];

      try
      {
        var response = await Rudder.Client.Stores.PurchaseAsync(
          offer.StoreSlug,
          offer.OfferId,
          "demo-offer-" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());

        if (response != null && !response.Success)
        {
          Toast("Offer failed", response.Error ?? "The store rejected the purchase.", CozyToastTone.Error);
          Log(CozyCollectorConsts.CapabilityStore, "Offer purchase rejected", response.Error);
          return;
        }

        session.Purchase();
      }
      catch (Exception exception)
      {
        Toast("Offer failed", exception.Message, CozyToastTone.Error);
        Log(CozyCollectorConsts.CapabilityStore, "Offer purchase failed", exception.Message);
        return;
      }

      _storeSession = null;
      _currentOffer = null;
      OfferChanged?.Invoke();
      SetModal(CozyModal.None);
      Toast("Offer purchased", "Waiting for the scenario reward…", CozyToastTone.Success);
      Log(CozyCollectorConsts.CapabilityStore, "Scenario offer purchased", offer.Name);
      await RefreshPurchasablesAsync();
    }

    public void DeclineScenarioOffer()
    {
      var session = _storeSession;
      if (session == null)
        return;

      session.Decline();
      _storeSession = null;
      _currentOffer = null;
      OfferChanged?.Invoke();
      SetModal(CozyModal.None);
      Toast("Offer declined", "The camp merchant will return later.", CozyToastTone.Info);
      Log(CozyCollectorConsts.CapabilityStore, "Scenario offer declined", null);
    }

    // ---------------------------------------------------------------
    // Scenario leaderboard (dormant path, driven from the result modal)
    // ---------------------------------------------------------------

    public void ClaimScenarioLeaderboardReward()
    {
      if (_leaderboardSession == null)
        return;

      _leaderboardSession.RewardClaimed();
      _leaderboardSession = null;
      Log(CozyCollectorConsts.CapabilityLeaderboard, "Scenario reward claimed", null);
    }

    public void EndScenarioLeaderboard()
    {
      if (_leaderboardSession == null)
        return;

      _leaderboardSession.End();
      _leaderboardSession = null;
      Log(CozyCollectorConsts.CapabilityLeaderboard, "Scenario leaderboard ended", null);
    }

    // ---------------------------------------------------------------
    // Scenario handlers
    // ---------------------------------------------------------------

    private void RegisterScenarioHandlers()
    {
      if (_scenarioHandlersRegistered || Rudder.State != RudderState.Ready)
        return;

      Rudder.Client.Scenario.OnNotification += OnScenarioNotification;
      Rudder.Client.Scenario.OnWait += OnScenarioWait;
      Rudder.Client.Scenario.OnStoreOffer += OnScenarioStoreOffer;
      Rudder.Client.Scenario.OnConfigChanged += OnScenarioConfigChanged;
      Rudder.Client.Scenario.OnQuest += OnScenarioQuest;
      Rudder.Client.Scenario.OnLeaderboard += OnScenarioLeaderboard;
      Rudder.Client.Scenario.OnBattlePass += OnScenarioBattlePass;
      Rudder.Client.Scenario.OnBattlePassLevel += OnScenarioBattlePassLevel;
      Rudder.Client.Scenario.OnScenarioCompleted += OnScenarioCompleted;
      Rudder.Client.Scenario.OnScenarioFailed += OnScenarioFailed;
      _scenarioHandlersRegistered = true;
    }

    private void UnregisterScenarioHandlers()
    {
      if (!_scenarioHandlersRegistered || Rudder.State != RudderState.Ready)
        return;

      Rudder.Client.Scenario.OnNotification -= OnScenarioNotification;
      Rudder.Client.Scenario.OnWait -= OnScenarioWait;
      Rudder.Client.Scenario.OnStoreOffer -= OnScenarioStoreOffer;
      Rudder.Client.Scenario.OnConfigChanged -= OnScenarioConfigChanged;
      Rudder.Client.Scenario.OnQuest -= OnScenarioQuest;
      Rudder.Client.Scenario.OnLeaderboard -= OnScenarioLeaderboard;
      Rudder.Client.Scenario.OnBattlePass -= OnScenarioBattlePass;
      Rudder.Client.Scenario.OnBattlePassLevel -= OnScenarioBattlePassLevel;
      Rudder.Client.Scenario.OnScenarioCompleted -= OnScenarioCompleted;
      Rudder.Client.Scenario.OnScenarioFailed -= OnScenarioFailed;
      _scenarioHandlersRegistered = false;
    }

    private void OnScenarioNotification(NotificationSession session)
    {
      var title = session.Get<string>("title", "");
      var message = session.Get<string>("message", "");
      Log(CozyCollectorConsts.CapabilityScenario, "Notification effect", string.IsNullOrEmpty(title) ? message : title);
      Toast(string.IsNullOrEmpty(title) ? "LiveOps" : title, message, CozyToastTone.Info);
      StartCoroutine(CompleteNotificationAfterDelay(session));
    }

    private IEnumerator CompleteNotificationAfterDelay(NotificationSession session)
    {
      yield return new WaitForSeconds(NotificationAutoCompleteSeconds);
      session.Complete();
      _ = RefreshWalletsAfterNotificationAsync();
    }

    private async Task RefreshWalletsAfterNotificationAsync()
    {
      // Notification rewards may have landed on the wallet; pull fresh state.
      try
      {
        await RefreshWalletsAsync();
      }
      catch (Exception exception)
      {
        Log(CozyCollectorConsts.CapabilityWallet, "Wallet refresh failed", exception.Message);
      }
    }

    private void OnScenarioWait(WaitSession session)
    {
      Log(CozyCollectorConsts.CapabilityScenario, "Scenario wait", session.DeadlineUtc.ToString("O"));
    }

    private void OnScenarioStoreOffer(StoreOfferSession session)
    {
      var storeSlug = session.Get<string>("storeSlug", CozyCollectorConsts.StoreSlug);
      Log(CozyCollectorConsts.CapabilityScenario, "Store-offer effect", storeSlug);
      _ = PresentStoreOfferAsync(session, storeSlug);
    }

    private async Task PresentStoreOfferAsync(StoreOfferSession session, string storeSlug)
    {
      try
      {
        var store = await Rudder.Client.Stores.GetAsync(storeSlug);
        var offers = new List<CozyShopOffer>();
        if (store?.Offers != null)
        {
          foreach (var offer in store.Offers)
          {
            offers.Add(MapOffer(store.Slug, offer));
          }
        }

        _storeSession = session;
        _currentOffer = new CozyOfferInfo
        {
          StoreSlug = store?.Slug ?? storeSlug,
          StoreName = store?.Name ?? storeSlug,
          Offers = offers
        };
        OfferChanged?.Invoke();
        OpenModal(CozyModal.Offer);
      }
      catch (Exception exception)
      {
        Log(CozyCollectorConsts.CapabilityStore, "Store offer unavailable", exception.Message);
        Toast("Offer unavailable", exception.Message, CozyToastTone.Error);
        session.Decline();
      }
    }

    private void OnScenarioConfigChanged(ConfigChangedSession session)
    {
      // The SDK dispatch already applied the patches to the remote-config cache.
      Log(CozyCollectorConsts.CapabilityScenario, "Remote config override applied", session.Context.NodeId);
      RefreshTuning(true);
    }

    private void OnScenarioQuest(QuestSession session)
    {
      Log(CozyCollectorConsts.CapabilityScenario, "Scenario quest node", session.Context.NodeId);
    }

    private void OnScenarioLeaderboard(LeaderboardSession session)
    {
      // Dormant until a scenario emits a leaderboard node; shares the round-result modal.
      _leaderboardSession = session;
      Log(CozyCollectorConsts.CapabilityLeaderboard, "Leaderboard effect (scenario)", session.Context.NodeId);
      _ = PresentScenarioLeaderboardAsync();
    }

    private async Task PresentScenarioLeaderboardAsync()
    {
      var entries = new List<CozyLeaderboardEntry>();
      try
      {
        entries = await ListLeaderboardAsync();
      }
      catch (Exception exception)
      {
        Log(CozyCollectorConsts.CapabilityLeaderboard, "List failed", exception.Message);
      }

      _roundResult = new CozyRoundResult
      {
        Score = _save.LastScore,
        Best = _save.BestScore,
        Collected = 0,
        TopEntries = entries
      };
      RoundResultChanged?.Invoke();
      SetModal(CozyModal.RoundResult);
    }

    private void OnScenarioBattlePass(BattlePassSession session)
    {
      Log(CozyCollectorConsts.CapabilityScenario, "Scenario battlepass node", session.Context.NodeId);
    }

    private void OnScenarioBattlePassLevel(BattlePassLevelSession session)
    {
      Log(CozyCollectorConsts.CapabilityScenario, "Scenario battlepass level node", session.Context.NodeId);
    }

    private void OnScenarioCompleted(PlanRun run)
    {
      Log(CozyCollectorConsts.CapabilityScenario, "Scenario run completed", run.RunId);
    }

    private void OnScenarioFailed(ScenarioFailedEvent failed)
    {
      Log(CozyCollectorConsts.CapabilityScenario, "Scenario run failed", failed.NodeId + ": " + failed.Exception.Message);
    }

    // ---------------------------------------------------------------
    // Inspector log / toasts
    // ---------------------------------------------------------------

    public void Log(string capability, string message, string detail = null)
    {
      _logEntries.Insert(0, new CozyLogEntry
      {
        Capability = capability,
        Message = message,
        Detail = detail,
        Timestamp = DateTime.Now
      });
      if (_logEntries.Count > MaxLogEntries)
        _logEntries.RemoveAt(_logEntries.Count - 1);

      LogAdded?.Invoke();
    }

    public void Toast(string title, string message, CozyToastTone tone)
    {
      ToastRequested?.Invoke(title, message, tone);
    }

    // ---------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------

    private void SetPhase(CozyPhase phase, string statusMessage)
    {
      if (_phase == phase && _statusMessage == statusMessage)
        return;

      _phase = phase;
      _statusMessage = statusMessage;
      PhaseChanged?.Invoke();
    }

    private string WalletSummary()
    {
      if (_wallets.Count == 0)
        return "empty";

      var parts = new List<string>();
      foreach (var wallet in _wallets)
      {
        parts.Add((wallet.Balance + " " + wallet.Currency).Trim());
      }

      return string.Join(" · ", parts);
    }

    private static CozyShopOffer MapOffer(string storeSlug, Offer offer)
    {
      return new CozyShopOffer
      {
        StoreSlug = storeSlug,
        OfferId = offer.Id,
        Name = string.IsNullOrEmpty(offer.Name) ? offer.Id : offer.Name,
        Price = offer.Price?.Amount ?? 0,
        Currency = offer.Price?.Currency ?? string.Empty
      };
    }

    private static string GetOrCreateNickname(string deviceId)
    {
      var existing = PlayerPrefs.GetString(CozyCollectorConsts.NicknamePrefsKey, string.Empty);
      if (!string.IsNullOrEmpty(existing))
        return existing;

      // Nickname is generated locally once and passed to LoginWithDeviceAsync.
      var compact = deviceId.Replace("-", string.Empty);
      var suffix = compact.Length > 4 ? compact.Substring(0, 4) : compact;
      var nickname = "Player-" + suffix;
      PlayerPrefs.SetString(CozyCollectorConsts.NicknamePrefsKey, nickname);
      PlayerPrefs.Save();
      return nickname;
    }

    private static string ShortId(string value)
    {
      return value != null && value.Length > 10 ? value.Substring(0, 8) + "…" : value;
    }
  }
}
