//
//  WKWebViewController.swift
//  Sample
//
//  Created by Meniny on 2018-01-20.
//  Copyright © 2018年 Meniny. All rights reserved.
//

import UIKit
import QuickLook
import WebKit

private let estimatedProgressKeyPath = "estimatedProgress"
private let titleKeyPath = "title"
private let cookieKey = "Cookie"

enum CustomSchemeInterceptSupport {
    static let standardHandledSchemes = ["tel", "mailto", "sms"]
    private static let webSchemes = ["http", "https", "file", "data"]

    static func shouldEmitInterceptEvent(for url: URL) -> Bool {
        guard let scheme = url.scheme?.lowercased(), !scheme.isEmpty else {
            return false
        }
        return !webSchemes.contains(scheme) && !standardHandledSchemes.contains(scheme)
    }
}

private struct UrlsHandledByApp {
    static var hosts = ["itunes.apple.com"]
    static var schemes = CustomSchemeInterceptSupport.standardHandledSchemes
}

private struct WKDownloadState {
    let destinationURL: URL?
    let mimeType: String?
    let sourceURL: String?
}

private enum DownloadReservationStore {
    static var paths: Set<String> = []
    static let lock = NSLock()
}

private enum BlobDownloadSupport {
    static let maxLegacyBytes = 512 * 1024
    static let chunkBytes = 64 * 1024
}

/// Script message handlers registered on every in-app browser WKWebView.
/// Popup configs from `createWebViewWith` copy parent handlers, so we must
/// remove these names before re-adding or WKWebView raises NSInvalidArgumentException.
enum ScriptMessageHandlerSupport {
    static let allNames = [
        "messageHandler",
        "preShowScriptError",
        "preShowScriptSuccess",
        "close",
        "hide",
        "show",
        "blobDownload",
        "blobDownloadStart",
        "blobDownloadChunk",
        "blobDownloadFinish",
        "blobDownloadAbort",
        "takeScreenshot",
        "consoleMessageHandler",
        "magicPrint",
        "capgoProxyBridge"
    ]

    static func removeAll(from userContentController: WKUserContentController) {
        for name in allNames {
            userContentController.removeScriptMessageHandler(forName: name)
        }
    }
}

private struct BlobDownloadSession {
    let destinationURL: URL
    let sourceURL: String?
    let mimeType: String?
    let fileHandle: FileHandle
    let expectedSize: Int64?
    var bytesWritten: Int64 = 0
}

public struct WKWebViewCredentials {
    var username: String
    var password: String
}

@objc public protocol WKWebViewControllerDelegate {
    @objc optional func webViewController(_ controller: WKWebViewController, canDismiss url: URL) -> Bool

    @objc optional func webViewController(_ controller: WKWebViewController, didStart url: URL)
    @objc optional func webViewController(_ controller: WKWebViewController, didFinish url: URL)
    @objc optional func webViewController(_ controller: WKWebViewController, didFail url: URL, withError error: Error)
    @objc optional func webViewController(_ controller: WKWebViewController, decidePolicy url: URL, navigationType: NavigationType) -> Bool
}

extension Dictionary {
    func mapKeys<T>(_ transform: (Key) throws -> T) rethrows -> [T: Value] {
        var dictionary = [T: Value]()
        for (key, value) in self {
            dictionary[try transform(key)] = value
        }
        return dictionary
    }
}

enum ConsoleMessageSupport {
    static func normalizePayload(from data: [String: Any]) -> [String: Any] {
        var payload: [String: Any] = [
            "level": normalizedLevel(data["level"] as? String),
            "message": normalizedMessage(data["message"])
        ]

        if let source = data["source"] as? String, !source.isEmpty {
            payload["source"] = source
        }

        if let line = integerValue(from: data["line"]) {
            payload["line"] = line
        }

        if let column = integerValue(from: data["column"]) {
            payload["column"] = column
        }

        if let kind = data["kind"] as? String, !kind.isEmpty {
            payload["kind"] = kind
        }

        return payload
    }

    static func captureScriptSource() -> String {
        return """
        (function() {
          if (window.__capgoInAppBrowserConsoleCaptureInstalled) {
            return;
          }
          window.__capgoInAppBrowserConsoleCaptureInstalled = true;

          var nativeHandler =
            window.webkit &&
            window.webkit.messageHandlers &&
            window.webkit.messageHandlers.consoleMessageHandler;

          if (!nativeHandler) {
            return;
          }

          var serialize = function(value) {
            if (value instanceof Error) {
              return value.stack || value.message || String(value);
            }
            if (typeof value === 'string') {
              return value;
            }
            try {
              return JSON.stringify(value);
            } catch (_error) {
              try {
                return String(value);
              } catch (_error2) {
                return '[unserializable]';
              }
            }
          };

          var post = function(level, values, metadata) {
            try {
              var parts = Array.prototype.slice.call(values || []).map(serialize);
              nativeHandler.postMessage({
                level: level,
                message: parts.join(' '),
                source: metadata && metadata.source ? metadata.source : window.location.href,
                line: metadata && typeof metadata.line === 'number' ? metadata.line : null,
                column: metadata && typeof metadata.column === 'number' ? metadata.column : null,
                kind: metadata && metadata.kind ? metadata.kind : 'console',
              });
            } catch (_error) {}
          };

          ['log', 'info', 'warn', 'error', 'debug'].forEach(function(level) {
            var original = console[level] ? console[level].bind(console) : null;
            console[level] = function() {
              post(level, arguments, null);
              if (original) {
                return original.apply(console, arguments);
              }
            };
          });

          var originalAssert = console.assert ? console.assert.bind(console) : null;
          console.assert = function(condition) {
            if (!condition) {
              var args = Array.prototype.slice.call(arguments, 1);
              post('assert', args.length ? args : ['console.assert'], null);
            }
            if (originalAssert) {
              return originalAssert.apply(console, arguments);
            }
          };

          window.addEventListener('error', function(event) {
            post(
              'error',
              [event && event.message ? event.message : 'Uncaught error'],
              {
                source: event && event.filename ? event.filename : window.location.href,
                line: event && typeof event.lineno === 'number' ? event.lineno : null,
                column: event && typeof event.colno === 'number' ? event.colno : null,
                kind: 'error',
              }
            );
          });

          window.addEventListener('unhandledrejection', function(event) {
            var reason = event ? event.reason : null;
            var message =
              reason && reason.message
                ? reason.message
                : reason
                  ? serialize(reason)
                  : 'Unhandled promise rejection';
            post('error', [message], { source: window.location.href, kind: 'unhandledrejection' });
          });
        })();
        """
    }

    private static func normalizedLevel(_ value: String?) -> String {
        let normalized = value?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        switch normalized {
        case "log", "info", "warn", "error", "debug", "assert":
            return normalized ?? "log"
        default:
            return "log"
        }
    }

    private static func normalizedMessage(_ value: Any?) -> String {
        switch value {
        case let text as String:
            return text
        case let number as NSNumber:
            return number.stringValue
        case nil:
            return ""
        default:
            return String(describing: value!)
        }
    }

    private static func integerValue(from value: Any?) -> Int? {
        switch value {
        case let int as Int:
            return int
        case let number as NSNumber:
            return number.intValue
        case let text as String:
            return Int(text)
        default:
            return nil
        }
    }
}

enum WebViewViewportLayoutSupport {
    static func shouldRefreshViewport(previousSize: CGSize?, currentSize: CGSize, force: Bool = false) -> Bool {
        guard currentSize.width > 0, currentSize.height > 0 else {
            return false
        }

        return force || previousSize != currentSize
    }
}

enum WebViewSafeAreaLayoutSupport {
    static func contentInsetAdjustmentBehavior(enabledSafeBottomMargin: Bool) -> UIScrollView.ContentInsetAdjustmentBehavior {
        enabledSafeBottomMargin ? .automatic : .never
    }

    static func shouldInsetLayoutMarginsFromSafeArea(enabledSafeBottomMargin: Bool) -> Bool {
        enabledSafeBottomMargin
    }

    static func rawSystemBottomInset(effectiveBottomInset: CGFloat, additionalBottomInset: CGFloat) -> CGFloat {
        max(0, effectiveBottomInset - additionalBottomInset)
    }

    static func additionalBottomSafeAreaOffset(enabledSafeBottomMargin: Bool, safeAreaBottomInset: CGFloat) -> CGFloat {
        guard !enabledSafeBottomMargin, safeAreaBottomInset > 0 else {
            return 0
        }

        return -safeAreaBottomInset
    }

    static func cssBottomInset(enabledSafeBottomMargin: Bool, safeAreaBottomInset: CGFloat) -> CGFloat {
        enabledSafeBottomMargin ? safeAreaBottomInset : 0
    }

    static func cssTopInset(enabledSafeTopMargin: Bool, safeAreaTopInset: CGFloat) -> CGFloat {
        enabledSafeTopMargin ? safeAreaTopInset : 0
    }

    static func safeAreaCssVariablesScript(top: CGFloat, bottom: CGFloat, left: CGFloat, right: CGFloat) -> String {
        let topValue = Int(top.rounded())
        let bottomValue = Int(bottom.rounded())
        let leftValue = Int(left.rounded())
        let rightValue = Int(right.rounded())

        return "(function(){var root=document.documentElement;" +
            "root.style.setProperty('--safe-area-inset-top','\(topValue)px');" +
            "root.style.setProperty('--safe-area-inset-bottom','\(bottomValue)px');" +
            "root.style.setProperty('--safe-area-inset-left','\(leftValue)px');" +
            "root.style.setProperty('--safe-area-inset-right','\(rightValue)px');})();"
    }
}

open class WKWebViewController: UIViewController, WKScriptMessageHandler {

    public init() {
        super.init(nibName: nil, bundle: nil)
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    public init(source: WKWebSource?, credentials: WKWebViewCredentials? = nil, allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false) {
        super.init(nibName: nil, bundle: nil)
        self.source = source
        self.credentials = credentials
        self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
        self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
        self.captureConsoleLogs = captureConsoleLogs
        self.initWebview()
    }

    public init(url: URL, credentials: WKWebViewCredentials? = nil, allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false) {
        super.init(nibName: nil, bundle: nil)
        self.source = .remote(url)
        self.credentials = credentials
        self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
        self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
        self.captureConsoleLogs = captureConsoleLogs
        self.initWebview()
    }

    public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false) {
        super.init(nibName: nil, bundle: nil)
        self.source = .remote(url)
        self.credentials = credentials
        self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
        self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
        self.captureConsoleLogs = captureConsoleLogs
        self.setHeaders(headers: headers)
        self.setPreventDeeplink(preventDeeplink: preventDeeplink)
        self.initWebview(isInspectable: isInspectable)
    }

    public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false, openBlankTargetInWebView: Bool = false) {
        super.init(nibName: nil, bundle: nil)
        self.blankNavigationTab = blankNavigationTab
        self.enabledSafeBottomMargin = enabledSafeBottomMargin
        self.enabledSafeTopMargin = enabledSafeTopMargin
        self.source = .remote(url)
        self.credentials = credentials
        self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
        self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
        self.captureConsoleLogs = captureConsoleLogs
        self.openBlankTargetInWebView = openBlankTargetInWebView
        self.setHeaders(headers: headers)
        self.setPreventDeeplink(preventDeeplink: preventDeeplink)
        self.initWebview(isInspectable: isInspectable)
    }

    public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, blockedHosts: [String], allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false, openBlankTargetInWebView: Bool = false) {
        super.init(nibName: nil, bundle: nil)
        self.blankNavigationTab = blankNavigationTab
        self.enabledSafeBottomMargin = enabledSafeBottomMargin
        self.enabledSafeTopMargin = enabledSafeTopMargin
        self.source = .remote(url)
        self.credentials = credentials
        self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
        self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
        self.captureConsoleLogs = captureConsoleLogs
        self.openBlankTargetInWebView = openBlankTargetInWebView
        self.setHeaders(headers: headers)
        self.setPreventDeeplink(preventDeeplink: preventDeeplink)
        self.setBlockedHosts(blockedHosts: blockedHosts)
        self.initWebview(isInspectable: isInspectable)
    }

    public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, blockedHosts: [String], authorizedAppLinks: [String], allowWebViewJsVisibilityControl: Bool = false, allowScreenshotsFromWebPage: Bool = false, captureConsoleLogs: Bool = false, proxyRequests: Bool = false, proxySchemeHandler: ProxySchemeHandler? = nil, proxyBridge: ProxyBridge? = nil, proxyBridgeAccessToken: String? = nil, legacyProxyRequestURLRegexPattern: String? = nil, documentStartUserScripts: [String] = [], openBlankTargetInWebView: Bool = false) {
        super.init(nibName: nil, bundle: nil)
        self.blankNavigationTab = blankNavigationTab
        self.enabledSafeBottomMargin = enabledSafeBottomMargin
        self.enabledSafeTopMargin = enabledSafeTopMargin
        self.source = .remote(url)
        self.credentials = credentials
        self.allowWebViewJsVisibilityControl = allowWebViewJsVisibilityControl
        self.allowScreenshotsFromWebPage = allowScreenshotsFromWebPage
        self.captureConsoleLogs = captureConsoleLogs
        self.proxyRequests = proxyRequests
        self.proxySchemeHandler = proxySchemeHandler
        self.proxyBridge = proxyBridge
        self.proxyBridgeAccessToken = proxyBridgeAccessToken
        self.legacyProxyRequestURLRegexPattern = legacyProxyRequestURLRegexPattern
        self.openBlankTargetInWebView = openBlankTargetInWebView
        self.setHeaders(headers: headers)
        self.setPreventDeeplink(preventDeeplink: preventDeeplink)
        self.setBlockedHosts(blockedHosts: blockedHosts)
        self.setAuthorizedAppLinks(authorizedAppLinks: authorizedAppLinks)
        self.documentStartUserScripts = documentStartUserScripts
        self.initWebview(isInspectable: isInspectable)
    }

    open var hasDynamicTitle = false
    open var source: WKWebSource?
    /// use `source` instead
    open internal(set) var url: URL?
    open var tintColor: UIColor?
    open var allowsFileURL = true
    open var allowWebViewJsVisibilityControl = false
    open var allowScreenshotsFromWebPage = false
    open var captureConsoleLogs = false
    open var persistWebViewData = true
    open var useSharedDataStore = false
    open var clearCookiesOnOpen = false
    open var clearCacheOnOpen = false
    open var handleDownloads = false
    open var preferredContentMode: String?
    open var delegate: WKWebViewControllerDelegate?
    open var bypassedSSLHosts: [String]?
    open var cookies: [HTTPCookie]?
    open var headers: [String: String]?
    open var httpMethod: String?
    open var httpBody: String?
    open var capBrowserPlugin: CapgoInAppBrowserPlugin?
    open var isInspectable: Bool = false
    var instanceId: String = ""
    var shareDisclaimer: [String: Any]?
    var shareSubject: String?
    var didpageInit = false
    open var closeModal = false
    open var closeAction = "close"
    open var screenshotOnHide = false
    var toolbarHideInProgress = false
    open var titleFontFamily: String?
    open var titleIcon: UIImage?
    open var closeModalTitle = ""
    open var closeModalDescription = ""
    open var closeModalOk = ""
    open var closeModalCancel = ""
    open var closeModalURLPattern = "" {
        didSet {
            closeModalURLRegex = closeModalURLPattern.isEmpty ? nil : (try? NSRegularExpression(pattern: closeModalURLPattern))
        }
    }
    private var closeModalURLRegex: NSRegularExpression?
    open var ignoreUntrustedSSLError = false
    open var enableGooglePaySupport = false
    var viewWasPresented = false
    var preventDeeplink: Bool = false
    var openBlankTargetInWebView: Bool = false
    var blankNavigationTab: Bool = false
    var capacitorStatusBar: UIView?
    var enabledSafeBottomMargin: Bool = false
    var enabledSafeTopMargin: Bool = true
    var blockedHosts: [String] = []
    var authorizedAppLinks: [String] = []
    var activeNativeNavigationForWebview: Bool = true
    var enableReloadGesture: Bool = false
    var pendingReloadFromGesture = false
    var reloadFromGestureInProgress = false
    var reloadGestureNavigation: WKNavigation?
    var reloadGestureArmedPullDistance: CGFloat = 0
    var reloadPanObserverInstalled = false
    var disableOverscroll: Bool = false
    var proxyRequests: Bool = false
    var proxySchemeHandler: ProxySchemeHandler?
    var bundledAssetSchemeHandler: BundledAssetSchemeHandler?
    var bundledAssetLocalScheme: String = BundledAssetSupport.iosDefaults.scheme
    var bundledAssetLocalHost: String = BundledAssetSupport.iosDefaults.host
    var proxyBridge: ProxyBridge?
    var proxyBridgeAccessToken: String?
    var legacyProxyRequestURLRegexPattern: String?
    var initialWebConfiguration: WKWebViewConfiguration?
    var waitsForPopupNavigation = false
    var hiddenPopupWindow = false
    var opensHidden = false
    var isLayeredBehind = false
    var transparentHostBackground = true

    // Dimension properties
    var customWidth: CGFloat?
    var customHeight: CGFloat?
    var customX: CGFloat?
    var customY: CGFloat?

    /// Front (non-toBack) browsers with a custom height use a framed child overlay.
    var shouldPresentAsFramedOverlay: Bool {
        !isLayeredBehind && customHeight != nil
    }

    internal var preShowSemaphore: DispatchSemaphore?
    internal var preShowError: String?
    private var isWebViewInitialized = false
    private var isObservingKeyboardViewportChanges = false
    private var lastViewportRefreshSize: CGSize?
    private struct InjectedSafeAreaInsets: Equatable {
        let top: Int
        let bottom: Int
        let left: Int
        let right: Int
    }

    private var lastInjectedSafeAreaInsets: InjectedSafeAreaInsets?
    private var downloadStates: [ObjectIdentifier: WKDownloadState] = [:]
    private var blobDownloadSessions: [String: BlobDownloadSession] = [:]
    private var previewItemURL: URL?

    func setHeaders(headers: [String: String]) {
        self.headers = headers
        let lowercasedHeaders = headers.mapKeys { $0.lowercased() }
        let userAgent = lowercasedHeaders["user-agent"]
        self.headers?.removeValue(forKey: "User-Agent")
        self.headers?.removeValue(forKey: "user-agent")

        if let userAgent = userAgent {
            self.customUserAgent = userAgent
        }
    }

    func setPreventDeeplink(preventDeeplink: Bool) {
        self.preventDeeplink = preventDeeplink
    }

    func setBlockedHosts(blockedHosts: [String]) {
        self.blockedHosts = blockedHosts
    }

    func setAuthorizedAppLinks(authorizedAppLinks: [String]) {
        self.authorizedAppLinks = authorizedAppLinks
    }

    private func register(download: WKDownload, response: URLResponse?, sourceURL: String? = nil) {
        download.delegate = self
        downloadStates[ObjectIdentifier(download)] = WKDownloadState(
            destinationURL: nil,
            mimeType: response?.mimeType,
            sourceURL: sourceURL ?? response?.url?.absoluteString
        )
    }

    private func attachmentDisposition(_ response: URLResponse) -> String? {
        guard let httpResponse = response as? HTTPURLResponse else {
            return nil
        }

        if let value = httpResponse.value(forHTTPHeaderField: "Content-Disposition"),
           !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            return value
        }

        for (key, value) in httpResponse.allHeaderFields {
            guard let headerName = key as? String,
                  headerName.lowercased() == "content-disposition",
                  let headerValue = value as? String,
                  !headerValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
                continue
            }
            return headerValue
        }

        return nil
    }

    private func attachmentDispositionType(_ response: URLResponse) -> String? {
        guard let disposition = attachmentDisposition(response)?
                .split(separator: ";", maxSplits: 1)
                .first?
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .lowercased(),
              !disposition.isEmpty else {
            return nil
        }

        return disposition
    }

    private func shouldInterceptDownload(for response: WKNavigationResponse) -> Bool {
        guard handleDownloads else {
            return false
        }

        if !response.canShowMIMEType {
            return true
        }

        guard let disposition = attachmentDispositionType(response.response) else {
            return false
        }

        return disposition == "attachment"
    }

    private func sanitizeDownloadFilename(_ suggestedFilename: String) -> String {
        let invalidCharacters = CharacterSet(charactersIn: "/:\\?%*|\"<>")
        let sanitized = suggestedFilename
            .components(separatedBy: invalidCharacters)
            .joined(separator: "_")
            .trimmingCharacters(in: .whitespacesAndNewlines)

        return sanitized.isEmpty ? "download" : sanitized
    }

    private func uniqueDownloadDestination(for suggestedFilename: String) throws -> URL {
        let downloadsDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("InAppBrowserDownloads", isDirectory: true)
        try FileManager.default.createDirectory(at: downloadsDirectory, withIntermediateDirectories: true)

        let sanitizedFilename = sanitizeDownloadFilename(suggestedFilename)
        let baseName = (sanitizedFilename as NSString).deletingPathExtension
        let fileExtension = (sanitizedFilename as NSString).pathExtension

        var candidateURL = downloadsDirectory.appendingPathComponent(sanitizedFilename)
        var duplicateIndex = 1

        while FileManager.default.fileExists(atPath: candidateURL.path) || !reserveDownloadDestination(candidateURL) {
            let duplicateSuffix = "-\(duplicateIndex)"
            let duplicateFilename = fileExtension.isEmpty
                ? baseName + duplicateSuffix
                : baseName + duplicateSuffix + "." + fileExtension
            candidateURL = downloadsDirectory.appendingPathComponent(duplicateFilename)
            duplicateIndex += 1
        }

        return candidateURL
    }

    private func reserveDownloadDestination(_ candidateURL: URL) -> Bool {
        DownloadReservationStore.lock.lock()
        defer { DownloadReservationStore.lock.unlock() }

        let candidatePath = candidateURL.path
        guard !DownloadReservationStore.paths.contains(candidatePath) else {
            return false
        }
        DownloadReservationStore.paths.insert(candidatePath)
        return true
    }

    private func releaseDownloadDestination(_ destinationURL: URL?) {
        guard let destinationURL else {
            return
        }

        DownloadReservationStore.lock.lock()
        DownloadReservationStore.paths.remove(destinationURL.path)
        DownloadReservationStore.lock.unlock()
    }

    private func normalizedMimeType(_ mimeType: String?, fileURL: URL) -> String? {
        let candidate = mimeType?
            .components(separatedBy: ";")
            .first?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .lowercased()

        let genericMimeTypes: Set<String> = [
            "application/octet-stream",
            "binary/octet-stream",
            "application/download",
            "application/x-download",
            "application/binary",
            "application/x-binary"
        ]

        if let candidate, !candidate.isEmpty, !genericMimeTypes.contains(candidate) {
            return candidate
        }

        switch fileURL.pathExtension.lowercased() {
        case "pdf":
            return "application/pdf"
        case "json":
            return "application/json"
        case "txt":
            return "text/plain"
        case "csv":
            return "text/csv"
        case "html", "htm":
            return "text/html"
        case "xhtml":
            return "application/xhtml+xml"
        case "png":
            return "image/png"
        case "jpg", "jpeg":
            return "image/jpeg"
        case "gif":
            return "image/gif"
        case "webp":
            return "image/webp"
        case "svg":
            return "image/svg+xml"
        default:
            return nil
        }
    }

    private func shouldPreviewDownloadedFile(_ fileURL: URL, mimeType: String?) -> Bool {
        let activeExtensions: Set<String> = ["html", "htm", "xhtml", "svg"]
        if activeExtensions.contains(fileURL.pathExtension.lowercased()) {
            return false
        }

        guard let normalizedMimeType = normalizedMimeType(mimeType, fileURL: fileURL) else {
            return false
        }

        switch normalizedMimeType {
        case "text/html", "application/xhtml+xml", "image/svg+xml":
            return false
        default:
            break
        }

        return normalizedMimeType == "application/pdf" ||
            normalizedMimeType == "application/json" ||
            normalizedMimeType.starts(with: "text/") ||
            normalizedMimeType.starts(with: "image/")
    }

    private func emitDownloadCompleted(_ fileURL: URL, mimeType: String?, sourceURL: String?, handledBy: String) {
        var data: [String: Any] = [
            "fileName": fileURL.lastPathComponent,
            "path": fileURL.path,
            "localUrl": fileURL.absoluteString,
            "handledBy": handledBy
        ]
        if let sourceURL {
            data["sourceUrl"] = sourceURL
        }
        if let mimeType {
            data["mimeType"] = mimeType
        }
        emit("downloadCompleted", data: data)
    }

    private func emitDownloadFailed(sourceURL: String?, fileName: String? = nil, mimeType: String? = nil, error: String) {
        var data: [String: Any] = ["error": error]
        if let sourceURL {
            data["sourceUrl"] = sourceURL
        }
        if let fileName {
            data["fileName"] = fileName
        }
        if let mimeType {
            data["mimeType"] = mimeType
        }
        emit("downloadFailed", data: data)
    }

    private func previewDownloadedFile(_ fileURL: URL, mimeType: String?, sourceURL: String?) {
        DispatchQueue.main.async {
            if self.allowsFileURL && self.shouldPreviewDownloadedFile(fileURL, mimeType: mimeType) {
                let accessURL = fileURL.deletingLastPathComponent()
                self.source = .file(fileURL, access: accessURL)
                self.load(file: fileURL, access: accessURL)
                self.emitDownloadCompleted(fileURL, mimeType: mimeType, sourceURL: sourceURL, handledBy: "inAppBrowser")
                return
            }

            self.previewItemURL = fileURL
            let previewController = QLPreviewController()
            previewController.dataSource = self
            previewController.delegate = self
            self.present(previewController, animated: true)
            self.emitDownloadCompleted(fileURL, mimeType: mimeType, sourceURL: sourceURL, handledBy: "systemPreview")
        }
    }

    private func parseBlobBridgePayload(_ payload: Any) -> [String: Any]? {
        if let dictionary = payload as? [String: Any] {
            return dictionary
        }

        if let jsonString = payload as? String,
           let data = jsonString.data(using: .utf8),
           let dictionary = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
            return dictionary
        }

        return nil
    }

    private func blobDownloadFileName(from payload: [String: Any], fallback: String = "download") -> String {
        sanitizeDownloadFilename(payload["fileName"] as? String ?? fallback)
    }

    private func cleanupBlobDownloadSession(_ session: BlobDownloadSession?, deleteFile: Bool) {
        guard let session else {
            return
        }

        try? session.fileHandle.close()
        if deleteFile {
            try? FileManager.default.removeItem(at: session.destinationURL)
            releaseDownloadDestination(session.destinationURL)
        }
    }

    @discardableResult
    private func abortBlobDownloadSession(sessionId: String, deleteFile: Bool) -> BlobDownloadSession? {
        guard let session = blobDownloadSessions.removeValue(forKey: sessionId) else {
            return nil
        }
        cleanupBlobDownloadSession(session, deleteFile: deleteFile)
        return session
    }

    private func handleLegacyBlobDownloadPayload(_ payload: Any) {
        guard handleDownloads else {
            return
        }

        guard let jsonPayload = parseBlobBridgePayload(payload) else {
            emitDownloadFailed(sourceURL: nil, error: "Blob download payload is missing")
            return
        }

        let base64 = jsonPayload["base64"] as? String ?? ""
        guard !base64.isEmpty else {
            emitDownloadFailed(sourceURL: jsonPayload["sourceUrl"] as? String, error: "Blob download payload is empty")
            return
        }

        let declaredSize = (jsonPayload["size"] as? NSNumber)?.int64Value
        let estimatedSize = Int64((Double(base64.count) * 3.0 / 4.0).rounded(.up))
        let decodedSize = max(declaredSize ?? estimatedSize, estimatedSize)
        if decodedSize > Int64(BlobDownloadSupport.maxLegacyBytes) {
            emitDownloadFailed(
                sourceURL: jsonPayload["sourceUrl"] as? String,
                fileName: blobDownloadFileName(from: jsonPayload),
                mimeType: jsonPayload["mimeType"] as? String,
                error: "Blob download is too large for the legacy bridge"
            )
            return
        }

        do {
            guard let data = Data(base64Encoded: base64) else {
                throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Blob download payload is invalid"])
            }

            let fileName = blobDownloadFileName(from: jsonPayload)
            let destinationURL = try uniqueDownloadDestination(for: fileName)
            try data.write(to: destinationURL, options: .atomic)
            let mimeType = jsonPayload["mimeType"] as? String
            let sourceURL = jsonPayload["sourceUrl"] as? String
            previewDownloadedFile(destinationURL, mimeType: mimeType, sourceURL: sourceURL)
        } catch {
            emitDownloadFailed(
                sourceURL: jsonPayload["sourceUrl"] as? String,
                fileName: blobDownloadFileName(from: jsonPayload),
                mimeType: jsonPayload["mimeType"] as? String,
                error: "Failed to save blob download: \(error.localizedDescription)"
            )
        }
    }

    private func startBlobDownloadPayload(_ payload: Any) {
        guard handleDownloads else {
            return
        }

        do {
            guard let jsonPayload = parseBlobBridgePayload(payload) else {
                throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Blob download start payload is missing"])
            }

            let sessionId = jsonPayload["sessionId"] as? String ?? ""
            guard !sessionId.isEmpty else {
                throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Blob download session id is missing"])
            }

            if blobDownloadSessions[sessionId] != nil {
                throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Blob download session already exists"])
            }

            let fileName = blobDownloadFileName(from: jsonPayload)
            let destinationURL = try uniqueDownloadDestination(for: fileName)
            let fileHandle = try FileHandle(forWritingTo: destinationURL)
            let expectedSize = (jsonPayload["size"] as? NSNumber)?.int64Value
            blobDownloadSessions[sessionId] = BlobDownloadSession(
                destinationURL: destinationURL,
                sourceURL: jsonPayload["sourceUrl"] as? String,
                mimeType: jsonPayload["mimeType"] as? String,
                fileHandle: fileHandle,
                expectedSize: expectedSize
            )
        } catch {
            emitDownloadFailed(sourceURL: nil, error: "Failed to start blob download: \(error.localizedDescription)")
        }
    }

    private func appendBlobDownloadChunkPayload(_ payload: Any) {
        guard handleDownloads else {
            return
        }

        guard let jsonPayload = parseBlobBridgePayload(payload),
              let sessionId = jsonPayload["sessionId"] as? String,
              !sessionId.isEmpty,
              var session = blobDownloadSessions[sessionId] else {
            return
        }

        let base64 = jsonPayload["base64"] as? String ?? ""
        guard !base64.isEmpty, let data = Data(base64Encoded: base64) else {
            abortBlobDownloadSession(sessionId: sessionId, deleteFile: true)
            emitDownloadFailed(sourceURL: session.sourceURL, error: "Failed to save blob download")
            return
        }

        do {
            if let expectedSize = session.expectedSize, session.bytesWritten + Int64(data.count) > expectedSize {
                throw NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Blob download exceeded expected size"])
            }

            try session.fileHandle.write(contentsOf: data)
            session.bytesWritten += Int64(data.count)
            blobDownloadSessions[sessionId] = session
        } catch {
            abortBlobDownloadSession(sessionId: sessionId, deleteFile: true)
            emitDownloadFailed(sourceURL: session.sourceURL, error: "Failed to save blob download: \(error.localizedDescription)")
        }
    }

    private func finishBlobDownloadPayload(_ payload: Any) {
        guard handleDownloads else {
            return
        }

        guard let jsonPayload = parseBlobBridgePayload(payload),
              let sessionId = jsonPayload["sessionId"] as? String,
              !sessionId.isEmpty,
              let session = blobDownloadSessions.removeValue(forKey: sessionId) else {
            emitDownloadFailed(sourceURL: nil, error: "Blob download session was not initialized")
            return
        }

        defer {
            try? session.fileHandle.close()
        }

        if let expectedSize = session.expectedSize, session.bytesWritten != expectedSize {
            cleanupBlobDownloadSession(session, deleteFile: true)
            emitDownloadFailed(
                sourceURL: session.sourceURL,
                fileName: session.destinationURL.lastPathComponent,
                mimeType: session.mimeType,
                error: "Blob download size mismatch"
            )
            return
        }

        previewDownloadedFile(session.destinationURL, mimeType: session.mimeType, sourceURL: session.sourceURL)
    }

    private func abortBlobDownloadPayload(_ payload: Any) {
        guard handleDownloads else {
            return
        }

        let jsonPayload = parseBlobBridgePayload(payload)
        let sessionId = jsonPayload?["sessionId"] as? String ?? ""
        let session = abortBlobDownloadSession(sessionId: sessionId, deleteFile: true)
        let reason = jsonPayload?["reason"] as? String ?? "Blob download aborted"
        emitDownloadFailed(
            sourceURL: session?.sourceURL ?? jsonPayload?["sourceUrl"] as? String,
            fileName: session?.destinationURL.lastPathComponent ?? jsonPayload?["fileName"] as? String,
            mimeType: session?.mimeType ?? jsonPayload?["mimeType"] as? String,
            error: reason
        )
    }

    private func handleBlobDownloadFromPage(blobUrl: String, mimeType: String?, contentDisposition: String?) {
        guard handleDownloads, let webView else {
            emitDownloadFailed(sourceURL: blobUrl, error: "Blob download requires an active WebView")
            return
        }

        let fallbackMimeType = normalizedMimeType(mimeType, fileURL: URL(fileURLWithPath: "download")) ?? "application/octet-stream"
        let fallbackFileName = sanitizeDownloadFilename(
            contentDisposition?.components(separatedBy: "filename=").last?
                .trimmingCharacters(in: CharacterSet(charactersIn: "\"' "))
                ?? "download"
        )

        let params: [String: Any] = [
            "blobUrl": blobUrl,
            "fallbackMimeType": fallbackMimeType,
            "fallbackFileName": fallbackFileName,
            "legacyMaxBytes": BlobDownloadSupport.maxLegacyBytes,
            "chunkSize": BlobDownloadSupport.chunkBytes
        ]

        guard JSONSerialization.isValidJSONObject(params),
              let paramsData = try? JSONSerialization.data(withJSONObject: params),
              let paramsJson = String(data: paramsData, encoding: .utf8) else {
            emitDownloadFailed(sourceURL: blobUrl, error: "Failed to prepare blob download script")
            return
        }

        let script = """
        (function(params) {
          const blobUrl = params.blobUrl;
          const fallbackMimeType = params.fallbackMimeType;
          const fallbackFileName = params.fallbackFileName;
          const legacyMaxBytes = params.legacyMaxBytes;
          const chunkSize = params.chunkSize;
          const matchingLink = Array.from(document.querySelectorAll('a[download]')).find(function(link) { return link.href === blobUrl; });
          const bridge = (window.mobileApp && window.mobileApp.startBlobDownload && window.mobileApp.appendBlobDownloadChunk && window.mobileApp.finishBlobDownload)
            ? window.mobileApp
            : (window.mobileApp && window.mobileApp.handleBlobDownload)
              ? window.mobileApp
              : null;
          const sessionId = 'blob-download-' + Date.now() + '-' + Math.random().toString(36).slice(2);
          let fileName = fallbackFileName;
          const readChunkAsBase64 = function(chunk) {
            return new Promise(function(resolve, reject) {
              const reader = new FileReader();
              reader.onloadend = function() {
                const dataUrl = reader.result || '';
                resolve(String(dataUrl).split(',').pop() || '');
              };
              reader.onerror = function() {
                reject(reader.error || new Error('Failed to read blob chunk'));
              };
              reader.readAsDataURL(chunk);
            });
          };

          return (async function() {
            if (!bridge) {
              throw new Error('Blob download bridge is not available');
            }

            const response = await fetch(blobUrl);
            const blob = await response.blob();
            fileName = (matchingLink && matchingLink.getAttribute('download')) || fallbackFileName;

            if (bridge.startBlobDownload && bridge.appendBlobDownloadChunk && bridge.finishBlobDownload) {
              bridge.startBlobDownload(JSON.stringify({
                sessionId: sessionId,
                fileName: fileName,
                sourceUrl: blobUrl,
                mimeType: blob.type || fallbackMimeType,
                size: blob.size
              }));
              for (let offset = 0; offset < blob.size; offset += chunkSize) {
                const base64 = await readChunkAsBase64(blob.slice(offset, offset + chunkSize));
                bridge.appendBlobDownloadChunk(JSON.stringify({ sessionId: sessionId, base64: base64 }));
              }
              bridge.finishBlobDownload(JSON.stringify({ sessionId: sessionId }));
              return;
            }

            if (blob.size > legacyMaxBytes) {
              throw new Error('Blob download is too large for the legacy bridge');
            }

            const base64 = await readChunkAsBase64(blob);
            bridge.handleBlobDownload(JSON.stringify({
              fileName: fileName,
              sourceUrl: blobUrl,
              mimeType: blob.type || fallbackMimeType,
              size: blob.size,
              base64: base64
            }));
          })().catch(function(error) {
            if (bridge && bridge.abortBlobDownload) {
              bridge.abortBlobDownload(JSON.stringify({
                sessionId: sessionId,
                fileName: fileName,
                sourceUrl: blobUrl,
                mimeType: fallbackMimeType,
                reason: String((error && error.message) || error || 'Blob download failed')
              }));
            }
            console.error('Failed to capture blob download', error);
          });
        })(\(paramsJson));
        """

        DispatchQueue.main.async {
            webView.evaluateJavaScript(script) { _, error in
                if let error {
                    self.emitDownloadFailed(sourceURL: blobUrl, error: "Failed to capture blob download: \(error.localizedDescription)")
                }
            }
        }
    }

    internal var customUserAgent: String? {
        didSet {
            guard let agent = userAgent else {
                return
            }
            webView?.customUserAgent = agent
        }
    }

    open var userAgent: String? {
        didSet {
            guard let originalUserAgent = originalUserAgent, let userAgent = userAgent else {
                return
            }
            webView?.customUserAgent = [originalUserAgent, userAgent].joined(separator: " ")
        }
    }

    open var pureUserAgent: String? {
        didSet {
            guard let agent = pureUserAgent else {
                return
            }
            webView?.customUserAgent = agent
        }
    }

    open var websiteTitleInNavigationBar = true
    open var doneBarButtonItemPosition: NavigationBarPosition = .right
    open var showArrowAsClose = false
    open var documentStartUserScripts: [String] = []
    open var preShowScript: String?
    open var preShowScriptInjectionTime: String = "pageLoad" // "documentStart" or "pageLoad"
    open var leftNavigationBarItemTypes: [BarButtonItemType] = []
    open var rightNavigaionBarItemTypes: [BarButtonItemType] = []

    // Status bar style to be applied
    open var statusBarStyle: UIStatusBarStyle = .default

    // Status bar background view
    var statusBarBackgroundView: UIView?

    // Status bar height
    private var statusBarHeight: CGFloat {
        let windowScene = UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .first { $0.activationState == .foregroundActive }
            ?? UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .first
        return windowScene?.statusBarManager?.statusBarFrame.height ?? 0
    }

    private func applyNavigationBarBackground(color: UIColor) {
        guard let navigationBar = navigationController?.navigationBar else {
            return
        }

        navigationBar.backgroundColor = color
        navigationBar.barTintColor = color
        navigationBar.isTranslucent = false
        navigationBar.setBackgroundImage(UIImage(), for: .default)
        navigationBar.shadowImage = UIImage()
        navigationBar.setValue(true, forKey: "hidesShadow")

        if #available(iOS 13.0, *) {
            let appearance = UINavigationBarAppearance()
            appearance.configureWithOpaqueBackground()
            appearance.backgroundColor = color
            appearance.backgroundEffect = nil
            appearance.shadowColor = .clear
            appearance.shadowImage = UIImage()

            if let titleTextAttributes = navigationBar.titleTextAttributes {
                appearance.titleTextAttributes = titleTextAttributes
            }
            if let largeTitleTextAttributes = navigationBar.largeTitleTextAttributes {
                appearance.largeTitleTextAttributes = largeTitleTextAttributes
            }

            navigationBar.standardAppearance = appearance
            navigationBar.compactAppearance = appearance
            navigationBar.scrollEdgeAppearance = appearance
            navigationBar.compactScrollEdgeAppearance = appearance
        }
    }

    // Make status bar background with colored view underneath
    open func setupStatusBarBackground(color: UIColor) {
        statusBarBackgroundView?.removeFromSuperview()
        statusBarBackgroundView = nil

        guard let navController = navigationController else {
            return
        }

        if let passThrough = navController.view as? PassThroughView {
            // Never paint the full-screen overlay — that fills the custom y-offset gap.
            passThrough.backgroundColor = .clear
        }

        let navigationBar = navController.navigationBar
        let hostCandidate = StatusBarBackgroundLayoutSupport.hostView(
            navigationControllerView: navController.view,
            framedContentView: (navController.view as? PassThroughView)?.framedContentView
        )
        let navigationBarInHost = hostCandidate.map { navigationBar.isDescendant(of: $0) } ?? false
        let placement = StatusBarBackgroundLayoutSupport.placement(
            isPassThroughOverlay: navController.view is PassThroughView,
            blankNavigationTab: blankNavigationTab,
            navigationBarInHostHierarchy: navigationBarInHost,
            isFramedCustomOverlay: shouldPresentAsFramedOverlay
        )

        guard case .host(let pinToNavigationBar) = placement,
              let hostView = hostCandidate else {
            return
        }

        let backgroundView = UIView()
        statusBarBackgroundView = backgroundView
        hostView.insertSubview(backgroundView, at: 0)
        backgroundView.translatesAutoresizingMaskIntoConstraints = false
        backgroundView.backgroundColor = color

        var constraints = [
            backgroundView.topAnchor.constraint(equalTo: hostView.topAnchor),
            backgroundView.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
            backgroundView.trailingAnchor.constraint(equalTo: hostView.trailingAnchor)
        ]

        if pinToNavigationBar {
            constraints.append(
                backgroundView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: 1)
            )
            applyNavigationBarBackground(color: color)
        } else {
            // Blank toolbar / detached nav bar: cover status-bar height only.
            // Avoid pinning to a hidden UINavigationBar (removed from hierarchy → crash).
            constraints.append(
                backgroundView.heightAnchor.constraint(equalToConstant: max(statusBarHeight, 20))
            )
        }

        NSLayoutConstraint.activate(constraints)
    }

    // Override to use our custom status bar style
    override open var preferredStatusBarStyle: UIStatusBarStyle {
        return statusBarStyle
    }

    // Force status bar style update when needed
    open func updateStatusBarStyle() {
        setNeedsStatusBarAppearanceUpdate()
    }

    open var backBarButtonItemImage: UIImage?
    open var forwardBarButtonItemImage: UIImage?
    open var reloadBarButtonItemImage: UIImage?
    open var stopBarButtonItemImage: UIImage?
    open var activityBarButtonItemImage: UIImage?

    open var buttonNearDoneIcon: UIImage?
    open var showScreenshotButton = false

    fileprivate var webView: WKWebView?
    fileprivate var progressView: UIProgressView?

    fileprivate var previousNavigationBarState: (tintColor: UIColor, hidden: Bool) = (.black, false)
    fileprivate var previousToolbarState: (tintColor: UIColor, hidden: Bool) = (.black, false)

    fileprivate var originalUserAgent: String?

    fileprivate lazy var backBarButtonItem: UIBarButtonItem = {
        let navBackImage = UIImage(systemName: "chevron.backward")?.withRenderingMode(.alwaysTemplate)
        let barButtonItem = UIBarButtonItem(image: navBackImage, style: .plain, target: self, action: #selector(backDidClick(sender:)))
        if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
            barButtonItem.tintColor = tintColor
        }
        return barButtonItem
    }()

    fileprivate lazy var forwardBarButtonItem: UIBarButtonItem = {
        let forwardImage = UIImage(systemName: "chevron.forward")?.withRenderingMode(.alwaysTemplate)
        let barButtonItem = UIBarButtonItem(image: forwardImage, style: .plain, target: self, action: #selector(forwardDidClick(sender:)))
        if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
            barButtonItem.tintColor = tintColor
        }
        return barButtonItem
    }()

    fileprivate lazy var reloadBarButtonItem: UIBarButtonItem = {
        if let image = reloadBarButtonItemImage {
            return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(reloadDidClick(sender:)))
        } else {
            return UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(reloadDidClick(sender:)))
        }
    }()

    fileprivate lazy var stopBarButtonItem: UIBarButtonItem = {
        if let image = stopBarButtonItemImage {
            return UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(stopDidClick(sender:)))
        } else {
            return UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(stopDidClick(sender:)))
        }
    }()

    fileprivate lazy var activityBarButtonItem: UIBarButtonItem = {
        // Check if custom image is provided
        if let image = activityBarButtonItemImage {
            let button = UIBarButtonItem(image: image.withRenderingMode(.alwaysTemplate),
                                         style: .plain,
                                         target: self,
                                         action: #selector(activityDidClick(sender:)))

            // Apply tint from navigation bar or from tintColor property
            if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
                button.tintColor = tintColor
            }

            print("[DEBUG] Created activity button with custom image")
            return button
        } else {
            // Use system share icon
            let button = UIBarButtonItem(barButtonSystemItem: .action,
                                         target: self,
                                         action: #selector(activityDidClick(sender:)))

            // Apply tint from navigation bar or from tintColor property
            if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
                button.tintColor = tintColor
            }

            print("[DEBUG] Created activity button with system action icon")
            return button
        }
    }()

    fileprivate lazy var doneBarButtonItem: UIBarButtonItem = {
        if showArrowAsClose {
            // Show chevron icon when showArrowAsClose is true (originally was arrow.left)
            let chevronImage = UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate)
            let barButtonItem = UIBarButtonItem(image: chevronImage, style: .plain, target: self, action: #selector(doneDidClick(sender:)))
            if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
                barButtonItem.tintColor = tintColor
            }
            return barButtonItem
        } else {
            // Show X icon by default
            let xImage = UIImage(systemName: "xmark")?.withRenderingMode(.alwaysTemplate)
            let barButtonItem = UIBarButtonItem(image: xImage, style: .plain, target: self, action: #selector(doneDidClick(sender:)))
            if let tintColor = self.tintColor ?? self.navigationController?.navigationBar.tintColor {
                barButtonItem.tintColor = tintColor
            }
            return barButtonItem
        }
    }()

    fileprivate lazy var flexibleSpaceBarButtonItem: UIBarButtonItem = {
        return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    }()

    fileprivate var credentials: WKWebViewCredentials?

    var textZoom: Int?

    var capableWebView: WKWebView? {
        return webView
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
        webView?.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
        if websiteTitleInNavigationBar {
            webView?.removeObserver(self, forKeyPath: titleKeyPath)
        }
        webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
    }

    override open func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        if self.isBeingDismissed || self.isMovingFromParent {
            self.cleanupWebView()
        }

        if let capacitorStatusBar = capacitorStatusBar {
            self.capBrowserPlugin?.bridge?.webView?.superview?.addSubview(capacitorStatusBar)
            self.capBrowserPlugin?.bridge?.webView?.frame.origin.y = capacitorStatusBar.frame.height
        }
    }

    override open func viewDidLoad() {
        super.viewDidLoad()
        if self.webView == nil {
            self.initWebview()
        }

        // Apply navigation gestures setting
        updateNavigationGestures()

        // Force all buttons to use tint color
        updateButtonTintColors()

        // Extra call to ensure buttonNearDone is visible
        if buttonNearDoneIcon != nil {
            // Delay slightly to ensure navigation items are set up
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
                self?.updateButtonTintColors()

                // Force update UI if needed
                self?.navigationController?.navigationBar.setNeedsLayout()

            }
        }
    }

    func updateTitleAppearance() {
        guard titleIcon != nil || titleFontFamily != nil else {
            navigationItem.titleView = nil
            return
        }

        let currentTitle = navigationItem.title ?? title ?? webView?.url?.host ?? ""
        let titleColor = navigationController?.navigationBar.titleTextAttributes?[.foregroundColor] as? UIColor
            ?? tintColor
            ?? navigationController?.navigationBar.tintColor
            ?? .label
        let fontSize = UIFont.preferredFont(forTextStyle: .headline).pointSize
        let titleFont = titleFontFamily.flatMap { UIFont(name: $0, size: fontSize) }
            ?? UIFont.systemFont(ofSize: fontSize, weight: .semibold)

        let label = UILabel()
        label.text = currentTitle
        label.textColor = titleColor
        label.font = titleFont
        label.lineBreakMode = .byTruncatingTail
        label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

        let stackView = UIStackView()
        stackView.axis = .horizontal
        stackView.alignment = .center
        stackView.spacing = 6

        if let titleIcon {
            let imageView = UIImageView(image: titleIcon.withRenderingMode(.alwaysTemplate))
            imageView.tintColor = titleColor
            imageView.contentMode = .scaleAspectFit
            imageView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                imageView.widthAnchor.constraint(equalToConstant: 18),
                imageView.heightAnchor.constraint(equalToConstant: 18)
            ])
            stackView.addArrangedSubview(imageView)
        }

        stackView.addArrangedSubview(label)
        navigationItem.titleView = stackView
    }

    override open func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        syncWebViewSafeAreaLayout()
        refreshWebViewViewportIfNeeded()
    }

    override open func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        syncWebViewSafeAreaLayout()
    }

    func updateButtonTintColors() {
        // Ensure all button items use the navigation bar's tint color
        if let tintColor = navigationController?.navigationBar.tintColor {
            backBarButtonItem.tintColor = tintColor
            forwardBarButtonItem.tintColor = tintColor
            reloadBarButtonItem.tintColor = tintColor
            stopBarButtonItem.tintColor = tintColor
            activityBarButtonItem.tintColor = tintColor
            doneBarButtonItem.tintColor = tintColor

            // Update navigation items
            if let leftItems = navigationItem.leftBarButtonItems {
                for item in leftItems {
                    item.tintColor = tintColor
                }
            }

            if let rightItems = navigationItem.rightBarButtonItems {
                for item in rightItems {
                    item.tintColor = tintColor
                }
            }

            // Create buttonNearDone button with the correct tint color if it doesn't already exist
            if buttonNearDoneIcon != nil {
                if doneBarButtonItemPosition == .left,
                   navigationItem.leftBarButtonItems?.count == 1,
                   navigationItem.leftBarButtonItems?.first == doneBarButtonItem {
                    let buttonItem = makeButtonNearDoneBarButtonItem(tintColor: tintColor)
                    navigationItem.leftBarButtonItems?.append(buttonItem)
                } else if doneBarButtonItemPosition == .right,
                          navigationItem.rightBarButtonItems?.count == 1,
                          navigationItem.rightBarButtonItems?.first == doneBarButtonItem {
                    let buttonItem = makeButtonNearDoneBarButtonItem(tintColor: tintColor)
                    navigationItem.rightBarButtonItems?.append(buttonItem)
                }
            }
        }
        updateTitleAppearance()
    }

    override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        // Update colors when appearance changes
        if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
            // Update tint colors
            let isDarkMode = traitCollection.userInterfaceStyle == .dark
            let textColor = isDarkMode ? UIColor.white : UIColor.black

            if let navBar = navigationController?.navigationBar {
                if navBar.backgroundColor == UIColor.black || navBar.backgroundColor == UIColor.white {
                    navBar.backgroundColor = isDarkMode ? UIColor.black : UIColor.white
                    navBar.tintColor = textColor
                    navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: textColor]

                    // Update all buttons
                    updateButtonTintColors()
                }
            }
        }
    }

    open func setCredentials(credentials: WKWebViewCredentials?) {
        self.credentials = credentials
    }

    // Method to send a message from Swift to JavaScript
    open func postMessageToJS(message: [String: Any]) {
        guard let jsonData = try? JSONSerialization.data(withJSONObject: message, options: []),
              let jsonString = String(data: jsonData, encoding: .utf8) else {
            print("[InAppBrowser] Failed to serialize message to JSON")
            return
        }

        // Safely build the script to avoid any potential issues
        let script = "window.dispatchEvent(new CustomEvent('messageFromNative', { detail: \(jsonString) }));"

        DispatchQueue.main.async {
            self.webView?.evaluateJavaScript(script) { _, error in
                if let error = error {
                    print("[InAppBrowser] JavaScript evaluation error in postMessageToJS: \(error)")
                }
            }
        }
    }

    private func payload(with data: [String: Any] = [:]) -> [String: Any] {
        guard !instanceId.isEmpty else {
            return data
        }
        var payload = data
        payload["id"] = instanceId
        return payload
    }

    private func emit(_ eventName: String, data: [String: Any] = [:]) {
        capBrowserPlugin?.notifyListeners(eventName, data: payload(with: data))
    }

    private func startObservingKeyboardViewportChanges() {
        guard !isObservingKeyboardViewportChanges else {
            return
        }

        isObservingKeyboardViewportChanges = true
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(keyboardViewportDidChange(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        center.addObserver(self, selector: #selector(keyboardViewportDidChange(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
        center.addObserver(self, selector: #selector(keyboardViewportDidChange(_:)), name: UIResponder.keyboardDidChangeFrameNotification, object: nil)
    }

    @objc private func keyboardViewportDidChange(_ notification: Notification) {
        scheduleWebViewViewportRefresh(force: true, delay: viewportRefreshDelay(from: notification))
    }

    private func viewportRefreshDelay(from notification: Notification) -> TimeInterval {
        guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else {
            return 0.05
        }

        return max(0.05, duration.doubleValue)
    }

    private func scheduleWebViewViewportRefresh(force: Bool = false, delay: TimeInterval = 0.05) {
        DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
            self?.refreshWebViewViewportIfNeeded(force: force)
        }
    }

    private func refreshWebViewViewportIfNeeded(force: Bool = false) {
        guard let webView = self.webView, webView.superview != nil else {
            return
        }

        if force {
            self.view.setNeedsLayout()
            self.view.layoutIfNeeded()
        }

        let currentSize = webView.bounds.size
        guard WebViewViewportLayoutSupport.shouldRefreshViewport(
            previousSize: lastViewportRefreshSize,
            currentSize: currentSize,
            force: force
        ) else {
            return
        }

        lastViewportRefreshSize = currentSize
        webView.setNeedsLayout()
        webView.layoutIfNeeded()
        webView.scrollView.setNeedsLayout()
        webView.scrollView.layoutIfNeeded()
        webView.evaluateJavaScript("window.dispatchEvent(new Event('resize'));", completionHandler: nil)
    }

    private func configureWebViewScrollViewSafeArea(_ webView: WKWebView) {
        if #available(iOS 11.0, *) {
            webView.scrollView.contentInsetAdjustmentBehavior = WebViewSafeAreaLayoutSupport
                .contentInsetAdjustmentBehavior(enabledSafeBottomMargin: enabledSafeBottomMargin)
        }

        webView.insetsLayoutMarginsFromSafeArea = WebViewSafeAreaLayoutSupport
            .shouldInsetLayoutMarginsFromSafeArea(enabledSafeBottomMargin: enabledSafeBottomMargin)

        if !enabledSafeBottomMargin {
            webView.scrollView.contentInset = .zero
            webView.scrollView.scrollIndicatorInsets = .zero
        }
    }

    private func syncWebViewSafeAreaLayout() {
        guard let webView = self.webView, webView.superview != nil else {
            return
        }

        let rawSafeAreaBottomInset = WebViewSafeAreaLayoutSupport.rawSystemBottomInset(
            effectiveBottomInset: view.safeAreaInsets.bottom,
            additionalBottomInset: additionalSafeAreaInsets.bottom
        )

        let bottomOffset = WebViewSafeAreaLayoutSupport.additionalBottomSafeAreaOffset(
            enabledSafeBottomMargin: enabledSafeBottomMargin,
            safeAreaBottomInset: rawSafeAreaBottomInset
        )
        if additionalSafeAreaInsets.bottom != bottomOffset {
            additionalSafeAreaInsets.bottom = bottomOffset
        }

        configureWebViewScrollViewSafeArea(webView)

        let topInset = WebViewSafeAreaLayoutSupport.cssTopInset(
            enabledSafeTopMargin: enabledSafeTopMargin,
            safeAreaTopInset: view.safeAreaInsets.top
        )
        let bottomInset = WebViewSafeAreaLayoutSupport.cssBottomInset(
            enabledSafeBottomMargin: enabledSafeBottomMargin,
            safeAreaBottomInset: rawSafeAreaBottomInset
        )
        let leftInset = view.safeAreaInsets.left
        let rightInset = view.safeAreaInsets.right

        let roundedInsets = InjectedSafeAreaInsets(
            top: Int(topInset.rounded()),
            bottom: Int(bottomInset.rounded()),
            left: Int(leftInset.rounded()),
            right: Int(rightInset.rounded())
        )
        if lastInjectedSafeAreaInsets == roundedInsets {
            return
        }
        lastInjectedSafeAreaInsets = roundedInsets

        let script = WebViewSafeAreaLayoutSupport.safeAreaCssVariablesScript(
            top: topInset,
            bottom: bottomInset,
            left: leftInset,
            right: rightInset
        )
        webView.evaluateJavaScript(script, completionHandler: nil)
    }

    private func jsonString(from object: Any) -> String? {
        guard JSONSerialization.isValidJSONObject(object),
              let data = try? JSONSerialization.data(withJSONObject: object),
              let string = String(data: data, encoding: .utf8) else {
            return nil
        }
        return string
    }

    private func resolveJavaScriptScreenshot(requestId: String, result: [String: Any]) {
        guard let payload = jsonString(from: ["requestId": requestId, "result": result]) else {
            return
        }
        let script = "window.__capgoInAppBrowserResolveScreenshot(\(payload));"
        DispatchQueue.main.async {
            self.webView?.evaluateJavaScript(script, completionHandler: nil)
        }
    }

    private func rejectJavaScriptScreenshot(requestId: String, message: String) {
        guard let payload = jsonString(from: ["requestId": requestId, "message": message]) else {
            return
        }
        let script = "window.__capgoInAppBrowserRejectScreenshot(\(payload));"
        DispatchQueue.main.async {
            self.webView?.evaluateJavaScript(script, completionHandler: nil)
        }
    }

    func takeScreenshot(emitEvent: Bool = true, completion: @escaping (Result<[String: Any], Error>) -> Void) {
        DispatchQueue.main.async {
            guard let webView = self.webView else {
                completion(.failure(NSError(domain: "InAppBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "WebView is not initialized"])))
                return
            }

            let targetSize = webView.bounds.size
            guard targetSize.width > 0, targetSize.height > 0 else {
                completion(.failure(NSError(domain: "InAppBrowser", code: 2, userInfo: [NSLocalizedDescriptionKey: "WebView is not ready to capture a screenshot"])))
                return
            }

            let configuration = WKSnapshotConfiguration()
            configuration.rect = webView.bounds
            configuration.afterScreenUpdates = false

            webView.takeSnapshot(with: configuration) { image, error in
                if let error {
                    completion(.failure(error))
                    return
                }

                guard let image,
                      let imageData = image.pngData() else {
                    completion(.failure(NSError(domain: "InAppBrowser", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to encode screenshot"])))
                    return
                }

                let base64 = imageData.base64EncodedString()
                let result: [String: Any] = [
                    "format": "png",
                    "mimeType": "image/png",
                    "base64": base64,
                    "dataUrl": "data:image/png;base64,\(base64)",
                    "width": Int(image.size.width * image.scale),
                    "height": Int(image.size.height * image.scale)
                ]
                if emitEvent {
                    self.emit("screenshotTaken", data: result)
                }
                completion(.success(result))
            }
        }
    }

    // Method to receive messages from JavaScript
    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "messageHandler" {
            if let messageBody = message.body as? [String: Any] {
                print("Received message from JavaScript:", messageBody)
                emit("messageFromWebview", data: messageBody)
            } else {
                print("Received non-dictionary message from JavaScript:", message.body)
                emit("messageFromWebview", data: ["rawMessage": String(describing: message.body)])
            }
        } else if message.name == "blobDownload" {
            handleLegacyBlobDownloadPayload(message.body)
        } else if message.name == "blobDownloadStart" {
            startBlobDownloadPayload(message.body)
        } else if message.name == "blobDownloadChunk" {
            appendBlobDownloadChunkPayload(message.body)
        } else if message.name == "blobDownloadFinish" {
            finishBlobDownloadPayload(message.body)
        } else if message.name == "blobDownloadAbort" {
            abortBlobDownloadPayload(message.body)
        } else if message.name == "consoleMessageHandler" {
            if let messageBody = message.body as? [String: Any] {
                emit("consoleMessage", data: ConsoleMessageSupport.normalizePayload(from: messageBody))
            } else {
                emit(
                    "consoleMessage",
                    data: ConsoleMessageSupport.normalizePayload(
                        from: [
                            "level": "log",
                            "message": String(describing: message.body)
                        ]
                    )
                )
            }
        } else if message.name == "preShowScriptSuccess" {
            guard let semaphore = preShowSemaphore else {
                print("[InAppBrowser - preShowScriptSuccess]: Semaphore not found")
                return
            }

            semaphore.signal()
        } else if message.name == "preShowScriptError" {
            guard let semaphore = preShowSemaphore else {
                print("[InAppBrowser - preShowScriptError]: Semaphore not found")
                return
            }
            print("[InAppBrowser - preShowScriptError]: Error!!!!")
            semaphore.signal()
        } else if message.name == "capgoProxyBridge" {
            guard let messageBody = message.body as? [String: Any],
                  let token = messageBody["token"] as? String,
                  let requestId = messageBody["requestId"] as? String,
                  let method = messageBody["method"] as? String,
                  let headersJson = messageBody["headersJson"] as? String else {
                return
            }

            let base64Body = messageBody["base64Body"] as? String ?? ""
            let credentialsMode = messageBody["credentialsMode"] as? String ?? "same-origin"
            proxyBridge?.storeRequest(
                token: token,
                requestId: requestId,
                method: method,
                headersJson: headersJson,
                base64Body: base64Body,
                credentialsMode: credentialsMode
            )
        } else if message.name == "close" {
            closeView()
        } else if message.name == "hide" {
            guard allowWebViewJsVisibilityControl else {
                return
            }
            capBrowserPlugin?.setHiddenFromJavaScript(true, sourceId: instanceId)
        } else if message.name == "show" {
            guard allowWebViewJsVisibilityControl else {
                return
            }
            capBrowserPlugin?.setHiddenFromJavaScript(false, sourceId: instanceId)
        } else if message.name == "takeScreenshot" {
            guard let body = message.body as? [String: Any],
                  let requestId = body["requestId"] as? String,
                  !requestId.isEmpty else {
                return
            }

            guard allowScreenshotsFromWebPage else {
                self.rejectJavaScriptScreenshot(requestId: requestId, message: "Screenshot bridge is not enabled for this page")
                return
            }

            takeScreenshot { result in
                switch result {
                case .success(let screenshot):
                    self.resolveJavaScriptScreenshot(requestId: requestId, result: screenshot)
                case .failure(let error):
                    self.rejectJavaScriptScreenshot(requestId: requestId, message: error.localizedDescription)
                }
            }
        } else if message.name == "magicPrint" {
            if let webView = self.webView {
                let printController = UIPrintInteractionController.shared

                let printInfo = UIPrintInfo(dictionary: nil)
                printInfo.outputType = .general
                printInfo.jobName = "Print Job"

                printController.printInfo = printInfo
                printController.printFormatter = webView.viewPrintFormatter()

                printController.present(animated: true, completionHandler: nil)
            }
        }
    }

    private func mobileAppScriptSource() -> String {
        let screenshotControls = allowScreenshotsFromWebPage ? """
                                ,
                                takeScreenshot: function() {
                                        return new Promise(function(resolve, reject) {
                                                if (!(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.takeScreenshot)) {
                                                        reject(new Error('Screenshot bridge is not available'));
                                                        return;
                                                }
                                                var requestId = 'screenshot_' + Date.now() + '_' + Math.random().toString(36).slice(2);
                                                window.__capgoInAppBrowserPendingScreenshots[requestId] = { resolve: resolve, reject: reject };
                                                window.webkit.messageHandlers.takeScreenshot.postMessage({ requestId: requestId });
                                        });
                                }
                """ : ""
        let extraControls = allowWebViewJsVisibilityControl ? """
                                ,
                                hide: function() {
                                        window.webkit.messageHandlers.hide.postMessage(null);
                                },
                                show: function() {
                                        window.webkit.messageHandlers.show.postMessage(null);
                                }
                """ : ""
        return """
                window.__capgoInAppBrowserPendingScreenshots = window.__capgoInAppBrowserPendingScreenshots || {};
                window.__capgoInAppBrowserResolveScreenshot = window.__capgoInAppBrowserResolveScreenshot || function(payload) {
                        var pending = window.__capgoInAppBrowserPendingScreenshots[payload.requestId];
                        if (!pending) {
                                return;
                        }
                        delete window.__capgoInAppBrowserPendingScreenshots[payload.requestId];
                        pending.resolve(payload.result);
                };
                window.__capgoInAppBrowserRejectScreenshot = window.__capgoInAppBrowserRejectScreenshot || function(payload) {
                        var pending = window.__capgoInAppBrowserPendingScreenshots[payload.requestId];
                        if (!pending) {
                                return;
                        }
                        delete window.__capgoInAppBrowserPendingScreenshots[payload.requestId];
                        pending.reject(new Error(payload.message));
                };
                window.mobileApp = Object.assign({}, window.mobileApp || {}, {
                        postMessage: function(message) {
                                if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.messageHandler) {
                                        window.webkit.messageHandlers.messageHandler.postMessage(message);
                                }
                        },
                        close: function() {
                                window.webkit.messageHandlers.close.postMessage(null);
                        },
                        handleBlobDownload: function(payload) {
                                window.webkit.messageHandlers.blobDownload.postMessage(payload);
                        },
                        startBlobDownload: function(payload) {
                                window.webkit.messageHandlers.blobDownloadStart.postMessage(payload);
                        },
                        appendBlobDownloadChunk: function(payload) {
                                window.webkit.messageHandlers.blobDownloadChunk.postMessage(payload);
                        },
                        finishBlobDownload: function(payload) {
                                window.webkit.messageHandlers.blobDownloadFinish.postMessage(payload);
                        },
                        abortBlobDownload: function(payload) {
                                window.webkit.messageHandlers.blobDownloadAbort.postMessage(payload);
                        }\(extraControls)\(screenshotControls)
                });
                if (!window.__capgoInAppBrowserWindowCloseInstalled) {
                        window.__capgoInAppBrowserWindowCloseInstalled = true;
                        window.__capgoInAppBrowserOriginalClose = window.close;
                        window.close = function() {
                                if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.close) {
                                        window.webkit.messageHandlers.close.postMessage(null);
                                        return;
                                }
                                if (typeof window.__capgoInAppBrowserOriginalClose === 'function') {
                                        return window.__capgoInAppBrowserOriginalClose.apply(window, arguments);
                                }
                        };
                }
                """
    }

    private func addMobileAppUserScript(to userContentController: WKUserContentController) {
        let script = WKUserScript(
            source: mobileAppScriptSource(),
            injectionTime: .atDocumentStart,
            forMainFrameOnly: true
        )
        userContentController.addUserScript(script)
    }

    private func addConsoleCaptureUserScript(to userContentController: WKUserContentController) {
        let script = WKUserScript(
            source: ConsoleMessageSupport.captureScriptSource(),
            injectionTime: .atDocumentStart,
            forMainFrameOnly: false
        )
        userContentController.addUserScript(script)
    }

    private func addProxyBridgeUserScripts(to userContentController: WKUserContentController) {
        guard let accessToken = proxyBridgeAccessToken else {
            return
        }

        let bootstrapScript = WKUserScript(
            source: ProxyBridgeSupport.bootstrapScriptSource(),
            injectionTime: .atDocumentStart,
            forMainFrameOnly: false
        )
        userContentController.addUserScript(bootstrapScript)

        let proxyRegexSource = legacyProxyRequestURLRegexPattern ?? ""
        guard let bridgeScriptSource = ProxyBridgeSupport.loadBundledBridgeScript(
            accessToken: accessToken,
            proxyRegexSource: proxyRegexSource
        ) else {
            print("[InAppBrowser][Proxy] WARNING: Failed to load proxy-bridge.js for iOS injection")
            return
        }

        let bridgeScript = WKUserScript(
            source: bridgeScriptSource,
            injectionTime: .atDocumentStart,
            forMainFrameOnly: false
        )
        userContentController.addUserScript(bridgeScript)
    }

    func injectJavaScriptInterface() {
        let script = mobileAppScriptSource()
        DispatchQueue.main.async {
            self.webView?.evaluateJavaScript(script) { result, error in
                if let error = error {
                    print("JavaScript evaluation error: \(error)")
                } else if let result = result {
                    print("JavaScript result: \(result)")
                } else {
                    print("JavaScript executed with no result")
                }
            }
        }
    }

    open func initWebview(isInspectable: Bool = false) {
        if self.isWebViewInitialized {
            return
        }
        self.isWebViewInitialized = true
        self.isInspectable = isInspectable
        self.startObservingKeyboardViewportChanges()
        self.view.backgroundColor = UIColor.white

        self.extendedLayoutIncludesOpaqueBars = true
        self.edgesForExtendedLayout = [.bottom]

        let webConfiguration = initialWebConfiguration ?? WKWebViewConfiguration()
        if let preferredContentMode {
            PreferredContentModeSupport.apply(to: webConfiguration, mode: preferredContentMode)
        }
        webConfiguration.websiteDataStore = BrowsingDataStoreSupport.websiteDataStore(
            persistWebViewData: persistWebViewData,
            useSharedDataStore: useSharedDataStore
        )
        let userContentController = webConfiguration.userContentController
        userContentController.removeAllUserScripts()
        ScriptMessageHandlerSupport.removeAll(from: userContentController)

        if proxyRequests || proxySchemeHandler != nil, let handler = proxySchemeHandler {
            WKWebView.enableCustomSchemeHandling(for: ["https", "http"])
            if !WKWebView.handlesURLScheme("https") && !WKWebView.handlesURLScheme("http") {
                webConfiguration.setURLSchemeHandler(handler, forURLScheme: "https")
                webConfiguration.setURLSchemeHandler(handler, forURLScheme: "http")
            } else {
                print("[InAppBrowser][Proxy] WARNING: handlesURLScheme swizzle failed; proxy scheme handler not registered")
            }
        }

        let bundledAssetHandler = BundledAssetSchemeHandler(expectedHost: bundledAssetLocalHost)
        self.bundledAssetSchemeHandler = bundledAssetHandler
        if webConfiguration.urlSchemeHandler(forURLScheme: bundledAssetLocalScheme) == nil {
            webConfiguration.setURLSchemeHandler(bundledAssetHandler, forURLScheme: bundledAssetLocalScheme)
        }

        if ProxyBridgeSupport.shouldInjectBridge(hasProxySchemeHandler: proxySchemeHandler != nil) {
            addProxyBridgeUserScripts(to: userContentController)
        }

        let weakHandler = WeakScriptMessageHandler(self)
        userContentController.add(weakHandler, name: "messageHandler")
        userContentController.add(weakHandler, name: "preShowScriptError")
        userContentController.add(weakHandler, name: "preShowScriptSuccess")
        userContentController.add(weakHandler, name: "close")
        userContentController.add(weakHandler, name: "hide")
        userContentController.add(weakHandler, name: "show")
        userContentController.add(weakHandler, name: "blobDownload")
        userContentController.add(weakHandler, name: "blobDownloadStart")
        userContentController.add(weakHandler, name: "blobDownloadChunk")
        userContentController.add(weakHandler, name: "blobDownloadFinish")
        userContentController.add(weakHandler, name: "blobDownloadAbort")
        if allowScreenshotsFromWebPage {
            userContentController.add(weakHandler, name: "takeScreenshot")
        }
        if captureConsoleLogs {
            userContentController.add(weakHandler, name: "consoleMessageHandler")
            addConsoleCaptureUserScript(to: userContentController)
        }
        if let preShowScript = self.preShowScript,
           !preShowScript.isEmpty,
           preShowScriptInjectionTime == "documentStart" {
            userContentController.addUserScript(
                WKUserScript(
                    source: preShowScript,
                    injectionTime: .atDocumentStart,
                    forMainFrameOnly: false
                )
            )
            print("[InAppBrowser] Injected preShowScript at document start")
        }
        for scriptSource in documentStartUserScripts where !scriptSource.isEmpty {
            userContentController.addUserScript(
                WKUserScript(
                    source: scriptSource,
                    injectionTime: .atDocumentStart,
                    forMainFrameOnly: false
                )
            )
        }
        if ProxyBridgeSupport.shouldInjectBridge(hasProxySchemeHandler: proxySchemeHandler != nil) {
            userContentController.add(weakHandler, name: "capgoProxyBridge")
        }
        userContentController.add(weakHandler, name: "magicPrint")

        // Inject JavaScript to override window.print
        let script = WKUserScript(
            source: """
            window.print = function() {
                window.webkit.messageHandlers.magicPrint.postMessage('magicPrint');
            };
            """,
            injectionTime: .atDocumentStart,
            forMainFrameOnly: false
        )
        userContentController.addUserScript(script)
        addMobileAppUserScript(to: userContentController)

        webConfiguration.allowsInlineMediaPlayback = true
        webConfiguration.userContentController = userContentController
        // Enable background task processing
        // WKProcessPool is shared automatically on iOS 15+; explicit assignment is deprecated.

        // Enable JavaScript to run automatically (needed for preShowScript and Firebase polyfill)
        webConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true

        // Enhanced configuration for Google Pay support (only when enabled)
        if enableGooglePaySupport {
            print("[InAppBrowser] Enabling Google Pay support features for iOS")

            // Inject Google Pay support script
            let googlePayScript = WKUserScript(
                source: """
                console.log('[InAppBrowser] Injecting Google Pay support for iOS');

                // Enhanced window.open for Google Pay
                (function() {
                    const originalWindowOpen = window.open;
                    window.open = function(url, target, features) {
                        console.log('[InAppBrowser iOS] Enhanced window.open called:', url, target, features);

                        // For Google Pay URLs, handle popup properly
                        if (url && (url.includes('google.com/pay') || url.includes('accounts.google.com'))) {
                            console.log('[InAppBrowser iOS] Google Pay popup detected');
                            return originalWindowOpen.call(window, url, target || '_blank', features);
                        }

                        return originalWindowOpen.call(window, url, target, features);
                    };

                    // Add Cross-Origin-Opener-Policy meta tag if not present
                    if (!document.querySelector('meta[http-equiv="Cross-Origin-Opener-Policy"]')) {
                        const meta = document.createElement('meta');
                        meta.setAttribute('http-equiv', 'Cross-Origin-Opener-Policy');
                        meta.setAttribute('content', 'same-origin-allow-popups');
                        if (document.head) {
                            document.head.appendChild(meta);
                            console.log('[InAppBrowser iOS] Added Cross-Origin-Opener-Policy meta tag');
                        }
                    }

                    console.log('[InAppBrowser iOS] Google Pay support enhancements complete');
                })();
                """,
                injectionTime: .atDocumentStart,
                forMainFrameOnly: false
            )
            userContentController.addUserScript(googlePayScript)
        }

        let webView = WKWebView(frame: .zero, configuration: webConfiguration)

        //        if webView.responds(to: Selector(("setInspectable:"))) {
        //            // Fix: https://stackoverflow.com/questions/76216183/how-to-debug-wkwebview-in-ios-16-4-1-using-xcode-14-2/76603043#76603043
        //            webView.perform(Selector(("setInspectable:")), with: isInspectable)
        //        }

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

        // First add the webView to view hierarchy
        self.view.addSubview(webView)

        // Then set up constraints
        webView.translatesAutoresizingMaskIntoConstraints = false
        var bottomAnchor = self.view.bottomAnchor
        var topAnchor = self.view.safeAreaLayoutGuide.topAnchor

        if self.enabledSafeBottomMargin {
            bottomAnchor = self.view.safeAreaLayoutGuide.bottomAnchor
        }

        if !self.enabledSafeTopMargin {
            topAnchor = self.view.topAnchor
        }

        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: topAnchor),
            webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            webView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])

        webView.uiDelegate = self
        webView.navigationDelegate = self

        webView.allowsBackForwardNavigationGestures = self.activeNativeNavigationForWebview
        webView.isMultipleTouchEnabled = true

        // Disable bounce effect by setting scrollView.bounces to false when disableOverscroll is true
        webView.scrollView.bounces = !self.disableOverscroll
        configureReloadGesture(for: webView)
        configureWebViewScrollViewSafeArea(webView)

        webView.addObserver(self, forKeyPath: estimatedProgressKeyPath, options: .new, context: nil)
        if websiteTitleInNavigationBar {
            webView.addObserver(self, forKeyPath: titleKeyPath, options: .new, context: nil)
        }
        webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: .new, context: nil)

        if !self.blankNavigationTab {
            self.view.addSubview(webView)
            // Then set up constraints
            webView.translatesAutoresizingMaskIntoConstraints = false
        }
        self.webView = webView

        self.webView?.customUserAgent = self.customUserAgent ?? self.userAgent ?? self.originalUserAgent

        self.navigationItem.title = self.navigationItem.title ?? self.source?.absoluteString

        if let navigation = self.navigationController {
            self.previousNavigationBarState = (navigation.navigationBar.tintColor, navigation.navigationBar.isHidden)
            self.previousToolbarState = (navigation.toolbar.tintColor, navigation.toolbar.isHidden)
        }

        if let sourceValue = self.source, !waitsForPopupNavigation {
            let dataStore = webConfiguration.websiteDataStore
            BrowsingDataStoreSupport.applyOpenTimeClearing(
                to: dataStore,
                clearCookies: clearCookiesOnOpen,
                clearCache: clearCacheOnOpen
            ) { [weak self] in
                guard let self else { return }
                self.load(source: sourceValue)
            }
        } else if self.source == nil {
            print("[\(type(of: self))][Error] Invalid url")
        }
    }

    open func setupViewElements() {
        self.setUpProgressView()
        self.setUpConstraints()
        self.setUpNavigationBarAppearance()
        self.addBarButtonItems()
        self.updateBarButtonItems()
    }

    @discardableResult
    func inheritPopupPresentation(
        from parent: WKWebViewController,
        request: URLRequest,
        configuration: WKWebViewConfiguration,
        instanceId: String,
        proxySchemeHandler: ProxySchemeHandler?
    ) -> WKWebView? {
        self.instanceId = instanceId
        self.source = request.url.map { .remote($0) }
        self.url = request.url
        self.credentials = parent.credentials
        self.headers = parent.headers
        self.customUserAgent = parent.customUserAgent
        self.userAgent = parent.userAgent
        self.pureUserAgent = parent.pureUserAgent
        self.capBrowserPlugin = parent.capBrowserPlugin
        self.shareDisclaimer = parent.shareDisclaimer
        self.shareSubject = parent.shareSubject
        self.closeModal = parent.closeModal
        self.closeModalTitle = parent.closeModalTitle
        self.closeModalDescription = parent.closeModalDescription
        self.closeModalOk = parent.closeModalOk
        self.closeModalCancel = parent.closeModalCancel
        self.closeModalURLPattern = parent.closeModalURLPattern
        self.ignoreUntrustedSSLError = parent.ignoreUntrustedSSLError
        self.enableGooglePaySupport = parent.enableGooglePaySupport
        self.preventDeeplink = parent.preventDeeplink
        self.closeAction = parent.closeAction
        self.screenshotOnHide = parent.screenshotOnHide
        self.titleFontFamily = parent.titleFontFamily
        self.titleIcon = parent.titleIcon
        self.openBlankTargetInWebView = parent.openBlankTargetInWebView
        self.blankNavigationTab = false
        self.enabledSafeBottomMargin = parent.enabledSafeBottomMargin
        self.enabledSafeTopMargin = parent.enabledSafeTopMargin
        self.blockedHosts = parent.blockedHosts
        self.authorizedAppLinks = parent.authorizedAppLinks
        // createWebViewWith configs copy parent script handlers; start clean so
        // initWebview can register blobDownload/etc without NSInvalidArgumentException.
        configuration.userContentController = WKUserContentController()
        self.initialWebConfiguration = configuration
        self.persistWebViewData = parent.persistWebViewData
        self.useSharedDataStore = parent.useSharedDataStore
        self.clearCookiesOnOpen = parent.clearCookiesOnOpen
        self.clearCacheOnOpen = parent.clearCacheOnOpen
        self.enableReloadGesture = parent.enableReloadGesture
        self.disableOverscroll = parent.disableOverscroll
        self.proxyRequests = parent.proxyRequests
        self.proxySchemeHandler = proxySchemeHandler
        self.bundledAssetLocalScheme = parent.bundledAssetLocalScheme
        self.bundledAssetLocalHost = parent.bundledAssetLocalHost
        self.proxyBridge = parent.proxyBridge
        self.proxyBridgeAccessToken = parent.proxyBridgeAccessToken
        self.legacyProxyRequestURLRegexPattern = parent.legacyProxyRequestURLRegexPattern
        self.waitsForPopupNavigation = true
        self.hiddenPopupWindow = parent.hiddenPopupWindow
        self.opensHidden = parent.hiddenPopupWindow
        self.captureConsoleLogs = parent.captureConsoleLogs
        self.allowWebViewJsVisibilityControl = parent.allowWebViewJsVisibilityControl
        self.allowScreenshotsFromWebPage = parent.allowScreenshotsFromWebPage
        self.preferredContentMode = parent.preferredContentMode
        self.handleDownloads = parent.handleDownloads
        self.websiteTitleInNavigationBar = parent.websiteTitleInNavigationBar
        self.doneBarButtonItemPosition = parent.doneBarButtonItemPosition
        self.showArrowAsClose = parent.showArrowAsClose
        self.documentStartUserScripts = parent.documentStartUserScripts
        self.preShowScript = parent.preShowScript
        self.preShowScriptInjectionTime = parent.preShowScriptInjectionTime
        self.leftNavigationBarItemTypes = parent.leftNavigationBarItemTypes
        self.rightNavigaionBarItemTypes = parent.rightNavigaionBarItemTypes
        self.statusBarStyle = parent.statusBarStyle
        self.tintColor = parent.tintColor
        self.backBarButtonItemImage = parent.backBarButtonItemImage
        self.forwardBarButtonItemImage = parent.forwardBarButtonItemImage
        self.reloadBarButtonItemImage = parent.reloadBarButtonItemImage
        self.stopBarButtonItemImage = parent.stopBarButtonItemImage
        self.activityBarButtonItemImage = parent.activityBarButtonItemImage
        self.buttonNearDoneIcon = parent.buttonNearDoneIcon
        self.showScreenshotButton = parent.showScreenshotButton
        self.textZoom = parent.textZoom
        self.customWidth = parent.customWidth
        self.customHeight = parent.customHeight
        self.customX = parent.customX
        self.customY = parent.customY
        self.view.backgroundColor = parent.view.backgroundColor
        self.title = parent.title ?? request.url?.host ?? "Popup Window"
        self.navigationItem.title = self.title
        self.initWebview(isInspectable: parent.isInspectable)
        return self.capableWebView
    }

    private func applyNavigationVisibility() {
        navigationController?.setNavigationBarHidden(blankNavigationTab, animated: false)
        // Always hide toolbar since we never want it.
        navigationController?.setToolbarHidden(true, animated: false)
    }

    override open func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if !self.viewWasPresented {
            self.setupViewElements()
            setUpState()
            self.viewWasPresented = true

            // Apply custom dimensions if specified
            applyCustomDimensions()
        }

        // Reapply presentation state after hide/show re-presents the same controller.
        applyNavigationVisibility()

        // Force update button appearances
        updateButtonTintColors()

        // Ensure status bar appearance is correct when view appears
        // Make sure we have the latest tint color
        if self.tintColor != nil {
            // Update the status bar background if needed
            if let navController = navigationController, let backgroundColor = navController.navigationBar.backgroundColor ?? statusBarBackgroundView?.backgroundColor {
                setupStatusBarBackground(color: backgroundColor)
            } else {
                setupStatusBarBackground(color: UIColor.white)
            }
        }

        // Update status bar style
        updateStatusBarStyle()

        // Special handling for blank toolbar mode
        if blankNavigationTab && statusBarBackgroundView != nil {
            if let color = statusBarBackgroundView?.backgroundColor {
                // Set view color to match status bar
                view.backgroundColor = color
            }
        }
    }

    override open func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        refreshWebViewViewportIfNeeded(force: true)
        scheduleWebViewViewportRefresh(force: true, delay: 0.25)

        // Force add buttonNearDone if it's not visible yet
        if buttonNearDoneIcon != nil {
            let doneOnLeft = doneBarButtonItemPosition == .left
            let existingItems = doneOnLeft ? navigationItem.leftBarButtonItems : navigationItem.rightBarButtonItems
            let buttonExists = buttonNearDoneAlreadyExists(in: existingItems)

            if !buttonExists {
                let buttonItem = makeButtonNearDoneBarButtonItem()
                if doneOnLeft {
                    if navigationItem.leftBarButtonItems == nil {
                        navigationItem.leftBarButtonItems = [buttonItem]
                    } else {
                        var items = navigationItem.leftBarButtonItems ?? []
                        items.append(buttonItem)
                        navigationItem.leftBarButtonItems = items
                    }
                } else {
                    if navigationItem.rightBarButtonItems == nil {
                        navigationItem.rightBarButtonItems = [buttonItem]
                    } else {
                        var items = navigationItem.rightBarButtonItems ?? []
                        items.append(buttonItem)
                        navigationItem.rightBarButtonItems = items
                    }
                }

                print("[DEBUG] Force added buttonNearDone in viewDidAppear")
            }
        }
    }

    override open func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        rollbackState()
    }

    override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        switch keyPath {
        case estimatedProgressKeyPath?:
            DispatchQueue.main.async {
                guard let estimatedProgress = self.webView?.estimatedProgress else {
                    return
                }
                self.progressView?.alpha = 1
                self.progressView?.setProgress(Float(estimatedProgress), animated: true)
                self.emit("browserPageLoadProgress", data: ["progress": estimatedProgress])

                if estimatedProgress >= 1.0 {
                    UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseOut, animations: {
                        self.progressView?.alpha = 0
                    }, completion: {
                        _ in
                        self.progressView?.setProgress(0, animated: false)
                    })
                }
            }
        case titleKeyPath?:
            if self.hasDynamicTitle {
                self.navigationItem.title = webView?.url?.host
                self.updateTitleAppearance()
            }
        case "URL":
            // Guard against notifications during cleanup when webView is being torn down
            guard self.webView != nil else { return }
            emit("urlChangeEvent", data: ["url": webView?.url?.absoluteString ?? ""])
            DispatchQueue.main.async { [weak self] in
                self?.updateBarButtonItems()
            }
            self.injectJavaScriptInterface()
        default:
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }
}

// MARK: - Public Methods
public extension WKWebViewController {

    func goBack() -> Bool {
        if webView?.canGoBack ?? false {
            webView?.goBack()
            return true
        }
        return false
    }

    func load(source sourceValue: WKWebSource) {
        switch sourceValue {
        case .remote(let url):
            self.load(remote: url)
        case .file(let url, access: let access):
            self.load(file: url, access: access)
        case .string(let str, base: let base):
            self.load(string: str, base: base)
        }
    }

    func load(remote: URL) {
        DispatchQueue.main.async {
            self.webView?.load(self.createRequest(url: remote))
        }
    }

    func load(file: URL, access: URL) {
        webView?.loadFileURL(file, allowingReadAccessTo: access)
    }

    func load(string: String, base: URL? = nil) {
        webView?.loadHTMLString(string, baseURL: base)
    }

    func goBackToFirstPage() {
        if let firstPageItem = webView?.backForwardList.backList.first {
            webView?.go(to: firstPageItem)
        }
    }
    func reload() {
        webView?.reload()
    }

    func websiteDataStore() -> WKWebsiteDataStore? {
        return webView?.configuration.websiteDataStore
    }

    func executeScript(script: String, completion: ((Any?, Error?) -> Void)? = nil) {
        DispatchQueue.main.async { [weak self] in
            self?.webView?.evaluateJavaScript(script, completionHandler: completion)
        }
    }

    func applyTextZoom(_ zoomPercent: Int) {
        let script = """
        document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust = '\(zoomPercent)%';
        document.getElementsByTagName('body')[0].style.textSizeAdjust = '\(zoomPercent)%';
        """

        executeScript(script: script)
    }

    func injectPreShowScriptAtDocumentStart() {
        guard let preShowScript = self.preShowScript,
              !preShowScript.isEmpty,
              self.preShowScriptInjectionTime == "documentStart",
              let webView = self.webView else {
            return
        }

        let userScript = WKUserScript(
            source: preShowScript,
            injectionTime: .atDocumentStart,
            forMainFrameOnly: false
        )
        webView.configuration.userContentController.addUserScript(userScript)
        print("[InAppBrowser] Injected preShowScript at document start")

        // Reload the webview so the script executes at document start
        if let currentURL = webView.url {
            load(remote: currentURL)
        } else if let source = self.source {
            load(source: source)
        }
    }

    func updateNavigationGestures() {
        self.webView?.allowsBackForwardNavigationGestures = self.activeNativeNavigationForWebview
    }

    func cleanupWebView() {
        guard let webView = self.webView else { return }
        webView.stopLoading()
        previewItemURL = nil

        if reloadPanObserverInstalled {
            webView.scrollView.panGestureRecognizer.removeTarget(self, action: #selector(handleReloadPanGesture(_:)))
            reloadPanObserverInstalled = false
        }
        pendingReloadFromGesture = false
        reloadFromGestureInProgress = false
        reloadGestureNavigation = nil
        reloadGestureArmedPullDistance = 0

        // Remove KVO observers FIRST, before any operation that could trigger them
        webView.removeObserver(self, forKeyPath: estimatedProgressKeyPath)
        if websiteTitleInNavigationBar {
            webView.removeObserver(self, forKeyPath: titleKeyPath)
        }
        webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))

        // Now safe to tear down
        webView.navigationDelegate = nil
        webView.uiDelegate = nil
        webView.loadHTMLString("", baseURL: nil)

        webView.configuration.userContentController.removeAllUserScripts()
        ScriptMessageHandlerSupport.removeAll(from: webView.configuration.userContentController)

        webView.removeFromSuperview()
        // Also clean progress bar view if present
        progressView?.removeFromSuperview()
        progressView = nil
        self.webView = nil
    }
}

// MARK: - Fileprivate Methods
fileprivate extension WKWebViewController {
    var availableCookies: [HTTPCookie]? {
        return cookies?.filter {
            cookie in
            var result = true
            let url = self.source?.remoteURL
            if let host = url?.host, !cookie.domain.hasSuffix(host) {
                result = false
            }
            if cookie.isSecure && url?.scheme != "https" {
                result = false
            }

            return result
        }
    }
    func createRequest(url: URL) -> URLRequest {
        var request = URLRequest(url: url)

        // Set up HTTP method if specified
        if let method = httpMethod {
            request.httpMethod = method.uppercased()
        }

        // Set up HTTP body if specified
        if let body = httpBody, let data = body.data(using: .utf8) {
            request.httpBody = data
        }

        // Set up headers
        if let headers = headers {
            for (field, value) in headers {
                request.addValue(value, forHTTPHeaderField: field)
            }
        }

        // Set up Cookies
        if let cookies = availableCookies, let value = HTTPCookie.requestHeaderFields(with: cookies)[cookieKey] {
            request.addValue(value, forHTTPHeaderField: cookieKey)
        }

        return request
    }

    func setUpProgressView() {
        let progressView = UIProgressView(progressViewStyle: .default)
        progressView.trackTintColor = UIColor(white: 1, alpha: 0)
        self.progressView = progressView
        //        updateProgressViewFrame()
    }

    func setUpConstraints() {
        if !(self.navigationController?.navigationBar.isHidden)! {
            self.progressView?.frame.origin.y = CGFloat((self.navigationController?.navigationBar.frame.height)!)
            self.navigationController?.navigationBar.addSubview(self.progressView!)
        }
    }

    func addBarButtonItems() {
        func barButtonItem(_ type: BarButtonItemType) -> UIBarButtonItem? {
            switch type {
            case .back:
                return backBarButtonItem
            case .forward:
                return forwardBarButtonItem
            case .reload:
                return reloadBarButtonItem
            case .stop:
                return stopBarButtonItem
            case .activity:
                return activityBarButtonItem
            case .done:
                return doneBarButtonItem
            case .flexibleSpace:
                return flexibleSpaceBarButtonItem
            case .custom(let icon, let title, let action):
                let item: BlockBarButtonItem
                if let icon = icon {
                    item = BlockBarButtonItem(image: icon, style: .plain, target: self, action: #selector(customDidClick(sender:)))
                } else {
                    item = BlockBarButtonItem(title: title, style: .plain, target: self, action: #selector(customDidClick(sender:)))
                }
                item.block = action
                return item
            }
        }

        switch doneBarButtonItemPosition {
        case .left:
            if !leftNavigationBarItemTypes.contains(where: { type in
                switch type {
                case .done:
                    return true
                default:
                    return false
                }
            }) {
                leftNavigationBarItemTypes.insert(.done, at: 0)
            }
        case .right:
            if !rightNavigaionBarItemTypes.contains(where: { type in
                switch type {
                case .done:
                    return true
                default:
                    return false
                }
            }) {
                rightNavigaionBarItemTypes.insert(.done, at: 0)
            }
        case .none:
            break
        }

        var leftBarButtons = leftNavigationBarItemTypes.map {
            barButtonItemType in
            if let barButtonItem = barButtonItem(barButtonItemType) {
                return barButtonItem
            }
            return UIBarButtonItem()
        }

        var rightBarButtons = rightNavigaionBarItemTypes.map {
            barButtonItemType in
            if let barButtonItem = barButtonItem(barButtonItemType) {
                return barButtonItem
            }
            return UIBarButtonItem()
        }

        appendButtonNearDoneIfNeeded(
            to: &leftBarButtons,
            doneExpectedOnThisSide: doneBarButtonItemPosition == .left
        )
        appendButtonNearDoneIfNeeded(
            to: &rightBarButtons,
            doneExpectedOnThisSide: doneBarButtonItemPosition == .right
        )

        navigationItem.leftBarButtonItems = leftBarButtons
        navigationItem.rightBarButtonItems = rightBarButtons

        // After all buttons are set up, apply tint color
        updateButtonTintColors()
    }

    func updateBarButtonItems() {
        // Update navigation buttons (completely separate from close button)
        backBarButtonItem.isEnabled = webView?.canGoBack ?? false
        forwardBarButtonItem.isEnabled = webView?.canGoForward ?? false

        let updateReloadBarButtonItem: (UIBarButtonItem, Bool) -> UIBarButtonItem = {
            [weak self] barButtonItem, isLoading in
            guard let self = self else { return barButtonItem }
            switch barButtonItem {
            case self.reloadBarButtonItem, self.stopBarButtonItem:
                return isLoading ? self.stopBarButtonItem : self.reloadBarButtonItem
            default:
                return barButtonItem
            }
        }

        let isLoading = webView?.isLoading ?? false
        navigationItem.leftBarButtonItems = navigationItem.leftBarButtonItems?.map {
            barButtonItem -> UIBarButtonItem in
            return updateReloadBarButtonItem(barButtonItem, isLoading)
        }

        navigationItem.rightBarButtonItems = navigationItem.rightBarButtonItems?.map {
            barButtonItem -> UIBarButtonItem in
            return updateReloadBarButtonItem(barButtonItem, isLoading)
        }
    }

    func setUpState() {
        applyNavigationVisibility()
        // Set tint colors but don't override specific colors
        if tintColor == nil {
            // Use system appearance if no specific tint color is set
            let isDarkMode = traitCollection.userInterfaceStyle == .dark
            let textColor = isDarkMode ? UIColor.white : UIColor.black

            navigationController?.navigationBar.tintColor = textColor
            progressView?.progressTintColor = textColor
        } else {
            progressView?.progressTintColor = tintColor
            navigationController?.navigationBar.tintColor = tintColor
        }
    }

    func rollbackState() {
        progressView?.progress = 0

        navigationController?.navigationBar.tintColor = previousNavigationBarState.tintColor

        navigationController?.setNavigationBarHidden(
            blankNavigationTab || previousNavigationBarState.hidden,
            animated: true
        )
    }

    func checkRequestCookies(_ request: URLRequest, cookies: [HTTPCookie]) -> Bool {
        if cookies.count <= 0 {
            return true
        }
        guard let headerFields = request.allHTTPHeaderFields, let cookieString = headerFields[cookieKey] else {
            return false
        }

        let requestCookies = cookieString.components(separatedBy: ";").map {
            $0.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=", maxSplits: 1).map(String.init)
        }

        var valid = false
        for cookie in cookies {
            valid = requestCookies.filter {
                $0[0] == cookie.name && $0[1] == cookie.value
            }.count > 0
            if !valid {
                break
            }
        }
        return valid
    }

    private func tryOpenCustomScheme(_ url: URL) -> Bool {
        let app = UIApplication.shared
        let shouldEmitCustomSchemeEvent = CustomSchemeInterceptSupport.shouldEmitInterceptEvent(for: url)
        let canOpen = app.canOpenURL(url)

        if CustomSchemeOpenSupport.shouldAttemptOpen(scheme: url.scheme, canOpenURL: canOpen) {
            app.open(url, options: [:], completionHandler: nil)
            if shouldEmitCustomSchemeEvent {
                emit("customSchemeIntercepted", data: ["url": url.absoluteString, "opened": true])
            }
            return true // external app opened -> cancel WebView load
        }

        // Cannot open scheme: notify and still block WebView (avoid rendering garbage / errors)
        if shouldEmitCustomSchemeEvent {
            emit("customSchemeIntercepted", data: ["url": url.absoluteString, "opened": false])
        }
        emit("pageLoadError")
        return true
    }

    /// Prefer Universal Links into the native app; if that fails, hand off to the system
    /// (App Store, Safari, etc.) before falling back to in-webview loading.
    private func openAuthorizedAppLink(_ url: URL, completion: @escaping (Bool) -> Void) {
        UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { universalLinkOpened in
            if universalLinkOpened {
                completion(true)
                return
            }

            UIApplication.shared.open(url, options: [:]) { systemOpenSucceeded in
                let outcome = AuthorizedAppLinkOpenSupport.resolve(
                    universalLinkOpened: false,
                    systemOpenSucceeded: systemOpenSucceeded
                )
                completion(outcome == .openedExternally)
            }
        }
    }

    func openURLWithApp(_ url: URL) -> Bool {
        let application = UIApplication.shared
        if application.canOpenURL(url) {
            application.open(url, options: [:], completionHandler: nil)
            return true
        }

        return false
    }

    private func normalizeHost(_ host: String?) -> String? {
        guard var hostValue = host?.lowercased() else { return nil }
        if hostValue.hasPrefix("www.") { hostValue.removeFirst(4) }
        return hostValue
    }

    func isUrlAuthorized(_ url: URL, authorizedLinks: [String]) -> Bool {
        guard !authorizedLinks.isEmpty else { return false }

        let urlHostNorm = normalizeHost(url.host)
        for auth in authorizedLinks {
            guard let comp = URLComponents(string: auth) else { continue }
            let authHostNorm = normalizeHost(comp.host)
            if urlHostNorm == authHostNorm {
                return true
            }
        }

        return false
    }

    private func isHttpOrHttps(_ url: URL) -> Bool {
        guard let scheme = url.scheme?.lowercased() else {
            return false
        }
        return scheme == "http" || scheme == "https"
    }

    private func shouldLoadBlankTargetInCurrentWebView(_ url: URL, targetFrame: WKFrameInfo?) -> Bool {
        guard targetFrame == nil else {
            return false
        }
        guard isHttpOrHttps(url) else {
            return false
        }

        // Preserve native handling for authorized App/Universal Links unless deeplinks are blocked
        if isUrlAuthorized(url, authorizedLinks: authorizedAppLinks) && !preventDeeplink {
            return false
        }

        return openBlankTargetInWebView || preventDeeplink
    }

    /// Attempts to open URL in an external app if it's a custom scheme OR an authorized universal link.
    /// Returns via completion whether an external app was opened (true) or not (false).
    private func handleURLWithApp(_ url: URL, targetFrame _: WKFrameInfo?, completion: @escaping (Bool) -> Void) {

        // If preventDeeplink is true, don't try to open URLs in external apps
        if preventDeeplink {
            print("[InAppBrowser] preventDeeplink is true, won't try to open URLs in external apps")
            completion(false)
            return
        }

        let scheme = url.scheme?.lowercased() ?? ""
        let host = url.host?.lowercased() ?? ""

        print("[InAppBrowser] scheme \(scheme), host \(host)")

        // Don't try to open internal WebKit URLs externally (about:, data:, blob:, etc.)
        let internalSchemes = ["about", "data", "blob", "javascript"]
        if internalSchemes.contains(scheme) {
            print("[InAppBrowser] internal WebKit scheme detected, allowing navigation")
            completion(false)
            return
        }

        let bundledLocalConfig = BundledAssetSupport.LocalConfig(
            scheme: bundledAssetLocalScheme,
            host: bundledAssetLocalHost
        )
        if BundledAssetSupport.isBundledLocalURL(url.absoluteString, localConfig: bundledLocalConfig) {
            print("[InAppBrowser] bundled local asset URL detected, allowing navigation")
            completion(false)
            return
        }

        // Handle all non-http(s) schemes by default
        if scheme != "http" && scheme != "https" && scheme != "file" {
            print("[InAppBrowser] not http(s) scheme, try to open URLs in external apps")
            completion(tryOpenCustomScheme(url))
            return
        }

        // Also handle specific hosts and schemes from UrlsHandledByApp
        let hosts = UrlsHandledByApp.hosts
        let schemes = UrlsHandledByApp.schemes

        if hosts.contains(host) {
            print("[InAppBrowser] host \(host) matches one in UrlsHandledByApp, try to open URLs in external apps")
            completion(tryOpenCustomScheme(url))
            return
        }
        if schemes.contains(scheme) {
            print("[InAppBrowser] scheme \(scheme) matches one in UrlsHandledByApp, try to open URLs in external apps")
            completion(tryOpenCustomScheme(url))
            return
        }

        // Authorized App Link hosts: Universal Link first, then system open.
        print("[InAppBrowser] Authorized App Links: \(self.authorizedAppLinks)")
        if isUrlAuthorized(url, authorizedLinks: self.authorizedAppLinks) {
            print("[InAppBrowser] Authorized App Link detected \(scheme + host), try to open URLs in external apps")
            openAuthorizedAppLink(url) { opened in
                print("[InAppBrowser] Authorized App Link opened externally: \(opened)")
                completion(opened) // opened => cancel navigation; not opened => allow WebView
            }
            return
        }

        // Default: let WebView load
        print("[InAppBrowser] default completion handler: false")
        completion(false)
    }

    @objc func backDidClick(sender: AnyObject) {
        // Only handle back navigation, not closing
        if webView?.canGoBack ?? false {
            webView?.goBack()
        }
    }

    // Public method for safe back navigation
    @objc func forwardDidClick(sender: AnyObject) {
        webView?.goForward()
    }

    func makeButtonNearDoneBarButtonItem(tintColor: UIColor? = nil) -> UIBarButtonItem {
        let buttonItem = UIBarButtonItem(
            image: buttonNearDoneIcon?.withRenderingMode(.alwaysTemplate),
            style: .plain,
            target: self,
            action: #selector(buttonNearDoneDidClick)
        )
        if let resolvedTint = tintColor ?? self.tintColor ?? self.navigationController?.navigationBar.tintColor {
            buttonItem.tintColor = resolvedTint
        }
        return buttonItem
    }

    func buttonNearDoneAlreadyExists(in barButtons: [UIBarButtonItem]?) -> Bool {
        barButtons?.contains { $0.action == #selector(buttonNearDoneDidClick) } ?? false
    }

    func appendButtonNearDoneIfNeeded(to barButtons: inout [UIBarButtonItem], doneExpectedOnThisSide: Bool) {
        guard buttonNearDoneIcon != nil else { return }

        let shouldAdd = (barButtons.count == 1 && barButtons[0] == doneBarButtonItem) ||
            (barButtons.isEmpty && doneExpectedOnThisSide) ||
            barButtons.contains(doneBarButtonItem)
        guard shouldAdd else { return }
        guard !buttonNearDoneAlreadyExists(in: barButtons) else { return }

        if barButtons.isEmpty && doneExpectedOnThisSide {
            barButtons.append(doneBarButtonItem)
        }

        barButtons.append(makeButtonNearDoneBarButtonItem())
        let side = doneExpectedOnThisSide ? "expected" : "other"
        print("[DEBUG] Added buttonNearDone to \(side) bar buttons, icon: \(String(describing: buttonNearDoneIcon))")
    }

    @objc func buttonNearDoneDidClick(sender: AnyObject) {
        if showScreenshotButton {
            takeScreenshot { result in
                if case .failure(let error) = result {
                    print("[InAppBrowser] Failed to capture screenshot from toolbar button: \(error.localizedDescription)")
                }
            }
            return
        }
        emit("buttonNearDoneClick")
    }

    @objc func reloadDidClick(sender: AnyObject) {
        webView?.stopLoading()
        if webView?.url != nil {
            webView?.reload()
        } else if let sourceValue = self.source {
            self.load(source: sourceValue)
        }
    }

    @objc func stopDidClick(sender: AnyObject) {
        webView?.stopLoading()
    }

    @objc func activityDidClick(sender: AnyObject) {
        print("[DEBUG] Activity button clicked, shareSubject: \(self.shareSubject ?? "nil")")

        guard let sourceValue = self.source else {
            print("[DEBUG] Activity button: No source available")
            return
        }

        let items: [Any]
        switch sourceValue {
        case .remote(let urlValue):
            items = [urlValue]
        case .file(let urlValue, access: _):
            items = [urlValue]
        case .string(let str, base: _):
            items = [str]
        }
        showDisclaimer(items: items, sender: sender)
    }

    func showDisclaimer(items: [Any], sender: AnyObject) {
        // Show disclaimer dialog before sharing if shareDisclaimer is set
        if let disclaimer = self.shareDisclaimer, !disclaimer.isEmpty {
            // Create and show the alert
            let alert = UIAlertController(
                title: disclaimer["title"] as? String ?? "Title",
                message: disclaimer["message"] as? String ?? "Message",
                preferredStyle: UIAlertController.Style.alert)
            let currentUrl = self.webView?.url?.absoluteString ?? ""

            // Add confirm button that continues with sharing
            alert.addAction(UIAlertAction(
                title: disclaimer["confirmBtn"] as? String ?? "Confirm",
                style: UIAlertAction.Style.default,
                handler: { _ in
                    // Notify that confirm was clicked
                    self.emit("confirmBtnClicked", data: ["url": currentUrl])

                    // Show the share dialog
                    self.showShareSheet(items: items, sender: sender)
                }
            ))

            // Add cancel button
            alert.addAction(UIAlertAction(
                title: disclaimer["cancelBtn"] as? String ?? "Cancel",
                style: UIAlertAction.Style.cancel,
                handler: nil
            ))

            // Present the alert
            self.present(alert, animated: true, completion: nil)
        } else {
            // No disclaimer, directly show share sheet
            showShareSheet(items: items, sender: sender)
        }
    }

    // Separated the actual sharing functionality
    private func showShareSheet(items: [Any], sender: AnyObject) {
        let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
        activityViewController.setValue(self.shareSubject ?? self.title, forKey: "subject")
        if let barButtonItem = sender as? UIBarButtonItem {
            activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
        }
        self.present(activityViewController, animated: true, completion: nil)
    }

    func closeView() {
        var canDismiss = true
        if let url = self.source?.url {
            canDismiss = delegate?.webViewController?(self, canDismiss: url) ?? true
        }
        if canDismiss {
            let currentUrl = webView?.url?.absoluteString ?? ""
            if closeAction == "hide" {
                if toolbarHideInProgress {
                    return
                }
                toolbarHideInProgress = true
                if screenshotOnHide {
                    takeScreenshot(emitEvent: false) { result in
                        switch result {
                        case .success(let screenshot):
                            self.capBrowserPlugin?.handleWebViewDidHide(id: self.instanceId, url: currentUrl, screenshot: screenshot)
                        case .failure(let error):
                            print("[InAppBrowser] Failed to capture screenshot before hiding: \(error.localizedDescription)")
                            self.capBrowserPlugin?.handleWebViewDidHide(id: self.instanceId, url: currentUrl)
                        }
                    }
                } else {
                    self.capBrowserPlugin?.handleWebViewDidHide(id: instanceId, url: currentUrl)
                }
                return
            }
            cleanupWebView()
            self.capBrowserPlugin?.handleWebViewDidClose(id: instanceId, url: currentUrl)
            dismiss(animated: true, completion: nil)
        }
    }

    @objc func doneDidClick(sender: AnyObject) {
        // check if closeModal is true, if true display alert before close
        if self.closeModal {
            let currentUrl = webView?.url?.absoluteString ?? ""
            var shouldShowModal = true
            if let regex = self.closeModalURLRegex {
                let matched = regex.firstMatch(in: currentUrl, range: NSRange(currentUrl.startIndex..., in: currentUrl)) != nil
                shouldShowModal = matched
            }
            if shouldShowModal {
                let alert = UIAlertController(title: self.closeModalTitle, message: self.closeModalDescription, preferredStyle: UIAlertController.Style.alert)
                alert.addAction(UIAlertAction(title: self.closeModalOk, style: UIAlertAction.Style.default, handler: { _ in
                    // Notify that confirm was clicked
                    self.emit("confirmBtnClicked", data: ["url": currentUrl])
                    self.closeView()
                }))
                alert.addAction(UIAlertAction(title: self.closeModalCancel, style: UIAlertAction.Style.default, handler: nil))
                self.present(alert, animated: true, completion: nil)
            } else {
                self.closeView()
            }
        } else {
            self.closeView()
        }

    }

    @objc func customDidClick(sender: BlockBarButtonItem) {
        sender.block?(self)
    }

    func canRotate() {}

    func close() {
        let currentUrl = webView?.url?.absoluteString ?? ""
        cleanupWebView()
        capBrowserPlugin?.handleWebViewDidClose(id: instanceId, url: currentUrl)
        dismiss(animated: true, completion: nil)
    }

    func setUpNavigationBarAppearance() {
        // Set up basic bar appearance
        if let navBar = navigationController?.navigationBar {
            // Make navigation bar transparent
            navBar.setBackgroundImage(UIImage(), for: .default)
            navBar.shadowImage = UIImage()
            navBar.isTranslucent = true

            // Ensure tint colors are applied properly
            if navBar.tintColor == nil {
                navBar.tintColor = tintColor ?? .black
            }

            // Ensure text colors are set
            if navBar.titleTextAttributes == nil {
                navBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor ?? .black]
            }

            // Ensure the navigation bar buttons are properly visible
            for item in navBar.items ?? [] {
                for barButton in (item.leftBarButtonItems ?? []) + (item.rightBarButtonItems ?? []) {
                    barButton.tintColor = tintColor ?? navBar.tintColor ?? .black
                }
            }
        }

        // Force button colors to update
        updateButtonTintColors()
    }
}

// MARK: - WKUIDelegate
extension WKWebViewController: WKUIDelegate {
    public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        // Create a strong reference to the completion handler to ensure it's called
        let strongCompletionHandler = completionHandler

        // Ensure UI updates are on the main thread
        DispatchQueue.main.async { [weak self] in
            guard let self = self else {
                // View controller was deallocated
                strongCompletionHandler()
                return
            }

            // Check if view is available and ready for presentation
            guard self.view.window != nil, !self.isBeingDismissed, !self.isMovingFromParent else {
                print("[InAppBrowser] Cannot present alert - view not in window hierarchy or being dismissed")
                strongCompletionHandler()
                return
            }

            let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
                strongCompletionHandler()
            }))

            self.present(alertController, animated: true, completion: nil)
        }
    }

    public func webView(_: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame _: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else {
                completionHandler(false)
                return
            }

            guard self.view.window != nil, !self.isBeingDismissed, !self.isMovingFromParent else {
                print("[InAppBrowser] Cannot present confirm - view not in window hierarchy or being dismissed")
                completionHandler(false)
                return
            }

            guard self.presentedViewController == nil else {
                print("[InAppBrowser] Cannot present confirm - another controller is already presented")
                completionHandler(false)
                return
            }

            let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
            alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in
                completionHandler(false)
            }))
            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
                completionHandler(true)
            }))

            self.present(alertController, animated: true, completion: nil)
        }
    }

    public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
        guard let url = navigationAction.request.url else {
            return nil
        }

        print("[InAppBrowser] Handling popup/new window request for URL: \(url.absoluteString)")

        let isAuthorized = isUrlAuthorized(url, authorizedLinks: authorizedAppLinks)
        switch BlankTargetNavigationSupport.resolve(
            urlIsHttpOrHttps: isHttpOrHttps(url),
            openBlankTargetInWebView: openBlankTargetInWebView,
            preventDeeplink: preventDeeplink,
            isAuthorizedAppLink: isAuthorized
        ) {
        case .openExternalApp:
            // Prefer native app / Universal Link, then system open (App Store / Safari).
            openAuthorizedAppLink(url) { [weak webView] opened in
                if !opened {
                    DispatchQueue.main.async {
                        webView?.load(navigationAction.request)
                    }
                }
            }
            return nil
        case .loadInCurrentWebView:
            webView.load(navigationAction.request)
            return nil
        case .createPopup:
            break
        }

        return capBrowserPlugin?.createManagedPopupWebView(
            from: self,
            configuration: configuration,
            navigationAction: navigationAction
        )
    }

    public func webViewDidClose(_ webView: WKWebView) {
        if webView == self.webView {
            closeView()
        }
    }

    @available(iOS 15.0, *)
    public func webView(_ webView: WKWebView, requestGeolocationPermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        print("[InAppBrowser] Geolocation permission requested for origin: \(origin.host)")

        // Grant geolocation permission automatically for openWebView
        // This allows websites to access location when opened with openWebView
        decisionHandler(.grant)
    }

    @available(iOS 15.0, *)
    public func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        print("[InAppBrowser] Media capture permission requested for origin: \(origin.host)")

        // Grant media capture permission automatically, matching Capacitor core's
        // WebViewDelegationHandler. The OS-level camera/microphone permission
        // (Info.plist usage description + system prompt) still applies — this only
        // suppresses WebKit's extra per-origin prompt, which is not persisted
        // across app launches (WebKit bug 220416) and would otherwise re-prompt
        // users on every cold start.
        decisionHandler(.grant)
    }
}

// MARK: - Host Blocking Utilities
extension WKWebViewController {

    /// Checks if a host should be blocked based on the configured blocked hosts patterns
    /// - Parameter host: The host to check
    /// - Returns: true if the host should be blocked, false otherwise
    private func shouldBlockHost(_ host: String) -> Bool {
        guard !host.isEmpty else { return false }

        let normalizedHost = host.lowercased()

        return blockedHosts.contains { blockPattern in
            return matchesBlockPattern(host: normalizedHost, pattern: blockPattern.lowercased())
        }
    }

    /// Matches a host against a blocking pattern (supports wildcards)
    /// - Parameters:
    ///   - host: The normalized host to check
    ///   - pattern: The normalized blocking pattern
    /// - Returns: true if the host matches the pattern
    private func matchesBlockPattern(host: String, pattern: String) -> Bool {
        guard !pattern.isEmpty else { return false }

        // Exact match - fastest check first
        if host == pattern {
            return true
        }

        // No wildcards - already checked exact match above
        guard pattern.contains("*") else {
            return false
        }

        // Handle wildcard patterns
        if pattern.hasPrefix("*.") {
            return matchesWildcardDomain(host: host, pattern: pattern)
        } else if pattern.contains("*") {
            return matchesRegexPattern(host: host, pattern: pattern)
        }

        return false
    }

    /// Handles simple subdomain wildcard patterns like "*.example.com"
    /// - Parameters:
    ///   - host: The host to check
    ///   - pattern: The wildcard pattern starting with "*."
    /// - Returns: true if the host matches the wildcard domain
    private func matchesWildcardDomain(host: String, pattern: String) -> Bool {
        let domain = String(pattern.dropFirst(2))  // Remove "*."

        guard !domain.isEmpty else { return false }

        // Match exact domain or any subdomain
        return host == domain || host.hasSuffix("." + domain)
    }

    /// Handles complex regex patterns with multiple wildcards
    /// - Parameters:
    ///   - host: The host to check
    ///   - pattern: The pattern with wildcards to convert to regex
    /// - Returns: true if the host matches the regex pattern
    private func matchesRegexPattern(host: String, pattern: String) -> Bool {
        // Escape everything, then re-enable '*' as a wildcard
        let escaped = NSRegularExpression.escapedPattern(for: pattern)
        let wildcardEnabled = escaped.replacingOccurrences(of: "\\*", with: ".*")
        let regexPattern = "^\(wildcardEnabled)$"

        do {
            let regex = try NSRegularExpression(pattern: regexPattern, options: [])
            let range = NSRange(location: 0, length: host.utf16.count)
            return regex.firstMatch(in: host, options: [], range: range) != nil
        } catch {
            print("[InAppBrowser] Invalid regex pattern '\(regexPattern)': \(error)")
            return false
        }
    }
}

// MARK: - WKNavigationDelegate
extension WKWebViewController: WKNavigationDelegate {
    internal func injectPreShowScript() {
        if preShowSemaphore != nil {
            return
        }

        // Safely construct script template with proper escaping
        let userScript = self.preShowScript ?? ""

        // Build script using safe concatenation to avoid multi-line string issues
        let scriptTemplate = [
            "async function preShowFunction() {",
            userScript,
            "}",
            "preShowFunction().then(",
            "    () => window.webkit.messageHandlers.preShowScriptSuccess.postMessage({})",
            ").catch(",
            "    err => {",
            "        console.error('Preshow error', err);",
            "        window.webkit.messageHandlers.preShowScriptError.postMessage(JSON.stringify(err, Object.getOwnPropertyNames(err)));",
            "    }",
            ")"
        ]

        let script = scriptTemplate.joined(separator: "\n")
        print("[InAppBrowser - InjectPreShowScript] PreShowScript script: \(script)")

        self.preShowSemaphore = DispatchSemaphore(value: 0)
        self.executeScript(script: script) // this will run on the main thread

        defer {
            self.preShowSemaphore = nil
            self.preShowError = nil
        }

        if self.preShowSemaphore?.wait(timeout: .now() + 10) == .timedOut {
            print("[InAppBrowser - InjectPreShowScript] PreShowScript running for over 10 seconds. The plugin will not wait any longer!")
            return
        }

        //            "async function preShowFunction() {\n" +
        //            self.preShowScript + "\n" +
        //            "};\n" +
        //            "preShowFunction().then(() => window.PreShowScriptInterface.success()).catch(err => { console.error('Preshow error', err); window.PreShowScriptInterface.error(JSON.stringify(err, Object.getOwnPropertyNames(err))) })";

    }

    public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        updateBarButtonItems()
        self.progressView?.progress = 0
        if let urlValue = webView.url {
            self.url = urlValue
            delegate?.webViewController?(self, didStart: urlValue)
        }
        emit("browserPageLoadStart")
    }
    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        if !didpageInit && self.capBrowserPlugin?.isPresentAfterPageLoad == true {
            // Only inject preShowScript if it wasn't already injected at document start
            let shouldInjectScript = self.preShowScript.map { !$0.isEmpty } ?? false &&
                self.preShowScriptInjectionTime != "documentStart"

            if shouldInjectScript {
                if self.opensHidden {
                    DispatchQueue.global(qos: .userInitiated).async {
                        self.injectPreShowScript()
                    }
                } else {
                    // injectPreShowScript will block, don't execute on the main thread
                    DispatchQueue.global(qos: .userInitiated).async {
                        self.injectPreShowScript()
                        DispatchQueue.main.async { [weak self] in
                            self?.capBrowserPlugin?.presentView(webViewId: self?.instanceId)
                        }
                    }
                }
            } else if !self.opensHidden {
                self.capBrowserPlugin?.presentView(webViewId: instanceId)
            }
        } else if self.preShowScript != nil &&
                    !self.preShowScript!.isEmpty &&
                    self.capBrowserPlugin?.isPresentAfterPageLoad == true &&
                    self.preShowScriptInjectionTime != "documentStart" {
            // Only inject if not already injected at document start
            DispatchQueue.global(qos: .userInitiated).async {
                self.injectPreShowScript()
            }
        }

        // Apply text zoom if set
        if let zoom = self.textZoom {
            applyTextZoom(zoom)
        }

        didpageInit = true
        updateBarButtonItems()
        self.progressView?.progress = 0
        if let url = webView.url {
            self.url = url
            delegate?.webViewController?(self, didFinish: url)
        }
        self.injectJavaScriptInterface()
        // End refresh + clear sticky insets before safe-area sync zeroes contentInset.
        stopReloadGesture(for: navigation)
        lastInjectedSafeAreaInsets = nil
        syncWebViewSafeAreaLayout()
        emit("browserPageLoaded")
    }

    public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        updateBarButtonItems()
        self.progressView?.progress = 0
        if let url = webView.url {
            self.url = url
            delegate?.webViewController?(self, didFail: url, withError: error)
        }
        stopReloadGesture(for: navigation)
        emit("pageLoadError")
    }

    public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        updateBarButtonItems()
        self.progressView?.progress = 0
        if let url = webView.url {
            self.url = url
            delegate?.webViewController?(self, didFail: url, withError: error)
        }
        stopReloadGesture(for: navigation)
        emit("pageLoadError")
    }

    public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        if let credentials = credentials,
           challenge.protectionSpace.receivesCredentialSecurely,
           let url = webView.url, challenge.protectionSpace.host == url.host, challenge.protectionSpace.protocol == url.scheme, challenge.protectionSpace.port == url.port ?? (url.scheme == "https" ? 443 : url.scheme == "http" ? 80 : nil) {
            let urlCredential = URLCredential(user: credentials.username, password: credentials.password, persistence: .none)
            completionHandler(.useCredential, urlCredential)
        } else if let bypassedSSLHosts = bypassedSSLHosts, bypassedSSLHosts.contains(challenge.protectionSpace.host) {
            let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(.useCredential, credential)
        } else {
            guard self.ignoreUntrustedSSLError else {
                completionHandler(.performDefaultHandling, nil)
                return
            }
            /* allows to open links with self-signed certificates
             Follow Apple's guidelines https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge/performing_manual_server_trust_authentication
             */
            guard let serverTrust = challenge.protectionSpace.serverTrust  else {
                completionHandler(.useCredential, nil)
                return
            }
            let credential = URLCredential(trust: serverTrust)
            completionHandler(.useCredential, credential)
        }
        self.injectJavaScriptInterface()
    }

    public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if handleDownloads && navigationAction.shouldPerformDownload {
            decisionHandler(.download)
            return
        }

        var actionPolicy: WKNavigationActionPolicy = self.preventDeeplink ? .preventDeeplinkActionPolicy : .allow

        guard let url = navigationAction.request.url else {
            print("[InAppBrowser] Cannot determine URL from navigationAction")
            decisionHandler(actionPolicy)
            return
        }

        if handleDownloads, url.scheme?.lowercased() == "blob" {
            handleBlobDownloadFromPage(
                blobUrl: url.absoluteString,
                mimeType: navigationAction.request.value(forHTTPHeaderField: "Content-Type"),
                contentDisposition: navigationAction.request.value(forHTTPHeaderField: "Content-Disposition")
            )
            decisionHandler(.cancel)
            return
        }

        let appStoreHosts = ["apps.apple.com", "itunes.apple.com"]
        if !self.preventDeeplink,
           let appStoreHost = self.normalizeHost(url.host),
           appStoreHosts.contains(appStoreHost) {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
            decisionHandler(.cancel)
            return
        }

        if !self.allowsFileURL, url.isFileURL {
            print("[InAppBrowser] Cannot handle file URLs")
            decisionHandler(.cancel)
            return
        }

        // Defer the rest of the logic until the async external-app handling checks completes.
        handleURLWithApp(url, targetFrame: navigationAction.targetFrame) { [weak self] openedExternally in
            guard let self else {
                decisionHandler(.cancel)
                return
            }

            if openedExternally {
                decisionHandler(.cancel)
                return
            }

            if self.shouldLoadBlankTargetInCurrentWebView(url, targetFrame: navigationAction.targetFrame) {
                if let webView = self.webView {
                    webView.load(navigationAction.request)
                } else {
                    self.load(remote: url)
                }
                decisionHandler(.cancel)
                return
            }

            let host = url.host ?? ""

            if host == self.source?.url?.host,
               let cookies = self.availableCookies,
               !self.checkRequestCookies(navigationAction.request, cookies: cookies) {
                self.load(remote: url)
                decisionHandler(.cancel)
                return
            }

            if self.shouldBlockHost(host) {
                print("[InAppBrowser] Blocked host detected: \(host)")
                emit("urlChangeEvent", data: ["url": url.absoluteString])
                decisionHandler(.cancel)
                return
            }

            if let navigationType = NavigationType(rawValue: navigationAction.navigationType.rawValue),
               let result = self.delegate?.webViewController?(self, decidePolicy: url, navigationType: navigationType) {
                actionPolicy = result ? .allow : .cancel
            }

            self.injectJavaScriptInterface()
            decisionHandler(actionPolicy)
        }
    }

    public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
        if shouldInterceptDownload(for: navigationResponse) {
            decisionHandler(.download)
            return
        }

        decisionHandler(.allow)
    }

    public func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
        register(download: download, response: nil, sourceURL: navigationAction.request.url?.absoluteString)
        stopReloadGesture()
    }

    public func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
        register(download: download, response: navigationResponse.response)
        stopReloadGesture()
    }

    public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
        stopReloadGesture()
    }

    // MARK: - Dimension Management

    func applyCustomDimensions() {
        guard let navigationController = navigationController else {
            return
        }

        let hostView = navigationController.view.superview
        let screenSize = CustomWebViewFrameSupport.screenSize(for: hostView ?? navigationController.view)
        guard let screenFrame = CustomWebViewFrameSupport.resolvedFrame(
            width: customWidth,
            height: customHeight,
            x: customX,
            y: customY,
            fallbackSize: screenSize
        ) else {
            return
        }

        if let passThroughView = navigationController.view as? PassThroughView {
            // PassThroughView is full-screen; content stays in screen-space coordinates.
            passThroughView.targetFrame = screenFrame
            let contentView = passThroughView.framedContentView ?? passThroughView.subviews.first
            contentView?.frame = screenFrame
            contentView?.setNeedsLayout()
            contentView?.layoutIfNeeded()
            passThroughView.setNeedsLayout()
            passThroughView.layoutIfNeeded()
        } else if let hostView {
            let frameInHost = CustomWebViewFrameSupport.frameInHost(
                width: customWidth,
                height: customHeight,
                x: customX,
                y: customY,
                screenSize: screenSize,
                hostOriginInScreen: CustomWebViewFrameSupport.hostOriginInScreen(for: hostView)
            ) ?? screenFrame
            navigationController.view.frame = frameInHost
            navigationController.view.clipsToBounds = true
            navigationController.view.setNeedsLayout()
            navigationController.view.layoutIfNeeded()
        } else {
            navigationController.view.frame = screenFrame
            navigationController.view.setNeedsLayout()
            navigationController.view.layoutIfNeeded()
        }

        refreshWebViewViewportIfNeeded(force: true)
    }

    /// Update dimensions at runtime
    func updateDimensions(width: CGFloat?, height: CGFloat?, xPos: CGFloat?, yPos: CGFloat?) {
        // Update stored dimensions
        if let width = width {
            customWidth = width
        }
        if let height = height {
            customHeight = height
        }
        if let xPos = xPos {
            customX = xPos
        }
        if let yPos = yPos {
            customY = yPos
        }

        // Apply the new dimensions
        applyCustomDimensions()
    }

    func updateSafeTopMargin(_ enabled: Bool) {
        guard enabled != self.enabledSafeTopMargin else { return }
        self.enabledSafeTopMargin = enabled
        guard let webView = self.webView else { return }
        guard webView.superview === self.view else { return }

        // Find and deactivate the existing top constraint
        let existingTopConstraints = self.view.constraints.filter {
            ($0.firstItem as? WKWebView) == webView && $0.firstAttribute == .top
        }
        NSLayoutConstraint.deactivate(existingTopConstraints)

        // Create new top constraint based on enabled value
        let topAnchor = enabled ? self.view.safeAreaLayoutGuide.topAnchor : self.view.topAnchor
        NSLayoutConstraint.activate([
            webView.topAnchor.constraint(equalTo: topAnchor)
        ])
        self.view.layoutIfNeeded()
        syncWebViewSafeAreaLayout()
        refreshWebViewViewportIfNeeded(force: true)
    }

    func updateSafeBottomMargin(_ enabled: Bool) {
        guard enabled != self.enabledSafeBottomMargin else { return }
        self.enabledSafeBottomMargin = enabled
        guard let webView = self.webView else { return }
        guard webView.superview === self.view else { return }

        // Find and deactivate the existing bottom constraint
        let existingBottomConstraints = self.view.constraints.filter {
            ($0.firstItem as? WKWebView) == webView && $0.firstAttribute == .bottom
        }
        NSLayoutConstraint.deactivate(existingBottomConstraints)

        // Create new bottom constraint based on enabled value
        let bottomAnchor = enabled ? self.view.safeAreaLayoutGuide.bottomAnchor : self.view.bottomAnchor
        NSLayoutConstraint.activate([
            webView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        self.view.layoutIfNeeded()
        syncWebViewSafeAreaLayout()
        refreshWebViewViewportIfNeeded(force: true)
    }
}

extension WKWebViewController: WKDownloadDelegate {
    public func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
        let identifier = ObjectIdentifier(download)
        let sourceURL = downloadStates[identifier]?.sourceURL ?? response.url?.absoluteString
        do {
            let destinationURL = try uniqueDownloadDestination(for: suggestedFilename)
            downloadStates[identifier] = WKDownloadState(destinationURL: destinationURL, mimeType: response.mimeType, sourceURL: sourceURL)
            completionHandler(destinationURL)
        } catch {
            print("[InAppBrowser] Failed to prepare download destination: \(error)")
            downloadStates.removeValue(forKey: identifier)
            emitDownloadFailed(
                sourceURL: sourceURL,
                fileName: suggestedFilename,
                mimeType: response.mimeType,
                error: "Failed to prepare download destination: \(error.localizedDescription)"
            )
            completionHandler(nil)
        }
    }

    public func downloadDidFinish(_ download: WKDownload) {
        let identifier = ObjectIdentifier(download)
        guard let state = downloadStates.removeValue(forKey: identifier) else {
            return
        }
        releaseDownloadDestination(state.destinationURL)
        guard let destinationURL = state.destinationURL else {
            emitDownloadFailed(sourceURL: state.sourceURL, mimeType: state.mimeType, error: "Download finished without a destination URL")
            return
        }

        previewDownloadedFile(destinationURL, mimeType: state.mimeType, sourceURL: state.sourceURL)
    }

    public func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
        let state = downloadStates.removeValue(forKey: ObjectIdentifier(download))
        releaseDownloadDestination(state?.destinationURL)
        emitDownloadFailed(
            sourceURL: state?.sourceURL,
            fileName: state?.destinationURL?.lastPathComponent,
            mimeType: state?.mimeType,
            error: "Download failed: \(error.localizedDescription)"
        )
        print("[InAppBrowser] Download failed: \(error.localizedDescription)")
    }
}

extension WKWebViewController: QLPreviewControllerDataSource, QLPreviewControllerDelegate {
    public func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
        previewItemURL == nil ? 0 : 1
    }

    public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
        return previewItemURL! as NSURL
    }

    public func previewControllerDidDismiss(_ controller: QLPreviewController) {
        previewItemURL = nil
    }
}

class BlockBarButtonItem: UIBarButtonItem {

    var block: ((WKWebViewController) -> Void)?
}

/// Custom view that passes touches outside a target frame to the underlying view
class PassThroughView: UIView {
    var targetFrame: CGRect? {
        didSet {
            setNeedsLayout()
        }
    }
    weak var framedContentView: UIView?

    override func layoutSubviews() {
        super.layoutSubviews()

        guard let targetFrame, let framedContentView else {
            return
        }

        framedContentView.frame = targetFrame
    }

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Outside the browser frame: decline the hit. Callers that still use a full-screen
        // PassThroughView depend on the parent hierarchy continuing hit-testing; framed
        // front overlays avoid modal UITransitionView entirely instead.
        if let frame = targetFrame, !frame.contains(point) {
            return nil
        }

        return super.hitTest(point, with: event)
    }
}

extension WKNavigationActionPolicy {
    static let preventDeeplinkActionPolicy = WKNavigationActionPolicy(rawValue: WKNavigationActionPolicy.allow.rawValue + 2)!
}

class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
    weak var delegate: WKScriptMessageHandler?

    init(_ delegate: WKScriptMessageHandler) {
        self.delegate = delegate
        super.init()
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        self.delegate?.userContentController(userContentController, didReceive: message)
    }
}
