// Copyright 2015-present 650 Industries. All rights reserved.

import Foundation
import React

#if os(iOS) || os(tvOS)

/**
 Re-feeds scene events to the app delegate. Under the scene life cycle UIKit no longer calls the
 app delegate's URL, user-activity, life-cycle and quick-action methods, so overrides in the app's
 `AppDelegate: ExpoAppDelegate` would never run; `ExpoAppDelegate` forwards every event to the
 subscribers, so one call reaches both.

 Main-actor isolated: `UIWindowSceneDelegate` hands these events over on the main actor, and every
 `ExpoAppDelegate` method they reach is main-actor isolated too.
 */
@MainActor
@available(iOSApplicationExtension, unavailable)
struct SceneEventForwarder {
  var appDelegate: () -> ExpoAppDelegate? = { UIApplication.shared.delegate as? ExpoAppDelegate }

  func open(url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) {
    let application = UIApplication.shared
    let delegate = appDelegate()

    notifyLinkingManagerUnlessAlreadyNotified(of: url) {
      _ = delegate?.application(application, open: url, options: options)
    } notify: {
      RCTLinkingManager.application(application, open: url, options: options)
    }
  }

  func `continue`(_ userActivity: NSUserActivity) {
    let application = UIApplication.shared
    let delegate = appDelegate()

    // `RCTLinkingManager` only announces browsing-web activities, so there is nothing to dedupe
    // against for the other activity types.
    notifyLinkingManagerUnlessAlreadyNotified(of: userActivity.webpageURL) {
      _ = delegate?.application(application, continue: userActivity, restorationHandler: { _ in })
    } notify: {
      RCTLinkingManager.application(application, continue: userActivity, restorationHandler: { _ in })
    }
  }

  func willContinueUserActivity(withType userActivityType: String) {
    // The scene callback has no return value, unlike its app-delegate counterpart. Calling the app
    // delegate still lets every subscriber prepare; its aggregated result is intentionally ignored.
    _ = appDelegate()?.application(
      UIApplication.shared,
      willContinueUserActivityWithType: userActivityType
    )
  }

  func didFailToContinueUserActivity(withType userActivityType: String, error: Error) {
    appDelegate()?.application(
      UIApplication.shared,
      didFailToContinueUserActivityWithType: userActivityType,
      error: error
    )
  }

  func didUpdate(_ userActivity: NSUserActivity) {
    appDelegate()?.application(UIApplication.shared, didUpdate: userActivity)
  }

  func didBecomeActive() {
    appDelegate()?.applicationDidBecomeActive(UIApplication.shared)
  }

  func willResignActive() {
    appDelegate()?.applicationWillResignActive(UIApplication.shared)
  }

  func willEnterForeground() {
    appDelegate()?.applicationWillEnterForeground(UIApplication.shared)
  }

  func didEnterBackground() {
    appDelegate()?.applicationDidEnterBackground(UIApplication.shared)
  }

#if os(iOS)
  /// UIKit waits on `completionHandler`, so it has to run exactly once.
  func perform(
    _ shortcutItem: UIApplicationShortcutItem,
    completionHandler: @escaping (Bool) -> Void
  ) {
    guard let delegate = appDelegate() else {
      completionHandler(false)
      return
    }
    delegate.application(
      UIApplication.shared,
      performActionFor: shortcutItem,
      completionHandler: completionHandler
    )
  }
#endif

  /// Runs `body`, then calls `notify` unless `RCTLinkingManager` was already notified about `url`
  /// while `body` ran. Always calls `notify` when `url` is `nil`.
  ///
  /// App delegates generated by SDK 57 and older call `RCTLinkingManager` themselves, which would
  /// deliver the JS `url` event twice now that their overrides run again, and no static check can
  /// tell such an override apart — so listen for what `RCTLinkingManager` announces instead.
  private func notifyLinkingManagerUnlessAlreadyNotified(
    of url: URL?,
    during body: () -> Void,
    notify: () -> Void
  ) {
    guard let url else {
      body()
      notify()
      return
    }
    let observer = LinkingManagerObserver(url: url)
    body()
    if !observer.wasNotified {
      notify()
    }
  }
}

/// Observes the notification `RCTLinkingManager` posts for every link it handles. The name is
/// private to React Native's `RCTLinkingManager.mm` — expo-router spells it out the same way in
/// `ExpoHeadAppDelegateSubscriber.swift`. If React Native renames it, legacy overrides deliver a
/// duplicate `url` event again; a link is never lost.
private final class LinkingManagerObserver: NSObject {
  private let expectedURL: String
  private(set) var wasNotified = false

  init(url: URL) {
    expectedURL = url.absoluteString
    super.init()
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(linkingManagerDidOpenURL(_:)),
      name: Notification.Name("RCTOpenURLNotification"),
      object: nil
    )
  }

  deinit {
    NotificationCenter.default.removeObserver(self)
  }

  @objc
  private func linkingManagerDidOpenURL(_ notification: Notification) {
    wasNotified = wasNotified || notification.userInfo?["url"] as? String == expectedURL
  }
}

#endif
