import CryptoKit
import Foundation
import OpenClawKit
import SwiftUI

#if canImport(WebKit) && (os(iOS) || os(macOS))
import Security
import WebKit

#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
import UniformTypeIdentifiers
#endif
#endif

enum OpenClawChatWidgetSurfaceRole: Sendable, Hashable {
    case node
    case operatorSurface
    case legacy
}

public struct OpenClawChatWidgetResource: Sendable, Equatable {
    public let url: URL
    public let tlsFingerprintSHA256: String?
    let surfaceRole: OpenClawChatWidgetSurfaceRole
    let attemptedSurfaceRoles: Set<OpenClawChatWidgetSurfaceRole>

    public init(url: URL, tlsFingerprintSHA256: String? = nil) {
        self.url = url
        self.tlsFingerprintSHA256 = tlsFingerprintSHA256
        self.surfaceRole = .legacy
        self.attemptedSurfaceRoles = []
    }

    init(
        url: URL,
        tlsFingerprintSHA256: String?,
        surfaceRole: OpenClawChatWidgetSurfaceRole,
        attemptedSurfaceRoles: Set<OpenClawChatWidgetSurfaceRole>)
    {
        self.url = url
        self.tlsFingerprintSHA256 = tlsFingerprintSHA256
        self.surfaceRole = surfaceRole
        self.attemptedSurfaceRoles = attemptedSurfaceRoles
    }

    public static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.url == rhs.url && lhs.tlsFingerprintSHA256 == rhs.tlsFingerprintSHA256
    }
}

public enum OpenClawChatWidgetURLResolver {
    private static let documentsPath = "/__openclaw__/canvas/documents"

    public static func resolve(surfaceURL rawSurfaceURL: String?, target rawTarget: String) -> URL? {
        guard let target = self.relativeWidgetTarget(rawTarget),
              var surface = self.capabilitySurface(rawSurfaceURL)
        else { return nil }

        var surfacePath = surface.percentEncodedPath
        while surfacePath.hasSuffix("/") {
            surfacePath.removeLast()
        }
        surface.percentEncodedPath = surfacePath + target.percentEncodedPath
        surface.percentEncodedQuery = target.percentEncodedQuery
        surface.fragment = target.fragment
        return surface.url
    }

    public static func supportsTarget(_ rawTarget: String) -> Bool {
        self.relativeWidgetTarget(rawTarget) != nil
    }

    public static func resolveResource(
        target: String,
        replacing failedResource: OpenClawChatWidgetResource?,
        currentSurfaceRoutes: @Sendable () async -> (
            node: GatewayCanvasHostRoute?,
            operatorSurface: GatewayCanvasHostRoute?),
        refreshNodeSurfaceRoute: @Sendable (GatewayCanvasHostRoute?) async -> GatewayCanvasHostRoute?,
        refreshOperatorSurfaceRoute: @Sendable (GatewayCanvasHostRoute?) async -> GatewayCanvasHostRoute?) async
        -> OpenClawChatWidgetResource?
    {
        let observed = await currentSurfaceRoutes()
        guard let failedResource else {
            return self.resolvePreferred(
                surfaces: observed,
                target: target,
                excluding: nil,
                blockedRoles: [],
                attemptedRoles: [])
        }
        let blockedRoles = failedResource.attemptedSurfaceRoles
        if failedResource.surfaceRole == .legacy,
           blockedRoles.contains(.legacy)
        {
            return nil
        }
        let attemptedRoles = blockedRoles.union([failedResource.surfaceRole])
        if !blockedRoles.contains(.node),
           let nodeSurface = observed.node,
           let currentNode = self.resolve(
               surface: nodeSurface,
               role: .node,
               target: target,
               attemptedRoles: attemptedRoles),
           self.isReplacement(currentNode, for: failedResource)
        {
            return currentNode
        }

        if !blockedRoles.contains(.node),
           let refreshedSurface = await refreshNodeSurfaceRoute(observed.node),
           let refreshed = resolve(
               surface: refreshedSurface,
               role: .node,
               target: target,
               attemptedRoles: attemptedRoles),
           self.isReplacement(refreshed, for: failedResource)
        {
            return refreshed
        }

        // A nil refresh can mean its route lease lost a reconnect race. Re-read
        // both roles so a replacement connection and its TLS pin win together.
        let afterNodeRefresh = await currentSurfaceRoutes()
        if let replacement = self.resolvePreferred(
            surfaces: afterNodeRefresh,
            target: target,
            excluding: failedResource,
            blockedRoles: blockedRoles,
            attemptedRoles: attemptedRoles)
        {
            return replacement
        }

        if !blockedRoles.contains(.operatorSurface),
           let refreshedSurface = await refreshOperatorSurfaceRoute(afterNodeRefresh.operatorSurface),
           let refreshed = resolve(
               surface: refreshedSurface,
               role: .operatorSurface,
               target: target,
               attemptedRoles: attemptedRoles),
           self.isReplacement(refreshed, for: failedResource)
        {
            return refreshed
        }

        return await self.resolvePreferred(
            surfaces: currentSurfaceRoutes(),
            target: target,
            excluding: failedResource,
            blockedRoles: blockedRoles,
            attemptedRoles: attemptedRoles)
    }

    private static func relativeWidgetTarget(_ rawTarget: String) -> URLComponents? {
        let target = rawTarget.trimmingCharacters(in: .whitespacesAndNewlines)
        guard target.hasPrefix("/"),
              let components = URLComponents(string: target),
              components.scheme == nil,
              components.host == nil,
              components.user == nil,
              components.password == nil,
              self.isCanonicalPath(components.percentEncodedPath),
              components.percentEncodedPath.hasPrefix("\(self.documentsPath)/")
        else { return nil }
        return components
    }

    private static func capabilitySurface(_ rawSurfaceURL: String?) -> URLComponents? {
        let raw = rawSurfaceURL?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        guard !raw.isEmpty,
              let components = URLComponents(string: raw),
              self.isWebURL(components),
              components.user == nil,
              components.password == nil,
              components.percentEncodedQuery == nil,
              components.fragment == nil
        else { return nil }

        let segments = components.percentEncodedPath.split(separator: "/", omittingEmptySubsequences: true)
        guard segments.count >= 3,
              segments[segments.count - 3] == "__openclaw__",
              segments[segments.count - 2] == "cap",
              let capability = String(segments[segments.count - 1]).removingPercentEncoding,
              !capability.isEmpty
        else { return nil }
        return components
    }

    private static func resolve(
        surface: GatewayCanvasHostRoute,
        role: OpenClawChatWidgetSurfaceRole,
        target: String,
        attemptedRoles: Set<OpenClawChatWidgetSurfaceRole>) -> OpenClawChatWidgetResource?
    {
        guard let url = resolve(surfaceURL: surface.url, target: target) else { return nil }
        let resource = OpenClawChatWidgetResource(
            url: url,
            tlsFingerprintSHA256: surface.tlsFingerprintSHA256,
            surfaceRole: role,
            attemptedSurfaceRoles: attemptedRoles)
        return resource.hasValidTLSBinding ? resource : nil
    }

    private static func resolvePreferred(
        surfaces: (node: GatewayCanvasHostRoute?, operatorSurface: GatewayCanvasHostRoute?),
        target: String,
        excluding failedResource: OpenClawChatWidgetResource?,
        blockedRoles: Set<OpenClawChatWidgetSurfaceRole>,
        attemptedRoles: Set<OpenClawChatWidgetSurfaceRole>) -> OpenClawChatWidgetResource?
    {
        [
            (role: OpenClawChatWidgetSurfaceRole.node, surface: surfaces.node),
            (role: OpenClawChatWidgetSurfaceRole.operatorSurface, surface: surfaces.operatorSurface),
        ]
            .lazy
            .filter { !blockedRoles.contains($0.role) }
            .compactMap { candidate in
                candidate.surface.flatMap {
                    self.resolve(
                        surface: $0,
                        role: candidate.role,
                        target: target,
                        attemptedRoles: attemptedRoles)
                }
            }
            .first { self.isReplacement($0, for: failedResource) }
    }

    private static func isReplacement(
        _ candidate: OpenClawChatWidgetResource,
        for failedResource: OpenClawChatWidgetResource?) -> Bool
    {
        guard let failedResource else { return true }
        // Legacy URL-only callers cannot express trust identity, so retain
        // their URL-only exclusion while resource-aware callers compare both.
        if failedResource.surfaceRole == .legacy,
           failedResource.tlsFingerprintSHA256 == nil
        {
            return candidate.url != failedResource.url
        }
        return candidate.url != failedResource.url ||
            candidate.tlsFingerprintSHA256 != failedResource.tlsFingerprintSHA256
    }

    private static func isWebURL(_ components: URLComponents) -> Bool {
        let scheme = components.scheme?.lowercased()
        return (scheme == "http" || scheme == "https") && components.host?.isEmpty == false
    }

    private static func isCanonicalPath(_ path: String) -> Bool {
        let segments = path.split(separator: "/", omittingEmptySubsequences: false)
        guard segments.first?.isEmpty == true else { return false }
        for (index, encodedSegment) in segments.enumerated() {
            if index == 0 || (index == segments.count - 1 && encodedSegment.isEmpty) {
                continue
            }
            guard !encodedSegment.isEmpty else { return false }
            guard let segment = self.decodeRepeatedly(String(encodedSegment)) else { return false }
            if segment == "." || segment == ".." || segment.contains("/") || segment.contains("\\") {
                return false
            }
        }
        return true
    }

    private static func decodeRepeatedly(_ encoded: String) -> String? {
        var value = encoded
        for _ in 0..<8 {
            guard let decoded = value.removingPercentEncoding else { return nil }
            if decoded == value { return decoded }
            value = decoded
        }
        return nil
    }
}

enum ChatInlineWidgetExport {
    static func filename(title: String?) -> String {
        var name = title ?? ""
        name.removeAll { character in
            character == "/" ||
                character == "\\" ||
                character.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains)
        }
        name = name.trimmingCharacters(in: .whitespacesAndNewlines)
        return "\(name.isEmpty ? "widget" : name).png"
    }
}

@MainActor
struct ChatInlineWidgetView: View {
    let preview: OpenClawChatCanvasPreview
    let resolverReady: Bool
    let resolveResource: @MainActor @Sendable (
        String,
        OpenClawChatWidgetResource?) async -> OpenClawChatWidgetResource?

    @State private var resolvedResource: OpenClawChatWidgetResource?
    @State private var recoveryAttempts = 0
    @State private var refreshInFlight = false
    @State private var unavailable = false
    @State private var activePath: String?
    /// A reset can keep the same path while installing a new connection route.
    /// Its generation prevents older resolver completions from restoring stale trust state.
    @State private var loadGeneration = UUID()

    #if canImport(WebKit) && (os(iOS) || os(macOS))
    @State private var snapshotRequest: ChatInlineWidgetSnapshotRequest?
    @State private var exportErrorMessage: String?

    #if os(iOS)
    @State private var sharedImage: ChatInlineWidgetSharedImage?
    #endif

    private var isPresentingExportError: Binding<Bool> {
        Binding(
            get: { self.exportErrorMessage != nil },
            set: { if !$0 { self.exportErrorMessage = nil } })
    }
    #endif

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            if let title = self.preview.title?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty {
                Text(title)
                    .font(OpenClawChatTypography.footnote)
                    .fontWeight(.semibold)
                    .foregroundStyle(OpenClawChatTheme.muted)
            }

            #if canImport(WebKit) && (os(iOS) || os(macOS))
            if let resolvedResource {
                self.renderedWidget(resource: resolvedResource)
            } else if self.unavailable {
                Text("Widget unavailable")
                    .font(OpenClawChatTypography.footnote)
                    .foregroundStyle(OpenClawChatTheme.muted)
            } else {
                ProgressView()
                    .controlSize(.small)
                    .frame(maxWidth: .infinity, minHeight: 44)
            }
            #else
            Text("Widget unavailable")
                .font(OpenClawChatTypography.footnote)
                .foregroundStyle(OpenClawChatTheme.muted)
            #endif
        }
        .task(id: LoadID(path: self.preview.inlineWidgetPath, resolverReady: self.resolverReady)) {
            let path = self.preview.inlineWidgetPath
            if self.activePath != path {
                self.reset(path: path)
            }
            guard self.resolverReady else { return }
            self.reset(path: path)
            guard let path else {
                self.unavailable = true
                return
            }
            await self.load(path: path, replacing: nil, generation: self.loadGeneration)
        }
        #if canImport(WebKit) && (os(iOS) || os(macOS))
        .alert("Widget export failed", isPresented: self.isPresentingExportError) {
            Button(role: .cancel) {
                self.exportErrorMessage = nil
            } label: {
                Text("OK")
                    .font(OpenClawChatTypography.body)
            }
        } message: {
            if let exportErrorMessage {
                Text(exportErrorMessage)
                    .font(OpenClawChatTypography.body)
            }
        }
        #if os(iOS)
        .sheet(item: self.$sharedImage) { item in
            ChatInlineWidgetShareSheet(image: item.image)
        }
        #endif
        #endif
    }

    #if canImport(WebKit) && (os(iOS) || os(macOS))
    private func renderedWidget(resource: OpenClawChatWidgetResource) -> some View {
        ChatInlineWidgetWebView(
            resource: resource,
            allowsScripts: self.preview.sandbox == "scripts",
            snapshotRequest: self.snapshotRequest,
            onFailure: { self.handleLoadFailure(resource: resource) },
            onSnapshot: self.handleSnapshot)
            .id([
                resource.url.absoluteString,
                resource.tlsFingerprintSHA256 ?? "",
                self.preview.sandbox ?? "",
            ].joined(separator: "\u{0}"))
            .frame(height: self.preview.inlineWidgetHeight)
            .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
            .overlay {
                RoundedRectangle(cornerRadius: 10, style: .continuous)
                    .stroke(OpenClawChatTheme.muted.opacity(0.24), lineWidth: 1)
            }
            .contentShape(Rectangle())
            .contextMenu {
                Button {
                    self.requestSnapshot(for: .copy)
                } label: {
                    Text("Copy image")
                        .font(OpenClawChatTypography.body)
                }

                Button {
                    self.requestSnapshot(for: .save)
                } label: {
                    #if os(macOS)
                    Text("Save image…")
                        .font(OpenClawChatTypography.body)
                    #else
                    Text("Save image")
                        .font(OpenClawChatTypography.body)
                    #endif
                }
            }
    }

    private func requestSnapshot(for action: ChatInlineWidgetSnapshotRequest.Action) {
        self.snapshotRequest = ChatInlineWidgetSnapshotRequest(action: action)
    }

    private func handleSnapshot(_ outcome: ChatInlineWidgetSnapshotOutcome) {
        switch outcome {
        case let .failure(request):
            self.clearSnapshotRequest(ifMatching: request)
            self.exportErrorMessage = String(localized: "The widget image could not be captured.")
        case let .success(request, image):
            self.clearSnapshotRequest(ifMatching: request)
            switch request.action {
            case .copy:
                self.copySnapshot(image)
            case .save:
                self.saveSnapshot(image)
            }
        }
    }

    private func clearSnapshotRequest(ifMatching completedRequest: ChatInlineWidgetSnapshotRequest) {
        guard self.snapshotRequest?.id == completedRequest.id else { return }
        self.snapshotRequest = nil
    }

    #if os(iOS)
    private func copySnapshot(_ image: ChatInlineWidgetSnapshotImage) {
        UIPasteboard.general.image = image
    }

    private func saveSnapshot(_ image: ChatInlineWidgetSnapshotImage) {
        self.sharedImage = ChatInlineWidgetSharedImage(image: image)
    }
    #elseif os(macOS)
    private func copySnapshot(_ image: ChatInlineWidgetSnapshotImage) {
        let pasteboard = NSPasteboard.general
        pasteboard.clearContents()
        guard pasteboard.writeObjects([image]) else {
            self.exportErrorMessage = String(localized: "The widget image could not be copied.")
            return
        }
    }

    private func saveSnapshot(_ image: ChatInlineWidgetSnapshotImage) {
        guard let pngData = image.chatInlineWidgetPNGData else {
            self.exportErrorMessage = String(localized: "The widget image could not be encoded as PNG.")
            return
        }

        let panel = NSSavePanel()
        panel.allowedContentTypes = [.png]
        panel.nameFieldStringValue = ChatInlineWidgetExport.filename(title: self.preview.title)
        panel.begin { response in
            guard response == .OK, let url = panel.url else { return }
            do {
                try pngData.write(to: url, options: .atomic)
            } catch {
                self.exportErrorMessage = String(localized: "The widget image could not be saved.")
            }
        }
    }
    #endif
    #endif

    private struct LoadID: Hashable {
        let path: String?
        let resolverReady: Bool
    }

    private func reset(path: String?) {
        self.loadGeneration = UUID()
        self.activePath = path
        self.resolvedResource = nil
        self.recoveryAttempts = 0
        self.refreshInFlight = false
        self.unavailable = false
    }

    private func load(
        path: String,
        replacing failedResource: OpenClawChatWidgetResource?,
        generation: UUID) async
    {
        let candidate = await self.resolveResource(path, failedResource)
        guard !Task.isCancelled,
              self.activePath == path,
              self.loadGeneration == generation
        else { return }
        let resource = candidate?.hasValidTLSBinding == true ? candidate : nil
        self.resolvedResource = resource
        self.unavailable = resource == nil
    }

    private func handleLoadFailure(resource: OpenClawChatWidgetResource) {
        guard self.resolvedResource == resource,
              let path = self.activePath,
              !self.refreshInFlight
        else { return }
        guard self.recoveryAttempts < 3 else {
            self.resolvedResource = nil
            self.unavailable = true
            return
        }
        self.recoveryAttempts += 1
        self.refreshInFlight = true
        let generation = self.loadGeneration
        Task { @MainActor in
            await self.load(path: path, replacing: resource, generation: generation)
            guard self.activePath == path, self.loadGeneration == generation else { return }
            self.refreshInFlight = false
        }
    }
}

extension OpenClawChatWidgetResource {
    fileprivate var hasValidTLSBinding: Bool {
        self.tlsFingerprintSHA256 == nil || self.url.scheme?.lowercased() == "https"
    }
}

#if canImport(WebKit) && (os(iOS) || os(macOS))
#if os(iOS)
private typealias ChatInlineWidgetSnapshotImage = UIImage
#elseif os(macOS)
private typealias ChatInlineWidgetSnapshotImage = NSImage
#endif

private struct ChatInlineWidgetSnapshotRequest: Equatable {
    enum Action: Equatable {
        case copy
        case save
    }

    let id = UUID()
    let action: Action
}

private enum ChatInlineWidgetSnapshotOutcome {
    case success(ChatInlineWidgetSnapshotRequest, ChatInlineWidgetSnapshotImage)
    case failure(ChatInlineWidgetSnapshotRequest)
}

enum ChatInlineWidgetTLSPin {
    static func normalize(_ raw: String) -> String? {
        let stripped = raw.replacingOccurrences(
            of: #"(?i)^sha-?256\s*:?\s*"#,
            with: "",
            options: .regularExpression)
        let normalized = stripped.lowercased().filter(\.isHexDigit)
        return normalized.count == 64 ? normalized : nil
    }

    static func fingerprint(certificateData: Data) -> String {
        SHA256.hash(data: certificateData).map { String(format: "%02x", $0) }.joined()
    }

    static func matches(_ expected: String, trust: SecTrust) -> Bool {
        guard let expected = normalize(expected),
              let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
              let certificate = chain.first
        else { return false }
        return self.fingerprint(certificateData: SecCertificateCopyData(certificate) as Data) == expected
    }
}

struct ChatInlineWidgetContentProcessRecovery {
    enum Action: Equatable {
        case reload
        case fail
    }

    private var didReload = false

    mutating func nextAction() -> Action {
        guard !self.didReload else { return .fail }
        self.didReload = true
        return .reload
    }

    mutating func reset() {
        self.didReload = false
    }
}

@MainActor
private final class ChatInlineWidgetNavigationDelegate: NSObject, WKNavigationDelegate {
    var resource: OpenClawChatWidgetResource {
        didSet {
            if self.resource != oldValue {
                self.contentProcessRecovery.reset()
            }
        }
    }

    let onFailure: @MainActor @Sendable () -> Void
    var onSnapshot: @MainActor @Sendable (ChatInlineWidgetSnapshotOutcome) -> Void
    private var contentProcessRecovery = ChatInlineWidgetContentProcessRecovery()
    private var lastSnapshotRequestID: UUID?

    init(
        resource: OpenClawChatWidgetResource,
        onFailure: @escaping @MainActor @Sendable () -> Void,
        onSnapshot: @escaping @MainActor @Sendable (ChatInlineWidgetSnapshotOutcome) -> Void)
    {
        self.resource = resource
        self.onFailure = onFailure
        self.onSnapshot = onSnapshot
    }

    func captureSnapshot(_ request: ChatInlineWidgetSnapshotRequest?, from webView: WKWebView) {
        guard let request, request.id != self.lastSnapshotRequestID else { return }
        self.lastSnapshotRequestID = request.id

        let configuration = WKSnapshotConfiguration()
        webView.takeSnapshot(with: configuration) { [weak self] image, _ in
            guard let self else { return }
            if let image {
                self.onSnapshot(.success(request, image))
            } else {
                self.onSnapshot(.failure(request))
            }
        }
    }

    func webView(
        _: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void)
    {
        if navigationAction.targetFrame?.isMainFrame == false {
            decisionHandler(.cancel)
            return
        }
        guard navigationAction.request.httpMethod?.caseInsensitiveCompare("GET") == .orderedSame,
              let url = navigationAction.request.url,
              self.matchesExpectedDocument(url)
        else {
            decisionHandler(.cancel)
            return
        }
        decisionHandler(.allow)
    }

    func webView(
        _: WKWebView,
        decidePolicyFor navigationResponse: WKNavigationResponse,
        decisionHandler: @escaping @MainActor @Sendable (WKNavigationResponsePolicy) -> Void)
    {
        if navigationResponse.isForMainFrame,
           let response = navigationResponse.response as? HTTPURLResponse,
           response.statusCode >= 400
        {
            self.onFailure()
            decisionHandler(.cancel)
            return
        }
        decisionHandler(.allow)
    }

    func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation?, withError _: any Error) {
        self.onFailure()
    }

    func webView(_: WKWebView, didFail _: WKNavigation?, withError _: any Error) {
        self.onFailure()
    }

    func webView(
        _: WKWebView,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping @MainActor @Sendable (
            URLSession.AuthChallengeDisposition,
            URLCredential?) -> Void)
    {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
              let expectedFingerprint = resource.tlsFingerprintSHA256
        else {
            completionHandler(.performDefaultHandling, nil)
            return
        }
        guard self.matchesExpectedProtectionSpace(challenge.protectionSpace),
              let trust = challenge.protectionSpace.serverTrust,
              ChatInlineWidgetTLSPin.matches(expectedFingerprint, trust: trust)
        else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            self.onFailure()
            return
        }
        completionHandler(.useCredential, URLCredential(trust: trust))
    }

    func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
        // One same-document recovery handles incidental process loss. A second
        // termination enters the view's bounded capability-refresh/failure path.
        switch self.contentProcessRecovery.nextAction() {
        case .reload:
            webView.load(URLRequest(url: self.resource.url, cachePolicy: .reloadIgnoringLocalCacheData))
        case .fail:
            self.onFailure()
        }
    }

    private func matchesExpectedDocument(_ candidate: URL) -> Bool {
        guard var expected = URLComponents(url: self.resource.url, resolvingAgainstBaseURL: false),
              var candidate = URLComponents(url: candidate, resolvingAgainstBaseURL: false)
        else { return false }
        expected.fragment = nil
        candidate.fragment = nil
        return expected == candidate
    }

    private func matchesExpectedProtectionSpace(_ protectionSpace: URLProtectionSpace) -> Bool {
        guard let expectedHost = self.resource.url.host,
              protectionSpace.host.caseInsensitiveCompare(expectedHost) == .orderedSame
        else { return false }
        let expectedPort = self.resource.url.port ?? (self.resource.url.scheme?.lowercased() == "https" ? 443 : 80)
        return protectionSpace.port == expectedPort
    }
}

@MainActor
private func makeChatInlineWidgetWebView(
    resource: OpenClawChatWidgetResource,
    allowsScripts: Bool,
    coordinator: ChatInlineWidgetNavigationDelegate) -> WKWebView
{
    let configuration = WKWebViewConfiguration()
    configuration.websiteDataStore = .nonPersistent()
    configuration.defaultWebpagePreferences.allowsContentJavaScript = allowsScripts
    configuration.preferences.javaScriptCanOpenWindowsAutomatically = false
    let webView = WKWebView(frame: .zero, configuration: configuration)
    webView.navigationDelegate = coordinator
    webView.allowsLinkPreview = false
    webView.load(URLRequest(url: resource.url, cachePolicy: .reloadIgnoringLocalCacheData))
    return webView
}

#if os(iOS)
private struct ChatInlineWidgetWebView: UIViewRepresentable {
    let resource: OpenClawChatWidgetResource
    let allowsScripts: Bool
    let snapshotRequest: ChatInlineWidgetSnapshotRequest?
    let onFailure: @MainActor @Sendable () -> Void
    let onSnapshot: @MainActor @Sendable (ChatInlineWidgetSnapshotOutcome) -> Void

    func makeCoordinator() -> ChatInlineWidgetNavigationDelegate {
        ChatInlineWidgetNavigationDelegate(
            resource: self.resource,
            onFailure: self.onFailure,
            onSnapshot: self.onSnapshot)
    }

    func makeUIView(context: Context) -> WKWebView {
        makeChatInlineWidgetWebView(
            resource: self.resource,
            allowsScripts: self.allowsScripts,
            coordinator: context.coordinator)
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        context.coordinator.onSnapshot = self.onSnapshot
        if context.coordinator.resource != self.resource {
            context.coordinator.resource = self.resource
            webView.load(URLRequest(url: self.resource.url, cachePolicy: .reloadIgnoringLocalCacheData))
        }
        context.coordinator.captureSnapshot(self.snapshotRequest, from: webView)
    }

    static func dismantleUIView(_ webView: WKWebView, coordinator: ChatInlineWidgetNavigationDelegate) {
        webView.stopLoading()
        webView.navigationDelegate = nil
    }
}
#elseif os(macOS)
private struct ChatInlineWidgetWebView: NSViewRepresentable {
    let resource: OpenClawChatWidgetResource
    let allowsScripts: Bool
    let snapshotRequest: ChatInlineWidgetSnapshotRequest?
    let onFailure: @MainActor @Sendable () -> Void
    let onSnapshot: @MainActor @Sendable (ChatInlineWidgetSnapshotOutcome) -> Void

    func makeCoordinator() -> ChatInlineWidgetNavigationDelegate {
        ChatInlineWidgetNavigationDelegate(
            resource: self.resource,
            onFailure: self.onFailure,
            onSnapshot: self.onSnapshot)
    }

    func makeNSView(context: Context) -> WKWebView {
        makeChatInlineWidgetWebView(
            resource: self.resource,
            allowsScripts: self.allowsScripts,
            coordinator: context.coordinator)
    }

    func updateNSView(_ webView: WKWebView, context: Context) {
        context.coordinator.onSnapshot = self.onSnapshot
        if context.coordinator.resource != self.resource {
            context.coordinator.resource = self.resource
            webView.load(URLRequest(url: self.resource.url, cachePolicy: .reloadIgnoringLocalCacheData))
        }
        context.coordinator.captureSnapshot(self.snapshotRequest, from: webView)
    }

    static func dismantleNSView(_ webView: WKWebView, coordinator: ChatInlineWidgetNavigationDelegate) {
        webView.stopLoading()
        webView.navigationDelegate = nil
    }
}
#endif

#if os(iOS)
private struct ChatInlineWidgetSharedImage: Identifiable {
    let id = UUID()
    let image: UIImage
}

private struct ChatInlineWidgetShareSheet: UIViewControllerRepresentable {
    let image: UIImage

    func makeUIViewController(context _: Context) -> UIActivityViewController {
        UIActivityViewController(activityItems: [self.image], applicationActivities: nil)
    }

    func updateUIViewController(_: UIActivityViewController, context _: Context) {}
}
#elseif os(macOS)
extension NSImage {
    fileprivate var chatInlineWidgetPNGData: Data? {
        guard let tiffRepresentation,
              let representation = NSBitmapImageRep(data: tiffRepresentation)
        else { return nil }
        return representation.representation(using: .png, properties: [:])
    }
}
#endif
#endif
