// Keep in sync with webview-wrapper.tsx
// https://github.com/expo/expo/blob/main/packages/expo/src/dom/webview-wrapper.tsx
#if canImport(UIKit) && EXPO_UNSTABLE_LOG_BOX

import UIKit
import WebKit
import React

protocol ExpoLogBoxNativeActionsProtocol {
    func onReload() -> Void
    func fetchTextAsync(url: String, method: String?, body: String?) async -> String
}

private class ExpoLogBoxNativeActions: ExpoLogBoxNativeActionsProtocol {
    func onReload() -> Void {
        fatalError()
    }
    func fetchTextAsync(url: String, method: String?, body: String?) async -> String {
        fatalError()
    }
    static let onReloadName = "onReload"
    static let fetchTextAsyncName = "fetchTextAsync"
    static let names = [
        onReloadName,
        fetchTextAsyncName,
    ]
}

private struct Constants {
    static let DOM_EVENT = "$$dom_event"
    static let NATIVE_ACTION_RESULT = "$$native_action_result"
    static let NATIVE_ACTION = "$$native_action"
}

class ExpoLogBoxWebViewWrapper: NSObject, WKScriptMessageHandler {
    private let nativeMessageHandlerName = "nativeHandler"
    private let nativeActions: ExpoLogBoxNativeActionsProtocol
    private let props: [String: Any]
    private let webView: WKWebView

    init(nativeActions: ExpoLogBoxNativeActionsProtocol, props: [String: Any]) {
        self.nativeActions = nativeActions
        self.props = props
        self.webView = WKWebView(frame: .zero)
    }

    func prepareWebView() -> WKWebView {
        let initProps: [String: Any] = [
            "names": ExpoLogBoxNativeActions.names,
            "props": props
        ]

        guard let initPropsObject = try? JSONSerialization.data(withJSONObject: initProps, options: []),
              let initPropsStringified = String(data: initPropsObject, encoding: .utf8) else {
            fatalError("Failed to serialize initProps. This is an issue in ExpoLogBox. Please report it.")
        }

        let devServerOrigin: String? = {
            guard let bundleUrl = RCTBundleURLProvider.sharedSettings()
                    .jsBundleURL(forBundleRoot: "unused.name"),
                  let scheme = bundleUrl.scheme,
                  let host = bundleUrl.host,
                  let port = bundleUrl.port
            else {
                return nil
            }
            return "\(scheme)://\(host):\(port)"
        }()
        let devServerOriginJsValue: String = devServerOrigin.map { "'\($0)'" } ?? "undefined"

        let injectJavascript = """
            var process = globalThis.process || {};
            process.env = process.env || {};
            process.env.EXPO_DEV_SERVER_ORIGIN = \(devServerOriginJsValue);
            window.$$EXPO_DOM_HOST_OS = 'ios';
            window.$$EXPO_INITIAL_PROPS = \(initPropsStringified);
            window.ReactNativeWebView = {};
            window.ReactNativeWebView.postMessage = (message) => {
                window.webkit.messageHandlers.\(nativeMessageHandlerName).postMessage(
                    JSON.parse(message)
                );
            };
            """

        webView.configuration.userContentController.addUserScript(WKUserScript(
            source: injectJavascript,
            injectionTime: .atDocumentStart,
            forMainFrameOnly: true
        ))
        if #available(iOS 16.4, *) {
#if EXPO_DEBUG_LOG_BOX || EXPO_DEVELOP_LOG_BOX
            webView.isInspectable = true
#else
            webView.isInspectable = false
#endif
        }
        webView.configuration.userContentController.add(self, name: nativeMessageHandlerName)
        webView.translatesAutoresizingMaskIntoConstraints = false
        webView.isOpaque = false
        return webView
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        Task.detached {
            await self.handleWebViewMessageAsync(message: message)
        }
    }

    func handleWebViewMessageAsync(message: WKScriptMessage) async {
        guard message.name == nativeMessageHandlerName,
              let messageBody = message.body as? [String: Any],
              let messageType = messageBody["type"] as? String else {
            return
        }

        let data = messageBody["data"] as? [String: Any] ?? [:]

        if (messageType == Constants.NATIVE_ACTION) {
            guard let actionId = data["actionId"] as? String,
                  let uid = data["uid"] as? String,
                  let args = data["args"] as? [Any] else {
                print("ExpoLogBoxDomRuntimeError native actions is missing actionId or uid.")
                return
            }

            do {
                switch actionId {
                    case ExpoLogBoxNativeActions.onReloadName:
                        nativeActions.onReload()
                    case ExpoLogBoxNativeActions.fetchTextAsyncName:
                        guard let url = args[0] as? String,
                              let options = args[1] as? [String: Any] else {
                            print("ExpoLogBox fetchTextAsync action is missing url or options.")
                            return
                        }

                        let method = options["method"] as? String
                        let body = options["body"] as? String
                        let result = await nativeActions.fetchTextAsync(url: url, method: method, body: body)
                        sendReturn(result: result, uid: uid, actionId: actionId)
                    default:
                        print("Unknown native action: \(actionId)")
                }
            } catch {
                sendReturn(error: error, uid: uid, actionId: actionId)
            }
        }
    }

    func sendReturn(result: Any, uid: String, actionId: String) {
        sendReturn(data: [
            "type": Constants.NATIVE_ACTION_RESULT,
            "data": [
              "uid": uid,
              "actionId": actionId,
              "result": result,
            ],
        ])
    }

    func sendReturn(error: Any, uid: String, actionId: String) {
        sendReturn(data: [
            "type": Constants.NATIVE_ACTION_RESULT,
            "data": [
              "uid": uid,
              "actionId": actionId,
              "error": [
                "message": "\(error)",
              ],
            ],
        ])
    }

    func sendReturn(data: [String: Any]) {
        guard let jsonData = try? JSONSerialization.data(
            withJSONObject: [ "detail": data ],
            options: []
        ), let jsonDataStringified = String(data: jsonData, encoding: .utf8) else {
            print("ExpoLogBox failed to stringify native action result.")
            return
        }
        sendReturn(value: jsonDataStringified)
    }

    func sendReturn(value: String) {
        let injectedJavascript = """
            ;
            (function() {
                try {
                    console.log("received", \(value));
                    window.dispatchEvent(new CustomEvent("\(Constants.DOM_EVENT)", \(value)));
                } catch (e) {}
            })();
            true;
            """
        webView.evaluateJavaScript(injectedJavascript) { _, error in
            if let error = error {
                print("ExpoLogBox NativeActions return value injection error: \(error.localizedDescription)")
            }
        }
    }
}

#endif
