// generated by bun run generate from the SwiftUI SDK and codegen/catalog.ts.
// edit the generator or catalog, then regenerate.
import SwiftUI
import UIKit
import WebKit

private final class WebViewModel: ObservableObject {
  @Published var url: String = ""
  @Published var html: String = ""
  @Published var script: String = ""
  @Published var command: String = ""
  @Published var commandRevision: Double = 0
  @Published var commandValue: String = ""
  @Published var limitsNavigationsToAppBoundDomains: Bool = false
  @Published var inlineMedia: Bool = false
  @Published var inspectable: Bool = false
  @Published var bounces: Bool = true
  @Published var backForwardNavigationGestures: String = ""
  @Published var magnificationGestures: String = ""
  @Published var linkPreviews: String = ""
  @Published var elementFullscreen: String = ""
  @Published var contentBackground: String = ""
  @Published var accessibility = OneNativeAccessibility()
  @Published var swiftStyle = OneNativeStyle()
  var active = false
  var onSDKEvent: ((String, String) -> Void)?
  func emitSDKEvent(_ name: String, _ value: String) { if active { onSDKEvent?(name, value) } }

  var onNavigate: ((String, Int) -> Void)?
  private var navigateCount = 0
  func navigate(_ url: String) {
    guard active else { return }
    navigateCount += 1
    onNavigate?(url, navigateCount)
  }
  var onTitleChange: ((String, Int) -> Void)?
  private var titleChangeCount = 0
  func titleChange(_ title: String) {
    guard active else { return }
    titleChangeCount += 1
    onTitleChange?(title, titleChangeCount)
  }
  var onLoadingChange: ((Bool, Double, Int) -> Void)?
  private var loadingChangeCount = 0
  func loadingChange(_ loading: Bool, _ progress: Double) {
    guard active else { return }
    loadingChangeCount += 1
    onLoadingChange?(loading, progress, loadingChangeCount)
  }
  var onLoadStart: ((Int) -> Void)?
  private var loadStartCount = 0
  func loadStart() {
    guard active else { return }
    loadStartCount += 1
    onLoadStart?(loadStartCount)
  }
  var onLoadEnd: ((Int) -> Void)?
  private var loadEndCount = 0
  func loadEnd() {
    guard active else { return }
    loadEndCount += 1
    onLoadEnd?(loadEndCount)
  }
  var onError: ((String, Int) -> Void)?
  private var errorCount = 0
  func error(_ message: String) {
    guard active else { return }
    errorCount += 1
    onError?(message, errorCount)
  }
  var onHttpError: ((Double, Int) -> Void)?
  private var httpErrorCount = 0
  func httpError(_ statusCode: Double) {
    guard active else { return }
    httpErrorCount += 1
    onHttpError?(statusCode, httpErrorCount)
  }
  var onMessage: ((String, Int) -> Void)?
  private var messageCount = 0
  func message(_ data: String) {
    guard active else { return }
    messageCount += 1
    onMessage?(data, messageCount)
  }
  var onHistoryChange: ((Bool, Bool, Int) -> Void)?
  private var historyChangeCount = 0
  func historyChange(_ canGoBack: Bool, _ canGoForward: Bool) {
    guard active else { return }
    historyChangeCount += 1
    onHistoryChange?(canGoBack, canGoForward, historyChangeCount)
  }
  var onProcessTerminate: ((Int) -> Void)?
  private var processTerminateCount = 0
  func processTerminate() {
    guard active else { return }
    processTerminateCount += 1
    onProcessTerminate?(processTerminateCount)
  }
}
@objcMembers public final class OneNativeWebViewView: UIView, OneNativeComposable {
  public var onSDKEvent: ((String, String) -> Void)?

  public var onNavigate: ((String, Int) -> Void)?
  public var onTitleChange: ((String, Int) -> Void)?
  public var onLoadingChange: ((Bool, Double, Int) -> Void)?
  public var onLoadStart: ((Int) -> Void)?
  public var onLoadEnd: ((Int) -> Void)?
  public var onError: ((String, Int) -> Void)?
  public var onHttpError: ((Double, Int) -> Void)?
  public var onMessage: ((String, Int) -> Void)?
  public var onHistoryChange: ((Bool, Bool, Int) -> Void)?
  public var onProcessTerminate: ((Int) -> Void)?
  private var model = WebViewModel()
  private var controller: OneNativeHostingController<OneNativeStandalone<WebViewContent>>?
  public override init(frame: CGRect) { super.init(frame: frame) }
  required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
  public func configureAccessibility(_ label: String, hint: String, value: String, identifier: String) {
    let next = OneNativeAccessibility(label: label, hint: hint, value: value, identifier: identifier)
    if model.accessibility != next { model.accessibility = next }
  }
  public func configureStyle(_ style: [String: Any]) {
    let next = OneNativeStyle(dictionary: style)
    if model.swiftStyle != next {
      let previousRows = model.swiftStyle.sdkModifiers.filter { $0.first?.hasPrefix("listRow") == true }
      let nextRows = next.sdkModifiers.filter { $0.first?.hasPrefix("listRow") == true }
      model.swiftStyle = next
      if previousRows != nextRows { compositionParent?.refreshRow(for: self) }
    }
  }
  public func configure(_ url: String, html: String, script: String, command: String, commandRevision: Double, commandValue: String, limitsNavigationsToAppBoundDomains: Bool, inlineMedia: Bool, inspectable: Bool, bounces: Bool, backForwardNavigationGestures: String, magnificationGestures: String, linkPreviews: String, elementFullscreen: String, contentBackground: String) {
    if model.url != url { model.url = url }
    if model.html != html { model.html = html }
    if model.script != script { model.script = script }
    if model.command != command { model.command = command }
    if model.commandRevision != commandRevision { model.commandRevision = commandRevision }
    if model.commandValue != commandValue { model.commandValue = commandValue }
    if model.limitsNavigationsToAppBoundDomains != limitsNavigationsToAppBoundDomains { model.limitsNavigationsToAppBoundDomains = limitsNavigationsToAppBoundDomains }
    if model.inlineMedia != inlineMedia { model.inlineMedia = inlineMedia }
    if model.inspectable != inspectable { model.inspectable = inspectable }
    if model.bounces != bounces { model.bounces = bounces }
    if model.backForwardNavigationGestures != backForwardNavigationGestures { model.backForwardNavigationGestures = backForwardNavigationGestures }
    if model.magnificationGestures != magnificationGestures { model.magnificationGestures = magnificationGestures }
    if model.linkPreviews != linkPreviews { model.linkPreviews = linkPreviews }
    if model.elementFullscreen != elementFullscreen { model.elementFullscreen = elementFullscreen }
    if model.contentBackground != contentBackground { model.contentBackground = contentBackground }
  }


  private weak var compositionParent: OneNativeCompositionParent?
  public func compositionContent() -> AnyView { AnyView(WebViewContent(model: model)) }
  // composed, activation follows the parent: a subtree mounted before root attachment
  // stays silent until the root attaches.
  public func composeInto(_ parent: OneNativeCompositionParent) {
    controller?.detach(); controller = nil
    compositionParent = parent
    bindCallbacks()
    model.active = parent.compositionActive
  }
  public func decompose() { compositionParent = nil; model.active = false }
  public func propagateActive(_ active: Bool) { model.active = active }
  public override func didMoveToWindow() { super.didMoveToWindow(); updateHost() }
  public override func layoutSubviews() { super.layoutSubviews(); updateHost() }
  private func bindCallbacks() {
    model.onSDKEvent = { [weak self] name, value in self?.onSDKEvent?(name, value) }

    model.onNavigate = { [weak self] url, navigateCount in self?.onNavigate?(url, navigateCount) }
    model.onTitleChange = { [weak self] title, titleChangeCount in self?.onTitleChange?(title, titleChangeCount) }
    model.onLoadingChange = { [weak self] loading, progress, loadingChangeCount in self?.onLoadingChange?(loading, progress, loadingChangeCount) }
    model.onLoadStart = { [weak self] loadStartCount in self?.onLoadStart?(loadStartCount) }
    model.onLoadEnd = { [weak self] loadEndCount in self?.onLoadEnd?(loadEndCount) }
    model.onError = { [weak self] message, errorCount in self?.onError?(message, errorCount) }
    model.onHttpError = { [weak self] statusCode, httpErrorCount in self?.onHttpError?(statusCode, httpErrorCount) }
    model.onMessage = { [weak self] data, messageCount in self?.onMessage?(data, messageCount) }
    model.onHistoryChange = { [weak self] canGoBack, canGoForward, historyChangeCount in self?.onHistoryChange?(canGoBack, canGoForward, historyChangeCount) }
    model.onProcessTerminate = { [weak self] processTerminateCount in self?.onProcessTerminate?(processTerminateCount) }
  }
  private func updateHost() {
    guard compositionParent == nil else { return }
    model.active = false
    guard window != nil else { controller?.detach(); return }
    if controller == nil {
      bindCallbacks()
      controller = OneNativeHostingController(rootView: OneNativeStandalone(content: WebViewContent(model: model)))
    }
    controller?.attach(to: self)
    model.active = controller?.isAttached == true
  }
  public func reset() {
    compositionParent = nil
    model.active = false; model.onSDKEvent = nil; model.onNavigate = nil; model.onTitleChange = nil; model.onLoadingChange = nil; model.onLoadStart = nil; model.onLoadEnd = nil; model.onError = nil; model.onHttpError = nil; model.onMessage = nil; model.onHistoryChange = nil; model.onProcessTerminate = nil
    controller?.detach(); controller = nil; model = WebViewModel()
  }
}
private struct WebViewContent: View {
  @ObservedObject var model: WebViewModel
  var body: some View {
    Group {
      if #available(iOS 26.0, *) {
        WebViewSurface(model: model)
          .oneNativeWebViewBackForwardNavigationGestures(model.backForwardNavigationGestures)
          .oneNativeWebViewMagnificationGestures(model.magnificationGestures)
          .oneNativeWebViewLinkPreviews(model.linkPreviews)
          .oneNativeWebViewElementFullscreenBehavior(model.elementFullscreen)
          .oneNativeWebViewContentBackground(model.contentBackground)
      } else {
        Color.clear
      }
    }
      .oneNativeAccessibility(model.accessibility)
      .oneNativeStyle(model.swiftStyle, emit: model.emitSDKEvent)
  }
}
// the page is created with its script bridge and navigation policy, because those
// are configuration. url and html still load once per value so later prop changes
// keep the scroll position and the back-forward list. commands are a separate
// revision, same shape as VideoPlayer.
@available(iOS 26.0, *)
@MainActor private final class OneNativeWebSession: ObservableObject {
  @Published private(set) var page: WebPage?
  // last source React asked for, and what the page actually holds. after a
  // back/forward traversal these differ, and an unchanged prop must not reload.
  private var requested: String?
  private var pageLoaded: String?
  private var configKey: String?
  private var appliedRevision: Double = 0
  private var navigationTask: Task<Void, Never>?
  private var bridge: OneNativeWebBridge?
  private var decider: OneNativeWebDecider?
  private var inspectable = false
  var onMessage: ((String) -> Void)?
  var onLoadStart: (() -> Void)?
  var onLoadEnd: (() -> Void)?
  var onError: ((String) -> Void)?
  var onHttpError: ((Double) -> Void)?
  var onHistory: ((Bool, Bool) -> Void)?
  var onProcessTerminate: (() -> Void)?

  func receive(_ data: String) { onMessage?(data) }
  func receiveHttp(_ statusCode: Int) { onHttpError?(Double(statusCode)) }

  // the surface observes page.url only once the page exists and WebView mounts,
  // which is after load() already set it. report the current url once per page
  // so the initial value is not lost; later changes still arrive via onChange.
  private var seededURLPage: ObjectIdentifier?
  func seedURL() -> String? {
    guard let page else { return nil }
    let id = ObjectIdentifier(page)
    guard seededURLPage != id else { return nil }
    seededURLPage = id
    return page.url?.absoluteString ?? ""
  }

  func configure(script: String, limits: Bool, inlineMedia: Bool, inspectable: Bool) {
    self.inspectable = inspectable
    let key = String(limits) + "|" + String(inlineMedia) + "|" + script
    if key != configKey {
      configKey = key
      pageLoaded = nil
      install(script: script, limits: limits, inlineMedia: inlineMedia)
    }
    page?.isInspectable = inspectable
  }

  // sources the component loaded, oldest first. WebKit records no history for
  // consecutive html loads, so back/forward past the native list traverse these.
  private var sources: [String] = []
  private var sourceIndex: Int = -1

  func load(url: String, html: String) {
    let source = html.isEmpty ? "url:" + url : "html:" + html
    guard let page else { return }
    if requested != source {
      requested = source
      sources = Array(sources.prefix(sourceIndex + 1))
      sources.append(source)
      sourceIndex = sources.count - 1
    }
    guard sourceIndex >= 0 else { return }
    let current = sources[sourceIndex]
    guard pageLoaded != current else { return }
    pageLoaded = current
    load(source: current, on: page)
  }

  private func load(source: String, on page: WebPage) {
    if source.hasPrefix("html:") {
      page.load(html: String(source.dropFirst(5)))
    } else if let parsed = URL(string: String(source.dropFirst(4))) {
      page.load(parsed)
    }
  }

  func apply(command: String, revision: Double, value: String) {
    guard revision > appliedRevision else { return }
    guard let page else { return }
    appliedRevision = revision
    switch command {
    case "reload":
      page.reload()
    case "goBack":
      if let item = page.backForwardList.backList.last {
        page.load(item)
      } else if sourceIndex > 0 {
        sourceIndex -= 1
        pageLoaded = sources[sourceIndex]
        load(source: sources[sourceIndex], on: page)
      }
    case "goForward":
      if let item = page.backForwardList.forwardList.first {
        page.load(item)
      } else if sourceIndex >= 0 && sourceIndex + 1 < sources.count {
        sourceIndex += 1
        pageLoaded = sources[sourceIndex]
        load(source: sources[sourceIndex], on: page)
      }
    case "evaluate":
      runJavaScript("eval(s)", value: value)
    case "postMessage":
      runJavaScript("window.dispatchEvent(new MessageEvent('message', { data: s }))", value: value)
    default:
      break
    }
  }

  private func install(script: String, limits: Bool, inlineMedia: Bool) {
    navigationTask?.cancel()
    let controller = WKUserContentController()
    let bootstrap = "window.ReactNativeWebView = { postMessage: function(data) { window.webkit.messageHandlers.ReactNativeWebView.postMessage(String(data)); } };"
    controller.addUserScript(WKUserScript(source: bootstrap, injectionTime: .atDocumentStart, forMainFrameOnly: true))
    if !script.isEmpty {
      controller.addUserScript(WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true))
    }
    let bridge = OneNativeWebBridge()
    bridge.session = self
    self.bridge = bridge
    controller.add(bridge, name: "ReactNativeWebView")
    var configuration = WebPage.Configuration()
    configuration.userContentController = controller
    configuration.limitsNavigationsToAppBoundDomains = limits
    if inlineMedia { configuration.mediaPlaybackBehavior = .allowsInlinePlayback }
    let decider = OneNativeWebDecider()
    decider.session = self
    self.decider = decider
    let next = WebPage(configuration: configuration, navigationDecider: decider)
    next.isInspectable = inspectable
    page = next
    watch(next)
  }

  private func watch(_ page: WebPage) {
    navigationTask = Task { @MainActor [weak self] in
      do {
        for try await event in page.navigations {
          guard let self else { return }
          switch event {
          case .startedProvisionalNavigation:
            self.onLoadStart?()
            self.publishHistory()
          case .finished:
            self.onLoadEnd?()
            self.publishHistory()
          case .committed, .receivedServerRedirect:
            self.publishHistory()
          @unknown default:
            break
          }
        }
      } catch let error as WebPage.NavigationError {
        guard let self else { return }
        switch error {
        case .webContentProcessTerminated:
          self.onProcessTerminate?()
        case .failedProvisionalNavigation(let underlying):
          self.onError?(underlying.localizedDescription)
        case .invalidURL:
          self.onError?("invalid URL")
        case .pageClosed:
          break
        @unknown default:
          self.onError?(String(describing: error))
        }
      } catch {
        self?.onError?(error.localizedDescription)
      }
    }
  }

  private func publishHistory() {
    guard let page else { return }
    let list = page.backForwardList
    let back = !list.backList.isEmpty || sourceIndex > 0
    let forward = !list.forwardList.isEmpty || (sourceIndex >= 0 && sourceIndex + 1 < sources.count)
    onHistory?(back, forward)
  }

  private func runJavaScript(_ body: String, value: String) {
    guard let page else { return }
    Task { @MainActor in
      do {
        _ = try await page.callJavaScript(body, arguments: ["s": value])
      } catch {
        self.onError?(error.localizedDescription)
      }
    }
  }
}

@available(iOS 26.0, *)
private final class OneNativeWebBridge: NSObject, WKScriptMessageHandler {
  weak var session: OneNativeWebSession?
  func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    let body = message.body as? String ?? String(describing: message.body)
    let session = self.session
    Task { @MainActor in session?.receive(body) }
  }
}

@available(iOS 26.0, *)
@MainActor
private final class OneNativeWebDecider: WebPage.NavigationDeciding {
  weak var session: OneNativeWebSession?
  func decidePolicy(for response: WebPage.NavigationResponse) async -> WKNavigationResponsePolicy {
    if let http = response.response as? HTTPURLResponse, http.statusCode >= 400 {
      session?.receiveHttp(http.statusCode)
    }
    return .allow
  }
}

@available(iOS 26.0, *)
private struct OneNativeWebScroll: UIViewRepresentable {
  var bounces: Bool
  func makeUIView(context: Context) -> UIView {
    let view = UIView(frame: .zero)
    view.isUserInteractionEnabled = false
    view.backgroundColor = .clear
    return view
  }
  func updateUIView(_ view: UIView, context: Context) {
    let bounces = self.bounces
    DispatchQueue.main.async {
      guard let web = OneNativeWebScroll.find(from: view) else { return }
      web.scrollView.bounces = bounces
      web.scrollView.contentInsetAdjustmentBehavior = .never
    }
  }
  private static func findDown(_ view: UIView) -> WKWebView? {
    if let web = view as? WKWebView { return web }
    for subview in view.subviews {
      if let found = findDown(subview) { return found }
    }
    return nil
  }
  private static func find(from view: UIView) -> WKWebView? {
    var current: UIView? = view
    var hops = 0
    while let next = current, hops < 16 {
      for subview in next.subviews {
        if let found = findDown(subview) { return found }
      }
      current = next.superview
      hops += 1
    }
    return nil
  }
}

@available(iOS 26.0, *)
@MainActor private struct WebViewSurface: View {
  @ObservedObject var model: WebViewModel
  @StateObject private var session = OneNativeWebSession()
  var body: some View {
    Group {
      if let page = session.page {
        WebView(page)
          // reading these here is what subscribes to WebPage's observation. WebKit
          // coalesces its own progress reporting, so this is not a per-frame event.
          .onChange(of: page.url) { _, url in model.navigate(url?.absoluteString ?? "") }
          .onChange(of: page.title) { _, title in model.titleChange(title) }
          .onChange(of: page.isLoading) { _, loading in
            model.loadingChange(loading, page.estimatedProgress)
          }
          .onChange(of: page.estimatedProgress) { _, progress in
            model.loadingChange(page.isLoading, progress)
          }
          .overlay(OneNativeWebScroll(bounces: model.bounces).allowsHitTesting(false))
      } else {
        Color.clear
      }
    }
    .onAppear { sync(includeCommand: true) }
    .onChange(of: model.url) { sync(includeCommand: false) }
    .onChange(of: model.html) { sync(includeCommand: false) }
    .onChange(of: model.script) { sync(includeCommand: false) }
    .onChange(of: model.limitsNavigationsToAppBoundDomains) { sync(includeCommand: false) }
    .onChange(of: model.inlineMedia) { sync(includeCommand: false) }
    .onChange(of: model.inspectable) { sync(includeCommand: false) }
    .onChange(of: model.commandRevision) { sync(includeCommand: true) }
  }
  private func sync(includeCommand: Bool) {
    session.onMessage = { [weak model] data in model?.message(data) }
    session.onLoadStart = { [weak model] in model?.loadStart() }
    session.onLoadEnd = { [weak model] in model?.loadEnd() }
    session.onError = { [weak model] message in model?.error(message) }
    session.onHttpError = { [weak model] status in model?.httpError(status) }
    session.onHistory = { [weak model] back, forward in model?.historyChange(back, forward) }
    session.onProcessTerminate = { [weak model] in model?.processTerminate() }
    session.configure(
      script: model.script,
      limits: model.limitsNavigationsToAppBoundDomains,
      inlineMedia: model.inlineMedia,
      inspectable: model.inspectable
    )
    session.load(url: model.url, html: model.html)
    if let url = session.seedURL() { model.navigate(url) }
    if includeCommand {
      session.apply(command: model.command, revision: model.commandRevision, value: model.commandValue)
    }
  }
}
