@preconcurrency import BBNativePlayerKit
@preconcurrency import React
import WebKit
@preconcurrency import bbnativeshared
import os
@preconcurrency import GoogleCast

// MARK: - Logging Helper
private enum LogLevel {
  case debug
  case info
  case warning
  case error

  var osLogType: OSLogType {
    switch self {
    case .debug: return .debug
    case .info: return .info
    case .warning: return .default
    case .error: return .error
    }
  }
}

private func log(_ message: String, level: LogLevel = .debug) {
  #if DEBUG
  // In debug builds, log everything
  NSLog("BBPlayer: \(message)")
  #else
  // In release builds, only log warnings and errors
  if level == .warning || level == .error {
    NSLog("BBPlayer: \(message)")
  }
  #endif
}

// MARK: - BBPlayerView
// Simplified architecture: BBPlayerView directly hosts BBNativePlayerView without intermediate view controller
// This reduces view hierarchy depth and eliminates extra layout passes for better performance/battery life

class BBPlayerView: UIView, BBNativePlayerViewDelegate {
  // Direct reference to SDK player view (no intermediate view controller)
  private var playerView: BBNativePlayerView?
  private var hasSetup: Bool = false

  private var isPlaying: Bool = false
  private var currentDuration: Double = 0.0
  private var isInFullscreen: Bool = false
  // Independent Google Cast button for showing the cast picker
  private var independentCastButton: GCKUICastButton?
  // Current embed URL, updated on each bb_embed_loaded (i.e., each loadWith* call)
  private var currentEmbedUrl: String?
  // Store parent view controller reference for SDK
  private weak var parentViewController: UIViewController?
  // Cached tag for deinit (nonisolated(unsafe) because deinit is nonisolated in Swift 6)
  nonisolated(unsafe) private var _cachedTag: Int?

  // MARK: - Props (set from React Native)

  @objc var jsonUrl: String = "" {
    didSet {
      setupPlayerIfNeeded()
    }
  }

  @objc var options: NSDictionary = [:] {
    didSet {
      setupPlayerIfNeeded()
    }
  }

  // MARK: - Event handlers (RCTDirectEventBlock)

  @objc var onDidFailWithError: RCTDirectEventBlock?
  @objc var onDidRequestCollapse: RCTDirectEventBlock?
  @objc var onDidRequestExpand: RCTDirectEventBlock?
  @objc var onDidRequestOpenUrl: RCTDirectEventBlock?
  @objc var onDidSetupWithJsonUrl: RCTDirectEventBlock?
  @objc var onDidTriggerAdError: RCTDirectEventBlock?
  @objc var onDidTriggerAdFinished: RCTDirectEventBlock?
  @objc var onDidTriggerAdLoaded: RCTDirectEventBlock?
  @objc var onDidTriggerAdLoadStart: RCTDirectEventBlock?
  @objc var onDidTriggerAdNotFound: RCTDirectEventBlock?
  @objc var onDidTriggerAdQuartile1: RCTDirectEventBlock?
  @objc var onDidTriggerAdQuartile2: RCTDirectEventBlock?
  @objc var onDidTriggerAdQuartile3: RCTDirectEventBlock?
  @objc var onDidTriggerAdStarted: RCTDirectEventBlock?
  @objc var onDidTriggerAllAdsCompleted: RCTDirectEventBlock?
  @objc var onDidTriggerAutoPause: RCTDirectEventBlock?
  @objc var onDidTriggerAutoPausePlay: RCTDirectEventBlock?
  @objc var onDidTriggerCanPlay: RCTDirectEventBlock?
  @objc var onDidTriggerCustomStatistics: RCTDirectEventBlock?
  @objc var onDidTriggerDurationChange: RCTDirectEventBlock?
  @objc var onDidTriggerEnded: RCTDirectEventBlock?
  @objc var onDidTriggerFullscreen: RCTDirectEventBlock?
  @objc var onDidTriggerMediaClipFailed: RCTDirectEventBlock?
  @objc var onDidTriggerMediaClipLoaded: RCTDirectEventBlock?
  @objc var onDidTriggerModeChange: RCTDirectEventBlock?
  @objc var onDidTriggerPause: RCTDirectEventBlock?
  @objc var onDidTriggerPhaseChange: RCTDirectEventBlock?
  @objc var onDidTriggerPlay: RCTDirectEventBlock?
  @objc var onDidTriggerPlaying: RCTDirectEventBlock?
  @objc var onDidTriggerProjectLoaded: RCTDirectEventBlock?
  @objc var onDidTriggerRetractFullscreen: RCTDirectEventBlock?
  @objc var onDidTriggerSeeked: RCTDirectEventBlock?
  @objc var onDidTriggerSeeking: RCTDirectEventBlock?
  @objc var onDidTriggerStall: RCTDirectEventBlock?
  @objc var onDidTriggerStateChange: RCTDirectEventBlock?
  @objc var onDidTriggerViewFinished: RCTDirectEventBlock?
  @objc var onDidTriggerViewStarted: RCTDirectEventBlock?
  @objc var onDidTriggerVolumeChange: RCTDirectEventBlock?
  @objc var onDidTriggerApiReady: RCTDirectEventBlock?

  override var intrinsicContentSize: CGSize {
    return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
  }

  private func setupPlayerIfNeeded() {
    guard !jsonUrl.isEmpty, window != nil, !hasSetup else { return }
    hasSetup = true
    setupPlayer()
  }

  override func didMoveToWindow() {
    super.didMoveToWindow()

    if window != nil {
      log("BBPlayerView.didMoveToWindow - view added to window")
      self.clipsToBounds = false

      // Optimize layer for video playback - reduce compositing overhead
      self.layer.isOpaque = true
      self.layer.drawsAsynchronously = true
      self.isOpaque = true

      // Register with view registry for command dispatch (supports New Architecture)
      if let tag = self.reactTag {
        let tagInt = tag.intValue
        _cachedTag = tagInt
        BBPlayerViewRegistry.shared.register(self, tag: tagInt)
      }

      // Find the parent view controller from the responder chain
      var responder = self.next
      while responder != nil {
        if let viewController = responder as? UIViewController {
          parentViewController = viewController
          break
        }
        responder = responder?.next
      }

      if parentViewController != nil {
        log("Found parent view controller: \(type(of: parentViewController!))")
      } else {
        log("WARNING - Could not find parent view controller!", level: .warning)
      }

      setupPlayerIfNeeded()
    } else {
      log("BBPlayerView.didMoveToWindow - view removed from window, isInFullscreen: \(isInFullscreen)")

      // Unregister from view registry when removed from window (unless in fullscreen)
      if !isInFullscreen, let tag = self.reactTag {
        BBPlayerViewRegistry.shared.unregister(tag: tag.intValue)
      }

      // No cleanup needed when not in fullscreen
    }
  }

  deinit {
    // Unregister from view registry using cached tag
    // (deinit is nonisolated in Swift 6, can't access @MainActor properties)
    if let tag = _cachedTag {
      BBPlayerViewRegistry.shared.unregister(tag: tag)
    }
  }


  // MARK: - Player Setup (Simplified - no intermediate view controller)

  func setupPlayer() {
    guard let parentVC = parentViewController else {
      log("ERROR - Cannot setup player without parent view controller", level: .error)
      return
    }

    log("BBPlayerView.setupPlayer() - creating player with jsonUrl: \(jsonUrl)")

    // Remove any existing player view
    playerView?.removeFromSuperview()

    // Convert options dictionary
    var optionsDict: [String: Any] = [:]
    if let opts = options as? [String: Any] {
      optionsDict = opts
    }

    // Create player view directly using SDK factory method
    // Pass the parent view controller so SDK can present fullscreen modals
    playerView = BBNativePlayer.createPlayerView(
      uiViewController: parentVC,
      frame: bounds,
      jsonUrl: jsonUrl,
      options: optionsDict
    )

    if let pv = playerView {
      // Add player view directly to BBPlayerView (no intermediate view controller)
      addSubview(pv)

      pv.translatesAutoresizingMaskIntoConstraints = false
      NSLayoutConstraint.activate([
        pv.topAnchor.constraint(equalTo: topAnchor),
        pv.leadingAnchor.constraint(equalTo: leadingAnchor),
        pv.trailingAnchor.constraint(equalTo: trailingAnchor),
        pv.bottomAnchor.constraint(equalTo: bottomAnchor)
      ])

      // Set ourselves as the delegate directly
      pv.delegate = self

      // Set castMode synchronously on the MediaController BEFORE resumeCastSession/play.
      // Options dict goes through the async web bridge — castMode may not be set yet when
      // the first play() fires, causing showPlayAlert() instead of auto-routing.
      if let castMode = optionsDict["castMode"] as? String {
        pv.setCastMode(castMode)
      }

      // Detect existing cast session (e.g. from standalone BBCastButton).
      // Matches Android BBPlayerView which calls resumeCastSession() after creation.
      pv.resumeCastSession()

      log("Player view created and added directly to BBPlayerView")
    } else {
      log("ERROR - playerView is nil after createPlayerView!", level: .error)
    }
  }

  // MARK: - BBNativePlayerViewDelegate Implementation

  func bbNativePlayerView(didRequestCollapse playerView: BBNativePlayerView) {
    onDidRequestCollapse?([:])
  }

  func bbNativePlayerView(didRequestExpand playerView: BBNativePlayerView) {
    onDidRequestExpand?([:])
  }

  func bbNativePlayerView(playerView: BBNativePlayerView, didFailWithError error: String?) {
    onDidFailWithError?(["payload": error as Any])
  }

  func didRequestOpenUrl(url: String?) {
    onDidRequestOpenUrl?(["payload": url as Any])
  }

  func didSetupWithJsonUrl(url: String?) {
    if let url = url, !url.isEmpty {
      currentEmbedUrl = url
      log("didSetupWithJsonUrl: \(url)")
    }
    onDidSetupWithJsonUrl?(["payload": url as Any])
  }

  func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerAdError error: String?) {
    onDidTriggerAdError?(["payload": error as Any])
  }

  func bbNativePlayerView(didTriggerAdFinished playerView: BBNativePlayerView) {
    onDidTriggerAdFinished?([:])
  }

  func bbNativePlayerView(didTriggerAdLoadStart playerView: BBNativePlayerView) {
    onDidTriggerAdLoadStart?([:])
  }

  func bbNativePlayerView(didTriggerAdLoaded playerView: BBNativePlayerView) {
    onDidTriggerAdLoaded?([:])
  }

  func bbNativePlayerView(didTriggerAdNotFound playerView: BBNativePlayerView) {
    onDidTriggerAdNotFound?([:])
  }

  func bbNativePlayerView(didTriggerAdQuartile1 playerView: BBNativePlayerView) {
    onDidTriggerAdQuartile1?([:])
  }

  func bbNativePlayerView(didTriggerAdQuartile2 playerView: BBNativePlayerView) {
    onDidTriggerAdQuartile2?([:])
  }

  func bbNativePlayerView(didTriggerAdQuartile3 playerView: BBNativePlayerView) {
    onDidTriggerAdQuartile3?([:])
  }

  func bbNativePlayerView(didTriggerAdStarted playerView: BBNativePlayerView) {
    onDidTriggerAdStarted?([:])
  }

  func bbNativePlayerView(didTriggerAllAdsCompleted playerView: BBNativePlayerView) {
    onDidTriggerAllAdsCompleted?([:])
  }

  func bbNativePlayerView(didTriggerAutoPause playerView: BBNativePlayerView) {
    onDidTriggerAutoPause?([:])
  }

  func bbNativePlayerView(didTriggerAutoPausePlay playerView: BBNativePlayerView) {
    onDidTriggerAutoPausePlay?([:])
  }

  func bbNativePlayerView(didTriggerCanPlay playerView: BBNativePlayerView) {
    onDidTriggerCanPlay?([:])
  }

  func bbNativePlayerView(
    didTriggerCustomStatistics playerView: BBNativePlayerView, ident: String, ev: String,
    aux: [String: String]
  ) {
    onDidTriggerCustomStatistics?([
      "ident": ident,
      "ev": ev,
      "aux": aux,
    ])
  }

  func bbNativePlayerView(
    playerView: BBNativePlayerView, didTriggerDurationChange duration: Double
  ) {
    currentDuration = duration
    onDidTriggerDurationChange?(["duration": duration as Any])
  }

  func bbNativePlayerView(didTriggerEnded playerView: BBNativePlayerView) {
    isPlaying = false
    onDidTriggerEnded?([:])
  }

  func bbNativePlayerView(didTriggerFullscreen playerView: BBNativePlayerView) {
    isInFullscreen = true
    log("FULLSCREEN ENTRY")

    // Enable landscape orientation for fullscreen
    if let orientationLockClass = NSClassFromString("OrientationLock") as? NSObject.Type {
      orientationLockClass.setValue(true, forKey: "isFullscreen")
    }

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
      guard let self = self, let parentVC = self.parentViewController else { return }
      if let presentedVC = parentVC.presentedViewController {
        if #available(iOS 16.0, *) {
          presentedVC.setNeedsUpdateOfSupportedInterfaceOrientations()
        }
        UIViewController.attemptRotationToDeviceOrientation()
      }
    }

    onDidTriggerFullscreen?([:])
  }

  func bbNativePlayerView(didTriggerMediaClipFailed playerView: BBNativePlayerView) {
    onDidTriggerMediaClipFailed?([:])
  }

  func bbNativePlayerView(
    playerView: BBNativePlayerView, didTriggerMediaClipLoaded data: MediaClip
  ) {
    onDidTriggerMediaClipLoaded?(data.toDictionary() as [String: Any])
  }

  func bbNativePlayerView(didTriggerModeChange playerView: BBNativePlayerView, mode: String?) {
    onDidTriggerModeChange?(["mode": mode as Any])
  }

  func bbNativePlayerView(didTriggerPause playerView: BBNativePlayerView) {
    isPlaying = false
    onDidTriggerPause?([:])
  }

  func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerPhaseChange phase: Phase?) {
    onDidTriggerPhaseChange?(["phase": (phase?.name ?? nil) as Any])
  }

  func bbNativePlayerView(didTriggerPlay playerView: BBNativePlayerView) {
    onDidTriggerPlay?([:])
  }

  func bbNativePlayerView(didTriggerPlaying playerView: BBNativePlayerView) {
    isPlaying = true
    onDidTriggerPlaying?([:])
  }

  func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerProjectLoaded data: Project) {
    onDidTriggerProjectLoaded?(data.toDictionary() as [String: Any])
  }

  func bbNativePlayerView(didTriggerRetractFullscreen playerView: BBNativePlayerView) {
    isInFullscreen = false
    log("FULLSCREEN EXIT")

    // Disable landscape orientation
    if let orientationLockClass = NSClassFromString("OrientationLock") as? NSObject.Type {
      orientationLockClass.setValue(false, forKey: "isFullscreen")
    }

    // Force rotation back to portrait
    if #available(iOS 16.0, *) {
      if let windowScene = window?.windowScene {
        windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
      }
    } else {
      UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
    }

    UIViewController.attemptRotationToDeviceOrientation()

    onDidTriggerRetractFullscreen?([:])
  }

  func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerSeeked seekOffset: Double) {
    onDidTriggerSeeked?(["payload": seekOffset as Any])
  }

  func bbNativePlayerView(didTriggerSeeking playerView: BBNativePlayerView) {
    onDidTriggerSeeking?([:])
  }

  func bbNativePlayerView(didTriggerStall playerView: BBNativePlayerView) {
    onDidTriggerStall?([:])
  }

  func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerStateChange state: State?) {
    onDidTriggerStateChange?(["state": (state?.name ?? nil) as Any])
  }

  func bbNativePlayerView(didTriggerViewFinished playerView: BBNativePlayerView) {
    onDidTriggerViewFinished?([:])
  }

  func bbNativePlayerView(didTriggerViewStarted playerView: BBNativePlayerView) {
    onDidTriggerViewStarted?([:])
  }

  func bbNativePlayerView(didTriggerVolumeChange playerView: BBNativePlayerView, volume: Double) {
    onDidTriggerVolumeChange?([
      "volume": volume,
      "muted": (volume == 0.0)
    ])
  }

  func bbNativePlayerView(didTriggerApiReady playerView: BBNativePlayerView) {
    onDidTriggerApiReady?([:])
  }

  // MARK: - Cast Button Setup

  private func setupIndependentCastButton() {
    if !GCKCastContext.isSharedInstanceInitialized() {
      log("ERROR - Cannot create cast button: Google Cast SDK not initialized yet", level: .error)
      return
    }

    independentCastButton = GCKUICastButton(frame: CGRect(x: -1000, y: -1000, width: 1, height: 1))

    guard let castButton = independentCastButton else {
      log("ERROR - Failed to create independent cast button", level: .error)
      return
    }

    castButton.alpha = 0.0
    castButton.isUserInteractionEnabled = false
    addSubview(castButton)
  }

  // MARK: - Public API Methods

  func adMediaHeight() -> Int? {
    return playerView?.player.adMediaHeight
  }

  func adMediaWidth() -> Int? {
    return playerView?.player.adMediaWidth
  }

  func clipData() -> Any? {
    return playerView?.player.clipData?.toDictionary()
  }

  func controls() -> Bool? {
    return nil
  }

  func duration() -> Double? {
    return playerView?.player.duration
  }

  func inView() -> Bool? {
    return playerView?.player.inView
  }

  func setInView(_ inView: Bool) {
    playerView?.player.inView = inView
  }

  func mode() -> String? {
    return playerView?.player.mode
  }

  func muted() -> Bool? {
    return playerView?.player.muted
  }

  func phase() -> Any? {
    return playerView?.player.phase?.name
  }

  func playoutData() -> Any? {
    return playerView?.player.playoutData?.toDictionary()
  }

  func projectData() -> Any? {
    return playerView?.player.projectData?.toDictionary()
  }

  func state() -> Any? {
    return playerView?.player.state?.name
  }

  func volume() -> Float? {
    return playerView?.player.volume
  }

  func isCastingValue() -> Bool? {
    return playerView?.player.isCasting
  }

  func castDeviceName() -> String? {
    guard GCKCastContext.isSharedInstanceInitialized() else { return nil }
    return GCKCastContext.sharedInstance().sessionManager.currentCastSession?.device.friendlyName
  }

  func endCastSession() {
    guard GCKCastContext.isSharedInstanceInitialized() else { return }
    GCKCastContext.sharedInstance().sessionManager.endSessionAndStopCasting(true)
  }

  func autoPlayNextCancel() {
    playerView?.player.autoPlayNextCancel()
  }

  func collapse() {
    playerView?.player.collapse()
  }

  func expand() {
    playerView?.player.expand()
  }

  func presentModal() {
    guard let parentVC = parentViewController else {
      log("Cannot present modal - no parent view controller", level: .warning)
      return
    }
    isInFullscreen = true
    playerView?.player.enterFullScreen()
  }

  func closeModal() {
    playerView?.player.closeModalPlayer()
    isInFullscreen = false
  }

  func enterFullscreen() {
    enterFullscreenWithLandscapeForce(forceLandscape: false)
  }

  func enterFullscreenLandscape() {
    enterFullscreenWithLandscapeForce(forceLandscape: true)
  }

  private func enterFullscreenWithLandscapeForce(forceLandscape: Bool) {
    // Set goingFullScreen flag on BBNativePlayerViewController for proper orientation support
    if let player = playerView?.player as? NSObject {
      if let bbViewController = player.value(forKey: "bbNativePlayerViewController") as? NSObject {
        bbViewController.setValue(true, forKey: "goingFullScreen")
        log("Set goingFullScreen = true on BBNativePlayerViewController", level: .info)
      }
    }

    playerView?.player.enterFullScreen()

    if forceLandscape {
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
        if #available(iOS 16.0, *) {
          if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape))
            log("Requested landscape rotation", level: .info)
          }
        }
      }
    }
  }

  func exitFullscreen() {
    playerView?.player.exitFullScreen()
  }

  func destroy() {
    // iOS SDK cleans up automatically when view is removed
  }

  func unload() {
    playerView?.player.unload()
  }

  func updatePlayout(_ playoutJson: String) {
    guard playerView != nil else {
      NSLog("BBPlayerView.updatePlayout ERROR - playerView not initialized")
      return
    }
    playerView?.player.updatePlayoutWithJson(playoutJson: playoutJson)
  }

  func pause() {
    playerView?.player.pause()
  }

  func play() {
    playerView?.player.play()
  }

  func seek(_ offsetInSeconds: Int) {
    playerView?.player.seek(offsetInSeconds: offsetInSeconds as NSNumber)
  }

  func seekRelative(_ offsetInSeconds: Double) {
    playerView?.player.seekRelative(offsetInSeconds: offsetInSeconds as NSNumber)
  }

  func setMuted(_ muted: Bool) {
    playerView?.setApiProperty(property: .muted, value: muted)
  }

  func setVolume(_ volume: Double) {
    playerView?.setApiProperty(property: .volume, value: Float(volume))
  }

  // Helper to parse context JSON into a dictionary for the native SDK
  private func parseContext(_ contextJson: String?) -> [String: Any]? {
    guard let jsonString = contextJson, !jsonString.isEmpty else { return nil }
    guard let data = jsonString.data(using: .utf8) else { return nil }
    do {
      if let dict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
        return dict
      }
    } catch {
      log("Failed to parse context JSON: \(error)", level: .warning)
    }
    return nil
  }

  func loadWithClipId(_ clipId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
    let context = parseContext(contextJson)
    playerView?.player.loadWithClipId(clipId: clipId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
  }

  func loadWithClipListId(_ clipListId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
    let context = parseContext(contextJson)
    playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
  }

  func loadWithProjectId(_ projectId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
    let context = parseContext(contextJson)
    playerView?.player.loadWithProjectId(projectId: projectId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
  }

  func loadWithClipJson(_ clipJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
    let context = parseContext(contextJson)
    playerView?.player.loadWithClipJson(clipJson: clipJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
  }

  func loadWithClipListJson(_ clipListJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
    let context = parseContext(contextJson)
    playerView?.player.loadWithClipListJson(clipListJson: clipListJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
  }

  func loadWithProjectJson(_ projectJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
    let context = parseContext(contextJson)
    playerView?.player.loadWithProjectJson(projectJson: projectJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
  }

  /**
   * Load content by explicit content ID and type. Preferred over loadWithJsonUrl.
   *
   * - Parameters:
   *   - contentId: numeric content ID string
   *   - contentType: "mediaclip", "mediacliplist", or "project" (short forms "c", "l", "p"/"pj" also accepted)
   *   - autoPlay: whether to start playback immediately
   *   - contextJson: optional context JSON string
   */
  func loadWithContentIdAndType(_ contentId: String, contentType: String, autoPlay: Bool, contextJson: String? = nil) {
    guard playerView != nil else {
      NSLog("BBPlayerView.loadWithContentIdAndType ERROR - playerView not initialized")
      return
    }
    let isCasting = playerView?.player.isCasting ?? false
    NSLog("BBPlayerView.loadWithContentIdAndType called - id: %@, type: %@, autoPlay: %d, isCasting: %d", contentId, contentType, autoPlay, isCasting)
    let context = parseContext(contextJson)
    switch contentType.lowercased() {
    case "mediacliplist", "l":
      playerView?.player.loadWithClipListId(clipListId: contentId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
    case "project", "p", "pj":
      playerView?.player.loadWithProjectId(projectId: contentId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
    case "mediaclip", "c":
      playerView?.player.loadWithClipId(clipId: contentId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
    default:
      NSLog("BBPlayerView.loadWithContentIdAndType ERROR - unknown contentType '%@'", contentType)
      onDidFailWithError?(["payload": "Unknown contentType: '\(contentType)'. Expected one of: mediaclip, mediacliplist, project"])
    }
  }

  /**
   * Convenience method: load content from a BB JSON URL.
   * Parses the URL to extract content ID and type, then delegates to loadWithContentIdAndType.
   * Note: Shorts URLs (/sh/{id}.json) are NOT supported — use BBShortsView instead.
   */
  func loadWithJsonUrl(_ url: String, autoPlay: Bool, contextJson: String? = nil) {
    NSLog("BBPlayerView.loadWithJsonUrl called - url: %@, autoPlay: %d", url, autoPlay)
    guard playerView != nil else {
      NSLog("BBPlayerView.loadWithJsonUrl ERROR - playerView not initialized")
      return
    }

    let shortsPattern = "/sh/([0-9]+)\\.json"
    let clipListPattern = "/l/([0-9]+)\\.json|/mediacliplist/([0-9]+)"
    let projectPattern = "/pj/([0-9]+)\\.json|/p/[^/]+/p/([0-9]+)\\.json|/project/([0-9]+)"
    let clipPattern = "/c/([0-9]+)\\.json|/mediaclip/([0-9]+)"

    if url.range(of: shortsPattern, options: .regularExpression) != nil {
      log("ERROR - Shorts URLs are not supported in BBPlayerView. Use BBShortsView instead.", level: .error)
      onDidFailWithError?(["payload": "Shorts URLs are not supported in BBPlayerView. Use BBShortsView instead."])
      return
    }

    if url.range(of: clipListPattern, options: .regularExpression) != nil {
      if let id = extractIdFromUrl(url, pattern: clipListPattern) {
        loadWithContentIdAndType(id, contentType: "mediacliplist", autoPlay: autoPlay, contextJson: contextJson)
      } else {
        NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract cliplist ID from URL: %@", url)
        onDidFailWithError?(["payload": "Cannot load content: failed to extract ID from URL"])
      }
      return
    }

    if url.range(of: projectPattern, options: .regularExpression) != nil {
      if let id = extractIdFromUrl(url, pattern: projectPattern) {
        loadWithContentIdAndType(id, contentType: "project", autoPlay: autoPlay, contextJson: contextJson)
      } else {
        NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract project ID from URL: %@", url)
        onDidFailWithError?(["payload": "Cannot load content: failed to extract ID from URL"])
      }
      return
    }

    if url.range(of: clipPattern, options: .regularExpression) != nil {
      if let id = extractIdFromUrl(url, pattern: clipPattern) {
        loadWithContentIdAndType(id, contentType: "mediaclip", autoPlay: autoPlay, contextJson: contextJson)
      } else {
        NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract clip ID from URL: %@", url)
        onDidFailWithError?(["payload": "Cannot load content: failed to extract ID from URL"])
      }
      return
    }

    NSLog("BBPlayerView.loadWithJsonUrl ERROR - Unknown URL format, cannot extract ID: %@", url)
    onDidFailWithError?(["payload": "Cannot load content: unsupported URL format"])
  }

  /**
   * Helper to extract numeric ID from URL using regex pattern with capture groups
   */
  private func extractIdFromUrl(_ url: String, pattern: String) -> String? {
    do {
      let regex = try NSRegularExpression(pattern: pattern, options: [])
      let range = NSRange(url.startIndex..., in: url)
      if let match = regex.firstMatch(in: url, options: [], range: range) {
        // Try each capture group (pattern has multiple alternatives with |)
        for i in 1..<match.numberOfRanges {
          if let groupRange = Range(match.range(at: i), in: url) {
            let extracted = String(url[groupRange])
            if !extracted.isEmpty {
              return extracted
            }
          }
        }
      }
    } catch {
      log("ERROR - Regex error: \(error)", level: .error)
    }
    return nil
  }

  func showCastPicker() {
    log("showCastPicker called")

    if !GCKCastContext.isSharedInstanceInitialized() {
      log("ERROR - showCastPicker called but Google Cast SDK not initialized yet", level: .error)
      return
    }

    // Set the GCK playerId so the native player's sessionStarted handler
    // recognises this player instance and auto-loads media onto the cast device.
    // Mirrors what BottomSheetCastViewController.activateChromeCastSelection() does internally.
    // Use currentEmbedUrl (updated on each clip load) over the static jsonUrl prop.
    let playerId = currentEmbedUrl ?? (jsonUrl.isEmpty ? nil : jsonUrl)
    if let playerId = playerId {
      log("showCastPicker setting GCKPlayerId: \(playerId)")
      UserDefaults.standard.set(playerId, forKey: "com.bluebillywig.bbnativeplayerkit.GCKPlayerId")
    } else {
      log("WARNING - showCastPicker: no playerId available, clearing stale GCKPlayerId", level: .warning)
      UserDefaults.standard.removeObject(forKey: "com.bluebillywig.bbnativeplayerkit.GCKPlayerId")
    }

    if independentCastButton == nil {
      setupIndependentCastButton()
    }

    guard let castButton = independentCastButton else {
      log("ERROR - independentCastButton is nil in showCastPicker", level: .error)
      return
    }

    DispatchQueue.main.async {
      log("showCastPicker triggering independent GCKUICastButton")
      castButton.sendActions(for: .touchUpInside)
    }
  }
}
