import ExpoModulesCore
import SafariServices
import UIKit

/// Native view that hosts the children and attaches a
/// `UIContextMenuInteraction`. JS-shipped props drive behaviour:
/// - `preview`: discriminated union describing what to show during peek
/// - `menuItems`: array of menu item specs (see `MenuBuilder`)
/// - `previewCornerRadius`: used for the targeted preview's visible path so the
///   lift animation matches the thumbnail's clipping. (Named distinctly from
///   the RN-owned `borderRadius` style prop on UIView.)
/// A `link` preview with `useInAppBrowser` peeks a live SFSafariViewController
/// that morphs to full-screen on commit; `browserToolbarColor`/`browserControlsColor`
/// (read from the preview spec) tint it (see `previewProvider`).
class ExpoBlueskyPeekMenuView: ExpoView, UIContextMenuInteractionDelegate,
  SFSafariViewControllerDelegate
{
  private var preview: [String: Any]?
  private var menuItems: [[String: Any]] = []
  private var previewCornerRadius: CGFloat = 0

  // The Safari VC returned from `previewProvider` for a `link` peek. UIKit owns
  // its lifetime (preview container → presented on commit → released on
  // cancel), so this is weak. Built for any valid `link` so the live page
  // always peeks; whether we morph into it on commit is `shouldMorphToBrowser`.
  private weak var liveSafariVC: SFSafariViewController?
  // True when the `link` peek should morph into the in-app browser on commit.
  // When false, the same Safari preview shows, but the tap just dismisses and
  // forwards to JS via `onPreviewPress` (host's default open behavior).
  private var shouldMorphToBrowser = false

  private let onItemPress = EventDispatcher()
  private let onPreviewPress = EventDispatcher()

  required init(appContext: AppContext? = nil) {
    super.init(appContext: appContext)
    let interaction = UIContextMenuInteraction(delegate: self)
    self.addInteraction(interaction)
  }

  // RN layout can leave bounds at fractional-pixel values. The targeted preview
  // snapshots this view's bounds for its return animation, and subpixel mismatches
  // cause a visible glitch when the preview shrinks back into the thumbnail.
  // Snapping to whole-pixel values prevents that.
  override var bounds: CGRect {
    get {
      let b = super.bounds
      let s = self.window?.screen.scale ?? UIScreen.main.scale
      return CGRect(
        x: b.origin.x,
        y: b.origin.y,
        width: round(b.width * s) / s,
        height: round(b.height * s) / s
      )
    }
    set { super.bounds = newValue }
  }

  func setPreview(_ value: [String: Any]?) { self.preview = value }
  func setMenuItems(_ value: [[String: Any]]) { self.menuItems = value }
  func setPreviewCornerRadius(_ value: Double) {
    self.previewCornerRadius = CGFloat(value)
  }

  // MARK: - UIContextMenuInteractionDelegate

  func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    configurationForMenuAtLocation location: CGPoint
  ) -> UIContextMenuConfiguration? {
    let previewSpec = self.preview
    let items = self.menuItems

    return UIContextMenuConfiguration(
      identifier: nil,
      previewProvider: { [weak self] in
        guard let self = self else { return nil }
        // A `link` always peeks the live page in an SFSafariViewController. On
        // commit it either morphs into the browser or just dismisses + forwards
        // to JS, per `shouldMorphToBrowser` (see `willPerformPreviewAction`).
        if let safari = self.makeLiveSafariController(from: previewSpec) {
          self.liveSafariVC = safari
          self.shouldMorphToBrowser =
            (previewSpec?["useInAppBrowser"] as? Bool) == true
          return safari
        }
        // Otherwise: image controller, or nil → UIKit lifts the source view.
        // The commit is forwarded to JS via `onPreviewPress`.
        return PreviewFactory.makeController(from: previewSpec)
      },
      actionProvider: { [weak self] _ in
        guard let self = self else { return nil }
        return MenuBuilder.build(items: items) { [weak self] id in
          self?.onItemPress(["id": id])
        }
      }
    )
  }

  /// Builds the live Safari preview for a `link` with a web (http/https) URL.
  /// Returns nil for every other case so the caller falls through to
  /// `PreviewFactory`. Whether the commit morphs into this browser is decided
  /// separately by `shouldMorphToBrowser`.
  private func makeLiveSafariController(
    from spec: [String: Any]?
  ) -> SFSafariViewController? {
    guard let spec = spec,
          spec["type"] as? String == "link",
          let urlString = spec["url"] as? String,
          let url = URL(string: urlString),
          let scheme = url.scheme?.lowercased(),
          scheme == "http" || scheme == "https"
    else { return nil }

    let config = SFSafariViewController.Configuration()
    let safari = SFSafariViewController(url: url, configuration: config)
    safari.delegate = self
    // SFSafariViewController's own view is transparent until the page paints,
    // so the peek bubble flashes through to whatever is behind it. A dark-mode
    // aware background fills that gap.
    safari.view.backgroundColor = .systemBackground
    // Tints must be set before presentation; ignored once presented.
    if let toolbar = color(from: spec["browserToolbarColor"]) {
      safari.preferredBarTintColor = toolbar
    }
    if let controls = color(from: spec["browserControlsColor"]) {
      safari.preferredControlTintColor = controls
    }
    return safari
  }

  /// Parses a `#rgb`/`#rrggbb`/`#rrggbbaa` hex string into a UIColor. Colors
  /// arrive as raw strings here (they're nested in the `preview` dict rather
  /// than a top-level `Prop`, so ExpoModulesCore doesn't coerce them).
  private func color(from value: Any?) -> UIColor? {
    guard var hex = value as? String else { return nil }
    if hex.hasPrefix("#") { hex.removeFirst() }
    if hex.count == 3 {  // expand shorthand #rgb → #rrggbb
      hex = hex.map { "\($0)\($0)" }.joined()
    }
    guard hex.count == 6 || hex.count == 8,
          let int = UInt64(hex, radix: 16) else { return nil }
    let hasAlpha = hex.count == 8
    let r = CGFloat((int >> (hasAlpha ? 24 : 16)) & 0xff) / 255
    let g = CGFloat((int >> (hasAlpha ? 16 : 8)) & 0xff) / 255
    let b = CGFloat((int >> (hasAlpha ? 8 : 0)) & 0xff) / 255
    let a = hasAlpha ? CGFloat(int & 0xff) / 255 : 1
    return UIColor(red: r, green: g, blue: b, alpha: a)
  }

  func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration
  ) -> UITargetedPreview? {
    return makeTargetedPreview()
  }

  func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration
  ) -> UITargetedPreview? {
    return makeTargetedPreview()
  }

  func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
    animator: UIContextMenuInteractionCommitAnimating
  ) {
    // A live Safari peek with the in-app browser enabled morphs into the
    // browser: `.pop` plays the morph animation, but UIKit does NOT present the
    // preview VC for us — it tears the preview container down at completion. So
    // we present the same (now-detached) Safari instance in the completion. The
    // morph has already filled the screen, so present without animation to snap
    // it in seamlessly rather than slide twice.
    if let safari = liveSafariVC, shouldMorphToBrowser {
      animator.preferredCommitStyle = .pop
      animator.addCompletion { [weak self] in
        self?.topmostViewController()?.present(safari, animated: false)
      }
      return
    }
    // Otherwise (image preview, lifted source view, or a `link` peek with the
    // in-app browser disabled): just dismiss and let JS handle the tap.
    self.onPreviewPress([:])
  }

  /// Walks from the key window's root to the topmost presented controller —
  /// the one we can safely present Safari from. Mirrors expo-web-browser.
  private func topmostViewController() -> UIViewController? {
    let scene = UIApplication.shared.connectedScenes
      .compactMap { $0 as? UIWindowScene }
      .first { $0.activationState == .foregroundActive }
      ?? UIApplication.shared.connectedScenes
        .compactMap { $0 as? UIWindowScene }.first
    let keyWindow = scene?.windows.first { $0.isKeyWindow } ?? scene?.windows.first
    var top = keyWindow?.rootViewController
    while let presented = top?.presentedViewController {
      top = presented
    }
    return top
  }

  func contextMenuInteraction(
    _ interaction: UIContextMenuInteraction,
    willEndFor configuration: UIContextMenuConfiguration,
    animator: UIContextMenuInteractionAnimating?
  ) {
    // The menu is ending (dismissed or committed). Drop our weak reference; on
    // commit the `addCompletion` closure retains the Safari instance until it's
    // presented, after which UIKit owns it.
    self.liveSafariVC = nil
    self.shouldMorphToBrowser = false
  }

  // MARK: - SFSafariViewControllerDelegate

  func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
    // Safari dismisses itself; nothing to forward to JS.
    self.liveSafariVC = nil
  }

  // MARK: - Targeted preview

  /// The targeted preview uses the view itself as target with a rounded-corner
  /// visible path matching the thumbnail's clipping, so the lift animation
  /// respects the existing corner radius.
  private func makeTargetedPreview() -> UITargetedPreview {
    let parameters = UIPreviewParameters()
    parameters.backgroundColor = .clear
    if previewCornerRadius > 0 {
      parameters.visiblePath = UIBezierPath(
        roundedRect: self.bounds,
        cornerRadius: previewCornerRadius
      )
    }
    return UITargetedPreview(view: self, parameters: parameters)
  }
}
