using System;
using System.Threading.Tasks;
using UnityEngine;
using CoreClient = RudderSdk.Core.RudderClient;

namespace RudderSdk.Unity
{
  public enum RudderState
  {
    NotInitialized,
    Initializing,
    Ready,
    Failed
  }

  /// <summary>
  /// Scene bootstrap MonoBehaviour and public entrypoint of the Rudder Unity SDK.
  /// Add the component to a scene with a RudderConfiguration assigned, or call
  /// Rudder.Initialize(configuration) and the hidden GameObject is created for you.
  /// </summary>
  [DefaultExecutionOrder(-1500), DisallowMultipleComponent]
  public class Rudder : MonoBehaviour
  {
    private static Rudder _instance;
    private static CoreClient _client;
    private static TaskCompletionSource<CoreClient> _readyCompletion = NewCompletion();

    [SerializeField] private RudderConfiguration _configuration;

    /// Attach point for optional add-on packages so this assembly never has to
    /// reference them. Sugar over Ready: subscribers that load late must check
    /// State themselves.
    public static event Action<CoreClient> Initialized;

    public static RudderState State { get; private set; } = RudderState.NotInitialized;

    public static Exception LastError { get; private set; }

    public static CoreClient Client
    {
      get
      {
        if (State != RudderState.Ready || _client == null)
        {
          throw new InvalidOperationException(
            "Rudder is not initialized. Add the Rudder component to the scene with a " +
            "RudderConfiguration assigned, or call Rudder.Initialize(configuration).");
        }

        return _client;
      }
    }

    /// Completes with the initialized client, or faults with the initialization error.
    public static Task<CoreClient> Ready => _readyCompletion.Task;

    public static void Initialize(RudderConfiguration configuration)
    {
      if (configuration == null)
        throw new ArgumentNullException(nameof(configuration));

      if (State == RudderState.Initializing || State == RudderState.Ready)
        throw new InvalidOperationException("Rudder is already initialized.");

      if (_instance == null)
      {
        var host = new GameObject(nameof(Rudder));
        _instance = host.AddComponent<Rudder>();
      }

      _instance._configuration = configuration;
      _ = _instance.InitializeAsync();
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void ResetStatics()
    {
      _instance = null;
      _client = null;
      State = RudderState.NotInitialized;
      LastError = null;
      Initialized = null;
      _readyCompletion = NewCompletion();
    }

    private void Awake()
    {
      if (_instance != null && _instance != this)
      {
        Destroy(gameObject);
        return;
      }

      _instance = this;
      DontDestroyOnLoad(gameObject);
    }

    private void Start()
    {
      if (_instance == this && _configuration != null && State == RudderState.NotInitialized)
        _ = InitializeAsync();
    }

    private async Task InitializeAsync()
    {
      State = RudderState.Initializing;
      LastError = null;

      try
      {
        var options = new RudderUnityClientOptions
        {
          BaseUrl = _configuration.BaseUrl,
          RealtimeUrl = _configuration.RealtimeUrl,
          ProjectKey = _configuration.ProjectKey,
          TimeoutSeconds = _configuration.TimeoutSeconds,
          KeyValueStore = new PlayerPrefsUnityKeyValueStore(),
          LoggerSink = new UnityDebugLoggerSink()
        };

        _client = RudderUnityClientFactory.Create(options);

        try
        {
          await _client.Scenario.RestoreAsync();
        }
        catch (Exception restoreException)
        {
          Debug.LogWarning("[Rudder] Failed to restore scenario state: " + restoreException.Message);
        }
      }
      catch (Exception exception)
      {
        _client = null;
        State = RudderState.Failed;
        LastError = exception;
        _readyCompletion.TrySetException(exception);
        Debug.LogError(
          "[Rudder] Failed to initialize the Rudder SDK. Check the RudderConfiguration asset " +
          $"(ProjectKey and BaseUrl must be set). {exception.Message}");
        return;
      }

      State = RudderState.Ready;
      Debug.Log("[Rudder] Rudder SDK initialized.");
      _readyCompletion.TrySetResult(_client);
      Initialized?.Invoke(_client);
    }

    private void Update()
    {
      _client?.Update(Time.deltaTime);
    }

    private void OnApplicationQuit()
    {
      DisconnectRealtime();
    }

    private void OnDestroy()
    {
      if (_instance != this)
        return;

      DisconnectRealtime();

      _instance = null;
      _client = null;
      State = RudderState.NotInitialized;
      LastError = null;
      _readyCompletion.TrySetException(new InvalidOperationException("Rudder was destroyed before initialization completed."));
      _readyCompletion = NewCompletion();
    }

    private void DisconnectRealtime()
    {
      var client = _client;
      if (client == null)
        return;

      try
      {
        _ = client.Realtime.DisconnectAsync().ContinueWith(
          task => Debug.LogWarning(
            "[Rudder] Realtime disconnect failed: " + task.Exception?.GetBaseException().Message),
          TaskContinuationOptions.OnlyOnFaulted);
      }
      catch (Exception exception)
      {
        Debug.LogWarning("[Rudder] Realtime disconnect failed: " + exception.Message);
      }
    }

    private static TaskCompletionSource<CoreClient> NewCompletion()
    {
      return new TaskCompletionSource<CoreClient>(TaskCreationOptions.RunContinuationsAsynchronously);
    }
  }
}
