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

import ExpoModulesCore
import WebKit

internal final class DomWebView: ExpoView, UIScrollViewDelegate, WKUIDelegate, WKScriptMessageHandler {
  // swiftlint:disable implicitly_unwrapped_optional
  private(set) var webView: WKWebView!
  private(set) var id: WebViewId!
  // swiftlint:enable implicitly_unwrapped_optional

  private var source: DomWebViewSource?
  private var injectedJSBeforeContentLoaded: WKUserScript?
  var webviewDebuggingEnabled = false
  var decelerationRate: UIScrollView.DecelerationRate = .normal

  internal typealias SyncCompletionHandler = (String?) -> Void

  private var needsResetupScripts = false

  private static let EVAL_PROMPT_HEADER = "__EXPO_DOM_WEBVIEW_JS_EVAL__"
  private static let POST_MESSAGE_HANDLER_NAME = "ReactNativeWebView"

  private let onMessage = EventDispatcher()

  required init(appContext: AppContext? = nil) {
    super.init(appContext: appContext)
    super.backgroundColor = .clear
    self.id = DomWebViewRegistry.shared.add(webView: self)
    webView = createWebView()
    resetupScripts()
    addSubview(webView)
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    webView.frame = bounds
  }

  override var backgroundColor: UIColor? {
    get { webView.backgroundColor }
    set {
      let isOpaque = (newValue ?? UIColor.clear).cgColor.alpha == 1.0
      self.isOpaque = isOpaque
      webView.isOpaque = isOpaque
      webView.scrollView.backgroundColor = newValue
      webView.backgroundColor = newValue
    }
  }

  override func removeFromSuperview() {
    webView.removeFromSuperview()
    webView = nil
    DomWebViewRegistry.shared.remove(webViewId: self.id)
    super.removeFromSuperview()
  }

  // MARK: - Public methods

  func reload() {
    if #available(iOS 16.4, *) {
      webView.isInspectable = webviewDebuggingEnabled
    }

    if needsResetupScripts {
      resetupScripts()
      needsResetupScripts = false
    }

    if let source,
      let request = RCTConvert.nsurlRequest(source.toDictionary(appContext: appContext)),
      webView.url?.absoluteURL != request.url {
      webView.load(request)
    }
  }

  func scrollTo(offset: CGPoint, animated: Bool) {
    webView.scrollView.setContentOffset(offset, animated: animated)
  }

  func injectJavaScript(_ script: String) {
    DispatchQueue.main.async { [weak self] in
      self?.webView.evaluateJavaScript(script)
    }
  }

  func setSource(_ source: DomWebViewSource) {
    self.source = source
  }

  func setInjectedJSBeforeContentLoaded(_ script: String?) {
    if let script, !script.isEmpty {
      injectedJSBeforeContentLoaded = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false)
    } else {
      injectedJSBeforeContentLoaded = nil
    }
    needsResetupScripts = true
  }

  // MARK: - UIScrollViewDelegate implementations

  func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    scrollView.decelerationRate = decelerationRate
  }

  // MARK: - WKUIDelegate implementations

  func webView(
    _ webView: WKWebView,
    runJavaScriptTextInputPanelWithPrompt prompt: String,
    defaultText: String?,
    initiatedByFrame frame: WKFrameInfo,
    completionHandler: @escaping SyncCompletionHandler
  ) {
    if !prompt.hasPrefix(Self.EVAL_PROMPT_HEADER) {
      completionHandler(nil)
      return
    }
    let script = String(prompt.dropFirst(Self.EVAL_PROMPT_HEADER.count))
    if let data = script.data(using: .utf8),
      let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
      let deferredId = json["deferredId"] as? Int,
      let source = json["source"] as? String {
      nativeJsiEvalSync(deferredId: deferredId, source: source, completionHandler: completionHandler)
    } else {
      completionHandler("Invalid parameters for nativeJsiEvalSync")
    }
  }

  // MARK: - WKScriptMessageHandler implementations

  func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == Self.POST_MESSAGE_HANDLER_NAME {
      var payload = createBaseEventPayload()
      payload["data"] = message.body
      onMessage(payload)
      return
    }
  }

  // MARK: - Internals

  private func createWebView() -> WKWebView {
    let config = WKWebViewConfiguration()
    config.userContentController = WKUserContentController()
    let webView = WKWebView(frame: .zero, configuration: config)
    webView.uiDelegate = self
    webView.backgroundColor = .clear
    webView.scrollView.delegate = self
    return webView
  }

  private func createBaseEventPayload() -> [String: Any] {
    return [
      "url": webView.url?.absoluteString ?? "",
      "title": webView.title ?? ""
    ]
  }

  private func resetupScripts() {
    let userContentController = webView.configuration.userContentController
    userContentController.removeAllUserScripts()
    userContentController.removeAllScriptMessageHandlers()

    userContentController.add(self, name: Self.POST_MESSAGE_HANDLER_NAME)

    if let injectedJSBeforeContentLoaded {
      userContentController.addUserScript(injectedJSBeforeContentLoaded)
    }

    let addDomWebViewBridgeScript = """
    window.ExpoDomWebViewBridge = {
      eval: function eval(params) {
        return window.prompt('\(Self.EVAL_PROMPT_HEADER)' + params);
      },
    };
    true;
    """
    userContentController.addUserScript(WKUserScript(source: addDomWebViewBridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))

    let addRNWObjectScript = """
    window.ReactNativeWebView ||= {};
    window.ReactNativeWebView.postMessage = function postMessage(data) {
      window.webkit.messageHandlers.\(Self.POST_MESSAGE_HANDLER_NAME).postMessage(String(data));
    };
    true;
    """
    userContentController.addUserScript(WKUserScript(source: addRNWObjectScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))

    guard let webViewId = self.id else {
      return
    }

    let addExpoDomWebViewObjectScript = "\(INSTALL_GLOBALS_SCRIPT);true;"
      .replacingOccurrences(of: "\"%%WEBVIEW_ID%%\"", with: String(webViewId))
    userContentController.addUserScript(WKUserScript(source: addExpoDomWebViewObjectScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))
  }

  private func nativeJsiEvalSync(deferredId: Int, source: String, completionHandler: @escaping SyncCompletionHandler) {
    guard let appContext else {
      completionHandler("Missing AppContext")
      return
    }
    guard let webViewId = self.id else {
      completionHandler("Missing webViewId")
      return
    }
    guard let runtime = try? appContext.runtime else {
      completionHandler("Missing JS Runtime")
      return
    }
    try? appContext.runtime.schedule {
      let wrappedSource = NATIVE_EVAL_WRAPPER_SCRIPT
        .replacingOccurrences(of: "\"%%DEFERRED_ID%%\"", with: String(deferredId))
        .replacingOccurrences(of: "\"%%WEBVIEW_ID%%\"", with: String(webViewId))
        .replacingOccurrences(of: "\"%%SOURCE%%\"", with: source)
      do {
        let result = try runtime.eval(wrappedSource)
        completionHandler(result.getString())
      } catch {
        completionHandler("\(error)")
      }
    }
  }
}
