package com.castlabs.reactnative.utils;

import android.net.Uri;
import androidx.annotation.NonNull;
import com.castlabs.android.SdkConsts;
import com.castlabs.android.drm.Drm;
import com.castlabs.android.drm.DrmConfiguration;
import com.castlabs.android.network.NetworkConfiguration;
import com.castlabs.android.network.RetryConfiguration;
import com.castlabs.android.player.AbrConfiguration;
import com.castlabs.android.player.BufferConfiguration;
import com.castlabs.android.player.CatchupConfiguration;
import com.castlabs.android.player.CustomUtcTimingElement;
import com.castlabs.android.player.LiveConfiguration;
import com.castlabs.android.player.PlayerConfig;
import com.castlabs.android.player.models.SideloadedTrack;
import com.castlabs.reactnative.errors.ErrorCode;
import com.castlabs.reactnative.errors.ErrorSeverity;
import com.castlabs.reactnative.errors.PrestoPlayError;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import android.os.Bundle;

/**
 * Converts from JavaScript types to Android types.
 */
public class BridgeDeserializer {

  @NonNull
  public static List<String> toPluginFactoryModuleNames(
      final @NonNull ReadableMap jsonSdkConfiguration) {
    ReadableArray jsonPluginFactoryModuleNames =
        jsonSdkConfiguration.getArray("pluginFactoryModuleNames");
    if (jsonPluginFactoryModuleNames != null) {
      List<String> pluginFactoryModuleNames = new ArrayList<>();

      for (int i = 0; i < jsonPluginFactoryModuleNames.size(); i++) {
        pluginFactoryModuleNames.add(jsonPluginFactoryModuleNames.getString(i));
      }

      return pluginFactoryModuleNames;
    } else {
      return Collections.emptyList();
    }
  }

  /**
   * Maps request or response URL from JavaScript to Android structure.
   *
   * @param jsonUrl the JavaScript request or response URL
   * @return the Android request or response URL
   */
  @NonNull
  public static Uri toUrl(@NonNull String jsonUrl) {
    return Uri.parse(jsonUrl);
  }

  @NonNull
  public static byte[] toBytes(@NonNull String base64Encoded) {
    return Base64.getDecoder().decode(base64Encoded);
  }

  /**
   * Builds request headers.
   *
   * @param jsonHeaders JavaScript request headers
   * @return Android request headers
   */
  @NonNull
  public static Map<String, String> toRequestHeaders(@NonNull ReadableMap jsonHeaders) {
    Map<String, String> headers = new HashMap<>();

    ReadableMapKeySetIterator iterator = jsonHeaders.keySetIterator();
    while (iterator.hasNextKey()) {
      String key = iterator.nextKey();
      String value = jsonHeaders.getString(key);
      headers.put(key, value);
    }

    return headers;
  }

  public static int toContentType(@NonNull String jsonContentType) throws PrestoPlayError {
    switch (jsonContentType) {
      case "dash":
        return SdkConsts.CONTENT_TYPE_DASH;
      case "mp4":
        return SdkConsts.CONTENT_TYPE_MP4;
      case "hls":
        return SdkConsts.CONTENT_TYPE_HLS;
      case "smooth":
        return SdkConsts.CONTENT_TYPE_SMOOTHSTREAMING;
      default:
        throw new PrestoPlayError(
            ErrorSeverity.FATAL,
            ErrorCode.INVALID_CONFIGURATION,
            "Unknown source content type"
        );
    }
  }

  /**
   * Builds response headers.
   *
   * @param jsonHeaders JavaScript response headers
   * @return Android response headers
   */
  @NonNull
  public static Map<String, List<String>> toResponseHeaders(@NonNull ReadableMap jsonHeaders) {
    Map<String, List<String>> headers = new HashMap<>();

    ReadableMapKeySetIterator iterator = jsonHeaders.keySetIterator();
    while (iterator.hasNextKey()) {
      String key = iterator.nextKey();
      String value = jsonHeaders.getString(key);

      headers.put(key, Arrays.asList(value.split(",")));
    }

    return headers;
  }

  /**
   * Maps the player configuration of the React Native SDK
   * to {@link com.castlabs.android.player.PlayerConfig}.
   *
   * @param jsonPlayerConfiguration The player configuration of the React Native SDK
   * @return The player configuration of the Android SDK
   * @throws PrestoPlayError Invalid configuration
   */
  @NonNull
  public static PlayerConfig toPlayerConfig(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) throws PrestoPlayError {
    JsonMap source = jsonPlayerConfiguration
        .getMap("source")
        .orElseThrow(invalidConfiguration("Source not found"));

    String sourceUrl = source
        .getString("url")
        .orElseThrow(invalidConfiguration("Source URL not found"));

    String sourceContentType = source
        .getString("type")
        .orElseThrow(invalidConfiguration("Source content type not found"));

    PlayerConfig.Builder builder = new PlayerConfig.Builder(sourceUrl);

    builder.preservePlayerViewSurface(true);
    builder.contentType(toContentType(sourceContentType));

    jsonPlayerConfiguration
        .getLong("startTimeMs")
        .ifPresent((Long value) -> {
          builder.positionUs(value * 1000L);
        });

    jsonPlayerConfiguration
        .getBoolean("autoPlay")
        .ifPresent(builder::autoPlay);

    Optional<JsonMap> drmConfiguration = jsonPlayerConfiguration.getMap("drm");
    if (drmConfiguration.isPresent()) {

        Optional<String> offlineId = drmConfiguration.get().getString("offlineId");

        DrmConfiguration.Builder drmBuilder = new DrmConfiguration.Builder()
                .url(DrmUtils.urlPlaceholder); // NOTE: This is a placeholder, ReactNative SDK MUST set it in request modifier

        if (offlineId.isPresent() && offlineId.get().length() != 0) {
            drmBuilder.offlineId(offlineId.get());
        }
        builder.drmConfiguration(drmBuilder.get());
    }

    jsonPlayerConfiguration
        .getString("userId")
        .ifPresent(builder::userID);

    jsonPlayerConfiguration
        .getString("preferredTextLanguage")
        .ifPresent(builder::preferredTextLanguage);

    jsonPlayerConfiguration
        .getString("preferredAudioLanguage")
        .ifPresent(builder::preferredAudioLanguage);

    jsonPlayerConfiguration
        .getMap("android")
        .ifPresent(jsonAndroidPlayerConfiguration -> {
          jsonAndroidPlayerConfiguration
              .getBoolean("forceInStreamDrmInitData")
              .ifPresent(builder::forceInStreamDrmInitData);
          jsonAndroidPlayerConfiguration
              .getMap("contentParameters")
              .ifPresent(val -> {
                builder.contentParameters(toBundle(val));
              });
          jsonAndroidPlayerConfiguration
              .getMap("contentQueryParameters")
              .ifPresent(val -> {
                builder.contentQueryParameters(toBundle(val));
              });
          jsonAndroidPlayerConfiguration
              .getMap("segmentParameters")
              .ifPresent(val -> {
                builder.contentParameters(toBundle(val));
              });
          jsonAndroidPlayerConfiguration
              .getMap("segmentQueryParameters")
              .ifPresent(val -> {
                builder.contentQueryParameters(toBundle(val));
              });
        });

    builder.bufferConfiguration(toBufferConfiguration(jsonPlayerConfiguration));
    builder.networkConfiguration(toNetworkConfiguration(jsonPlayerConfiguration));
    builder.liveConfiguration(toLiveConfiguration(jsonPlayerConfiguration));
    builder.sideloadedTracks(toSideloadedTracks(jsonPlayerConfiguration));
    builder.abrConfiguration(toAbrConfiguration(jsonPlayerConfiguration));
    builder.metaData(toMetaData(jsonPlayerConfiguration));

    return builder.get();
  }

  public static Bundle toMetaData(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) {
    MetaDataBuilder builder = new MetaDataBuilder();

    Optional.of(jsonPlayerConfiguration)
        .flatMap(json -> json.getMap("metaData"))
        .ifPresent(jsonMetaDataConfiguration -> {
          jsonMetaDataConfiguration
              .getString("title")
              .ifPresent(builder::title);
          jsonMetaDataConfiguration
              .getString("artist")
              .ifPresent(builder::artist);
          jsonMetaDataConfiguration
              .getString("artworkUrl")
              .ifPresent(builder::artworkUrl);
        });

    return builder.get();
  }

  public static BufferConfiguration toBufferConfiguration(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) {
    BufferConfiguration.Builder builder = new BufferConfiguration.Builder();

    Optional.of(jsonPlayerConfiguration)
        .flatMap(json -> json.getMap("android"))
        .flatMap(json -> json.getMap("bufferConfiguration"))
        .ifPresent(jsonBufferConfiguration -> {
          jsonBufferConfiguration
              .getBoolean("videoFeedYield")
              .ifPresent(builder::videoFeedYield);
          jsonBufferConfiguration
              .getBoolean("audioFeedYield")
              .ifPresent(builder::audioFeedYield);
          jsonBufferConfiguration
              .getLong("backBufferDurationMs")
              .ifPresent(value -> builder.backBufferDuration(value, TimeUnit.MILLISECONDS));
          jsonBufferConfiguration
              .getInteger("highMediaTimeMs")
              .ifPresent(value -> builder.highMediaTime(value, TimeUnit.MILLISECONDS));
          jsonBufferConfiguration
              .getInteger("lowMediaTimeMs")
              .ifPresent(value -> builder.lowMediaTime(value, TimeUnit.MILLISECONDS));
          jsonBufferConfiguration
              .getInteger("minPlaybackStartMs")
              .ifPresent(value -> builder.minPlaybackStart(value, TimeUnit.MILLISECONDS));
          jsonBufferConfiguration
              .getInteger("minRebufferStartMs")
              .ifPresent(value -> builder.minRebufferStart(value, TimeUnit.MILLISECONDS));
          jsonBufferConfiguration
              .getInteger("bufferSegmentSize")
              .ifPresent(builder::bufferSegmentSize);
          jsonBufferConfiguration
              .getInteger("bufferSizeBytes")
              .ifPresent(builder::bufferSizeBytes);
          jsonBufferConfiguration
              .getBoolean("drainWhileCharging")
              .ifPresent(builder::drainWhileCharging);
          jsonBufferConfiguration
              .getBoolean("prioritizeInstreamOverManifestDuration")
              .ifPresent(builder::prioritizeInstreamOverManifestDuration);
          jsonBufferConfiguration
              .getBoolean("prioritizeTimeOverSizeThresholds")
              .ifPresent(builder::prioritizeTimeOverSizeThresholds);
        });

    return builder.get();
  }

  public static AbrConfiguration toAbrConfiguration(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) {
    AbrConfiguration.Builder builder = new AbrConfiguration.Builder();

    Optional.of(jsonPlayerConfiguration)
        .flatMap(json -> json.getMap("android"))
        .flatMap(json -> json.getMap("abrConfiguration"))
        .ifPresent(jsonAbrConfiguration -> {
          jsonAbrConfiguration
              .getFloat("degradationPenalty")
              .ifPresent(builder::degradationPenalty);
          jsonAbrConfiguration
              .getFloat("degradationRecovery")
              .ifPresent(builder::degradationRecovery);
          jsonAbrConfiguration
              .getFloat("downloadTimeFactor")
              .ifPresent(builder::downloadTimeFactor);
          jsonAbrConfiguration
              .getString("initialTrackSelection")
              .ifPresent(value -> {
                boolean keepInitialSelection = jsonAbrConfiguration
                    .getBoolean("keepInitialSelection")
                    .orElse(AbrConfiguration.DEFAULT_KEEP_INITIAL_TRACK_SELECTION);
                builder.initialTrackSelection(toAbrTrackSelection(value), keepInitialSelection);
              });
          jsonAbrConfiguration
              .getLong("maxDurationForQualityDecreaseMs")
              .ifPresent(value -> {
                builder.maxDurationForQualityDecrease(value, TimeUnit.MILLISECONDS);
              });
          jsonAbrConfiguration
              .getLong("maxInitialBitrate")
              .ifPresent(builder::maxInitialBitrate);
          jsonAbrConfiguration
              .getString("method")
              .ifPresent(value -> {
                builder.method(toAbrMethod(value));
              });
          jsonAbrConfiguration
              .getInteger("minDegradationSamples")
              .ifPresent(builder::minDegradationSamples);
          jsonAbrConfiguration
              .getLong("minDurationForQualityIncreaseMs")
              .ifPresent(value -> {
                builder.minDurationForQualityIncrease(value, TimeUnit.MILLISECONDS);
              });
          jsonAbrConfiguration
              .getLong("minDurationToRetainAfterDiscardMs")
              .ifPresent(value -> {
                builder.minDurationToRetainAfterDiscard(value, TimeUnit.MILLISECONDS);
              });
          jsonAbrConfiguration
              .getInteger("minSampledBytes")
              .ifPresent(builder::minSampledBytes);
          jsonAbrConfiguration
              .getFloat("percentile")
              .ifPresent(builder::percentile);
          jsonAbrConfiguration
              .getInteger("percentileWeight")
              .ifPresent(builder::percentileWeight);
          jsonAbrConfiguration
              .getLong("safeBufferSizeMs")
              .ifPresent(value -> {
                builder.safeBufferSize(value, TimeUnit.MILLISECONDS);
              });
          jsonAbrConfiguration
              .getInteger("timeThresholdMs")
              .ifPresent(builder::timeThresholdMs);
          jsonAbrConfiguration
              .getBoolean("useCmsd")
              .ifPresent(builder::useCMSD);
        });

    return builder.get();
  }

  public static int toAbrMethod(String jsonAbrMethod) {
    return switch (jsonAbrMethod) {
      case "commonNba" -> AbrConfiguration.METHOD_COMMON_NBA;
      case "exo" -> AbrConfiguration.METHOD_EXO;
      case "flip" -> AbrConfiguration.METHOD_FLIP;
      case "iterate" -> AbrConfiguration.METHOD_ITERATE;
      default -> throw new IllegalArgumentException("Unknown ABR method");
    };
  }

  public static int toAbrTrackSelection(String jsonAbrTrackSelection) {
    return switch (jsonAbrTrackSelection) {
      case "videoQualityHighest" -> SdkConsts.VIDEO_QUALITY_HIGHEST;
      case "videoQualityLowest" -> SdkConsts.VIDEO_QUALITY_LOWEST;
      case "videoQualityAdaptive" -> SdkConsts.VIDEO_QUALITY_ADAPTIVE;
      default -> throw new IllegalArgumentException("Unknown ABR track selection");
    };
  }

  public static NetworkConfiguration toNetworkConfiguration(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) {
    NetworkConfiguration.Builder builder = new NetworkConfiguration.Builder();

    Optional<JsonMap> jsonNetworkConfiguration = Optional.of(jsonPlayerConfiguration)
        .flatMap(json -> json.getMap("android"))
        .flatMap(json -> json.getMap("networkConfiguration"));

    jsonNetworkConfiguration
        .flatMap(json -> json.getMap("manifestAttemptParameters"))
        .ifPresent(jsonManifestAttemptParameters -> {
          jsonManifestAttemptParameters
              .getInteger("connectionTimeoutMs")
              .ifPresent(builder::manifestConnectionTimeoutMs);
          jsonManifestAttemptParameters
              .getInteger("readTimeoutMs")
              .ifPresent(builder::manifestReadTimeoutMs);

          builder.manifestRetryConfiguration(
              toRetryConfiguration(jsonManifestAttemptParameters)
          );
        });

    jsonNetworkConfiguration
        .flatMap(json -> json.getMap("segmentAttemptParameters"))
        .ifPresent(jsonSegmentAttemptParameters -> {
          jsonSegmentAttemptParameters
              .getInteger("connectionTimeoutMs")
              .ifPresent(builder::segmentsConnectionTimeoutMs);
          jsonSegmentAttemptParameters
              .getInteger("readTimeoutMs")
              .ifPresent(builder::manifestReadTimeoutMs);

          builder.segmentsRetryConfiguration(
              toRetryConfiguration(jsonSegmentAttemptParameters)
          );
        });

      jsonNetworkConfiguration
          .flatMap(json -> json.getMap("drmAttemptParameters"))
          .ifPresent(jsonDrmAttemptParameters -> {
              jsonDrmAttemptParameters
                  .getInteger("connectionTimeoutMs")
                  .ifPresent(builder::drmConnectionTimeoutMs);

              jsonDrmAttemptParameters
                  .getInteger("readTimeoutMs")
                  .ifPresent(builder::drmReadTimeoutMs);

              jsonDrmAttemptParameters
                  .getInteger("acquisitionTimeoutMs")
                  .ifPresent(builder::drmAcquisitionTimeoutMs);

              builder.drmRetryConfiguration(toRetryConfiguration(jsonDrmAttemptParameters));
          });

    return builder.get();
  }

  public static RetryConfiguration toRetryConfiguration(
      final @NonNull JsonMap jsonAttemptParameters
  ) {
    RetryConfiguration.Builder builder = new RetryConfiguration.Builder();

    jsonAttemptParameters.getInteger("maxAttempts").ifPresent(builder::maxAttempts);
    jsonAttemptParameters.getInteger("baseDelayMs").ifPresent(builder::baseDelayMs);

    return builder.get();
  }

  /**
   * Takes the first player configuration in the list and
   * maps `android.secondaryDisplayBehaviour` to the secondary display constant.
   * Defaults to `PlayerSDK.SECONDARY_DISPLAY`.
   *
   * @param jsonPlayerConfiguration The list of player configurations
   * @return the secondary display constant.
   */
  public static int toSecondaryDisplay(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) {
    Optional<String> jsonAndroidSecondaryDisplayBehaviour =
        Optional.of(jsonPlayerConfiguration)
          .flatMap(json -> json.getMap("android"))
          .flatMap(json -> json.getString("secondaryDisplayBehaviour"));

    if (jsonAndroidSecondaryDisplayBehaviour.isPresent()) {
      switch (jsonAndroidSecondaryDisplayBehaviour.get()) {
        case "never":
          return SdkConsts.SECONDARY_DISPLAY_NEVER;
        case "allowUnprotectedContent":
          return SdkConsts.SECONDARY_DISPLAY_ALLOW_UNPROTECTED_CONTENT;
        case "allowSecureDisplay":
          return SdkConsts.SECONDARY_DISPLAY_ALLOW_SECURE_DISPLAY;
        case "allowAlways":
          return SdkConsts.SECONDARY_DISPLAY_ALLOW_ALWAYS;
      }
    }

    Optional<String> jsonSecondaryDisplayBehaviour =
        jsonPlayerConfiguration.getString("secondaryDisplayBehaviour");

    if (jsonSecondaryDisplayBehaviour.isPresent()) {
      switch (jsonSecondaryDisplayBehaviour.get()) {
        case "never":
          return SdkConsts.SECONDARY_DISPLAY_NEVER;
        case "allowAlways":
          return SdkConsts.SECONDARY_DISPLAY_ALLOW_ALWAYS;
      }
    }

    return SdkConsts.SECONDARY_DISPLAY_NEVER;
  }

  @NonNull
  private static LiveConfiguration toLiveConfiguration(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) throws PrestoPlayError {
    LiveConfiguration.Builder builder = new LiveConfiguration.Builder();

    jsonPlayerConfiguration
        .getInteger("liveEdgeLatencyMs")
        .ifPresent(builder::liveEdgeLatencyMs);

    Optional<JsonMap> jsonAndroidLiveConfiguration = Optional.of(jsonPlayerConfiguration)
        .flatMap(json -> json.getMap("android"))
        .flatMap(json -> json.getMap("liveConfiguration"));

    if (jsonAndroidLiveConfiguration.isPresent()) {
      jsonAndroidLiveConfiguration
          .get()
          .getLong("availabilityStartTimeOffsetOverwriteMs")
          .ifPresent(builder::availabilityStartTimeOffsetOverwriteMs);
      jsonAndroidLiveConfiguration
          .get()
          .getMap("customUtcTimingElement")
          .ifPresent(jsonCustomUtcTimingElement -> {
            builder.customUtcTimingElement(
                toCustomUtcTimingElement(jsonCustomUtcTimingElement)
            );
          });
      jsonAndroidLiveConfiguration
          .get()
          .getInteger("hlsLiveTailSegmentIndex")
          .ifPresent(builder::hlsLiveTailSegmentIndex);
      jsonAndroidLiveConfiguration
          .get()
          .getLong("minManifestUpdatePeriodMs")
          .ifPresent(builder::minManifestUpdatePeriodMs);
      jsonAndroidLiveConfiguration
          .get()
          .getInteger("notifyManifestIntervalMs")
          .ifPresent(builder::notifyManifestIntervalMs);
      jsonAndroidLiveConfiguration
          .get()
          .getBoolean("snapToSegmentStart")
          .ifPresent(builder::snapToSegmentStart);
      jsonAndroidLiveConfiguration
          .get()
          .getLong("timeSyncSafetyMs")
          .ifPresent(builder::timesyncSafetyMs);

      Optional<Float> hlsPlaylistUpdateTargetDurationCoefficient =
          jsonAndroidLiveConfiguration
              .get()
              .getFloat("hlsPlaylistUpdateTargetDurationCoefficient");
      Optional<Boolean> hlsForcePlaylistUpdateTargetDuration =
          jsonAndroidLiveConfiguration
              .get()
              .getBoolean("hlsForcePlaylistUpdateTargetDuration");

      if (
        hlsPlaylistUpdateTargetDurationCoefficient.isPresent() &&
        hlsForcePlaylistUpdateTargetDuration.isPresent()
      ) {
        builder.hlsPlaylistUpdateTargetDurationCoefficient(
            hlsPlaylistUpdateTargetDurationCoefficient.get(),
            hlsForcePlaylistUpdateTargetDuration.get()
        );
      }

      Optional<JsonMap> jsonCatchupConfiguration = jsonAndroidLiveConfiguration
          .get()
          .getMap("catchupConfiguration");

      if (jsonCatchupConfiguration.isPresent()) {
        builder.catchupConfiguration(
            toCatchupConfiguration(jsonCatchupConfiguration.get())
        );
      }
    }

    return builder.get();
  }

  @NonNull
  private static CatchupConfiguration toCatchupConfiguration(
      final @NonNull JsonMap jsonCatchupConfiguration
  ) throws PrestoPlayError {
    String jsonType = jsonCatchupConfiguration
        .getString("type")
        .orElseThrow(
            invalidConfiguration("The type reference is required for the catchup configuration")
        );
    String jsonTimeReference = jsonCatchupConfiguration
        .getString("timeReference")
        .orElseThrow(
            invalidConfiguration("The time reference is required for the catchup configuration")
        );

    CatchupConfiguration.TimeReference timeReference;

    switch (jsonTimeReference) {
      case "mediaEnd":
        timeReference = CatchupConfiguration.TimeReference.MEDIA_END;
        break;
      case "bufferHead":
        timeReference = CatchupConfiguration.TimeReference.BUFFER_AHEAD;
        break;
      default:
        throw new IllegalArgumentException(
            "The time reference is required for the catchup configuration"
        );
    }

    CatchupConfiguration.Builder builder = new CatchupConfiguration.Builder(timeReference);

    switch (jsonType) {
      case "none":
        builder.none();
        break;
      case "seek":
        long lowerTimeThresholdMs1 = jsonCatchupConfiguration
            .getLong("lowerTimeThresholdMs")
            .orElseThrow(
                invalidConfiguration("The lower time threshold is required for the seek type")
            );
        long upperTimeThresholdMs1 = jsonCatchupConfiguration
            .getLong("upperTimeThresholdMs")
            .orElseThrow(
                invalidConfiguration("The upper time threshold is required for the seek type")
            );
        builder.seek(lowerTimeThresholdMs1, upperTimeThresholdMs1);
        break;
      case "speed":
        long lowerTimeThresholdMs2 = jsonCatchupConfiguration
            .getLong("lowerTimeThresholdMs")
            .orElseThrow(
                invalidConfiguration("The lower time threshold is required for the speed type")
            );
        long upperTimeThresholdMs2 = jsonCatchupConfiguration
            .getLong("upperTimeThresholdMs")
            .orElseThrow(
                invalidConfiguration("The upper time threshold is required for the speed type")
            );
        long fallbackTimeThresholdMs = jsonCatchupConfiguration
            .getLong("fallbackTimeThresholdMs")
            .orElseThrow(
                invalidConfiguration("The fallback time threshold is required for the speed type")
            );
        float speedCoefficient = jsonCatchupConfiguration
            .getLong("speedCoefficient")
            .orElseThrow(
                invalidConfiguration("The speed coefficient is required for the speed type")
            );

        builder.speed(
            lowerTimeThresholdMs2,
            upperTimeThresholdMs2,
            fallbackTimeThresholdMs,
            speedCoefficient
        );
        break;
      default:
        throw new IllegalArgumentException("The type is required for the catchup configuration");
    }

    jsonCatchupConfiguration
        .getBoolean("seekOnDownloadError")
        .ifPresent(builder::shouldSeekOnDownloadError);

    return builder.get();
  }

  @NonNull
  private static CustomUtcTimingElement toCustomUtcTimingElement(
      final @NonNull JsonMap jsonCustomUtcTimingElement
  ) {
    // type and timeReference are mandatory in JavaScript

    return new CustomUtcTimingElement(
        jsonCustomUtcTimingElement.getString("schemeIdUri").get(),
        jsonCustomUtcTimingElement.getString("value").get(),
        jsonCustomUtcTimingElement.getBoolean("force").get()
    );
  }


  /**
   * Maps the DRM configuration between React Native and Android SDK.
   *
   * @param selectedDrm          The current key system
   * @param jsonDrmConfiguration The DRM configuration of the React Native SDK
   * @return The DRM configuration of the Android SDK
   */
  @NonNull
  public static DrmConfiguration toDrmConfiguration(
      final @NonNull Drm selectedDrm,
      final @NonNull JsonMap jsonDrmConfiguration
  ) throws PrestoPlayError {
    JsonMap jsonDrmSystem;

    PrestoPlayErrorSupplier drmSystemErrorSupplier =
        invalidConfiguration("DRM system not defined, drm:'" + selectedDrm.name() + "'");

    switch (selectedDrm) {
      case Widevine:
        jsonDrmSystem = jsonDrmConfiguration
            .getMap("widevine")
            .orElseThrow(drmSystemErrorSupplier);
        break;
      case Playready:
        jsonDrmSystem = jsonDrmConfiguration
            .getMap("playReady")
            .orElseThrow(drmSystemErrorSupplier);
        break;
      default:
        jsonDrmSystem = null;
        break;
    }

    if (jsonDrmSystem == null) {
      throw new IllegalArgumentException(
          "DRM system not defined, drm:'" + selectedDrm.name() + "'");
    }

    String offlineId = jsonDrmConfiguration
        .getString("offlineId")
        .orElse(null);

    boolean allowClearLead = jsonDrmSystem
        .getBoolean("allowClearLead")
        .orElse(true);

    return new DrmConfiguration.Builder()
        .url(DrmUtils.urlPlaceholder) // NOTE: This is a placeholder, ReactNative SDK will set it in request modifier
        .playClearSamplesWithoutKeys(allowClearLead)
        .offlineId(offlineId)
        .get();
  }

  /**
   * Maps the sideloaded tracks between React Native and Android SDK.
   *
   * @param jsonPlayerConfiguration The player configuration of the React Native SDK
   * @return The sideloaded tracks for the Android SDK
   */
  @NonNull
  public static ArrayList<SideloadedTrack> toSideloadedTracks(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) throws PrestoPlayError {
    ArrayList<SideloadedTrack> sideloadedTracks = new ArrayList<>();

    Optional<JsonArray> jsonRemoteThumbnailTracks =
        jsonPlayerConfiguration.getArray("remoteThumbnailTracks");

    if (jsonRemoteThumbnailTracks.isPresent()) {
      for (int i = 0; i < jsonRemoteThumbnailTracks.get().size(); i++) {
        SideloadedTrack.ThumbnailBuilder sideloadedThumbnailTrackBuilder =
            new SideloadedTrack.ThumbnailBuilder();

        Optional<JsonMap> jsonRemoteThumbnailTrack = jsonRemoteThumbnailTracks.get().getMap(i);
        if (jsonRemoteThumbnailTrack.isPresent()) {
          String url = jsonRemoteThumbnailTrack
              .get()
              .getString("url")
              .orElseThrow(invalidConfiguration("Remote thumbnail track URL not defined or invalid"));

          sideloadedThumbnailTrackBuilder.url(url);

          jsonRemoteThumbnailTrack
              .get()
              .getString("mimeType")
              .ifPresent(sideloadedThumbnailTrackBuilder::mimeType);
          jsonRemoteThumbnailTrack
              .get()
              .getLong("gridWidth")
              .ifPresent(sideloadedThumbnailTrackBuilder::gridWidth);
          jsonRemoteThumbnailTrack
              .get()
              .getLong("gridHeight")
              .ifPresent(sideloadedThumbnailTrackBuilder::gridHeight);
          jsonRemoteThumbnailTrack
              .get()
              .getLong("intervalMs")
              .ifPresent(sideloadedThumbnailTrackBuilder::intervalMs);
        }

        sideloadedTracks.add(sideloadedThumbnailTrackBuilder.get());
      }
    }

    Optional<JsonArray> jsonRemoteTextTracks =
        jsonPlayerConfiguration.getArray("remoteTextTracks");

    if (jsonRemoteTextTracks.isPresent()) {
      for (int i = 0; i < jsonRemoteTextTracks.get().size(); i++) {
        Optional<JsonMap> jsonRemoteTextTrack = jsonRemoteTextTracks.get().getMap(i);

        SideloadedTrack.SubtitleBuilder sideloadedTextTrackBuilder =
            new SideloadedTrack.SubtitleBuilder();

        String url = jsonRemoteTextTrack
            .get()
            .getString("url")
            .orElseThrow(invalidConfiguration("Remote text track URL not defined or invalid"));

        sideloadedTextTrackBuilder.url(url);

        jsonRemoteTextTrack
            .get()
            .getString("mimeType")
            .ifPresent(sideloadedTextTrackBuilder::mimeType);
        jsonRemoteTextTrack
            .get()
            .getString("language")
            .ifPresent(sideloadedTextTrackBuilder::language);
        jsonRemoteTextTrack
            .get()
            .getString("label")
            .ifPresent(sideloadedTextTrackBuilder::label);

        sideloadedTracks.add(sideloadedTextTrackBuilder.get());
      }
    }

    return sideloadedTracks;
  }

  private static Bundle toBundle(@NonNull JsonMap jsonMap) {
    final Bundle bundle = new Bundle();

    Iterator<Map.Entry<String, Object>> it = jsonMap.iterator();

    while (it.hasNext()) {
      Map.Entry<String, Object> entry = it.next();

      String key = entry.getKey();
      Object value = entry.getValue();

      if (value instanceof String) {
        bundle.putString(key, (String) value);
      }

      // TODO add more types if needed
    }

    return bundle;
  }

  private static PrestoPlayErrorSupplier invalidConfiguration(@NonNull String message) {
    return new PrestoPlayErrorSupplier(
        new PrestoPlayError(
            ErrorSeverity.FATAL,
            ErrorCode.INVALID_CONFIGURATION,
            message
        )
    );
  }
}
