import Foundation
import WebKit

/// Runs XHRs from inside a WKWebView that's been navigated to a target
/// origin, so the request carries Safari's TLS fingerprint and any
/// first-party cookies the edge expects. The cookie store is the
/// process-wide persistent `WKWebsiteDataStore.default()`, shared with
/// `RouteExplorerGateSheet` — once the user clears Cloudflare Turnstile
/// once, the `am_clearance` cookie sticks across app launches and every
/// subsequent fetch reuses it.
@MainActor
final class WebViewFetcher {

    func fetch(
        navigateTo pageURL: String,
        fetchURL: String,
        method: String = "POST",
        headers: [String: String] = [:],
        body: String? = nil,
        userAgent: String? = nil,
        includeCredentials: Bool = false
    ) async -> (data: String?, error: String?) {
        DiagnosticLogger.shared.log("WVF", "begin", [
            "pageURL": pageURL,
            "fetchURL": fetchURL,
            "method": method,
            "ua": userAgent ?? "(default)",
        ])
        let config = WKWebViewConfiguration()
        config.websiteDataStore = .default()
        let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: config)
        webView.customUserAgent = userAgent

        guard let url = URL(string: pageURL) else {
            DiagnosticLogger.shared.log("WVF", "invalidURL", ["url": pageURL])
            return (nil, "Invalid page URL")
        }

        let navResult = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
            let delegate = NavigationDelegate(continuation: continuation)
            webView.navigationDelegate = delegate
            webView.load(URLRequest(url: url))
            objc_setAssociatedObject(webView, "navDelegate", delegate, .OBJC_ASSOCIATION_RETAIN)
        }

        // Snapshot cookies on the data store so we can see what the
        // navigation handed us. Cookie scope matters here because
        // includeCredentials=true reads from this same store.
        let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
        let domainCookies = cookies.filter {
            guard let host = URL(string: pageURL)?.host else { return false }
            return $0.domain.contains(host) || host.contains($0.domain.trimmingCharacters(in: .init(charactersIn: ".")))
        }
        DiagnosticLogger.shared.log("WVF", "navDone", [
            "ok": navResult,
            "cookieCount": domainCookies.count,
            "cookieNames": domainCookies.map { $0.name }.sorted().joined(separator: ","),
        ])

        guard navResult else {
            DiagnosticLogger.shared.log("WVF", "navFailed", ["pageURL": pageURL])
            return (nil, "Failed to load page")
        }

        let js = """
        return await new Promise((resolve) => {
            const xhr = new XMLHttpRequest();
            xhr.open("\(method)", "\(fetchURL)", true);
            xhr.withCredentials = \(includeCredentials ? "true" : "false");
            \(headers.map { "xhr.setRequestHeader(\"\($0.key)\", \"\($0.value)\");" }.joined(separator: "\n            "))
            xhr.onload = function() {
                resolve(JSON.stringify({ status: xhr.status, body: xhr.responseText }));
            };
            xhr.onerror = function() {
                resolve(JSON.stringify({ status: -1, body: "XHR error" }));
            };
            xhr.send(\(body ?? "null"));
        });
        """

        do {
            let result = try await webView.callAsyncJavaScript(js, contentWorld: .page)
            guard let resultStr = result as? String,
                  let data = resultStr.data(using: .utf8),
                  let wrapper = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
            else {
                DiagnosticLogger.shared.log("WVF", "fetchNoResult", [:])
                return (nil, "No string result from JS")
            }
            let status = wrapper["status"] as? Int ?? -1
            let respBody = wrapper["body"] as? String ?? ""
            DiagnosticLogger.shared.log("WVF", "fetchDone", [
                "fetchURL": fetchURL,
                "status": status,
                "bodyPreview": String(respBody.prefix(220)),
            ])
            if status == 200 {
                return (respBody, nil)
            } else {
                return (nil, "HTTP \(status): \(String(respBody.prefix(200)))")
            }
        } catch {
            DiagnosticLogger.shared.log("WVF", "fetchThrew", [
                "error": error.localizedDescription,
            ])
            return (nil, error.localizedDescription)
        }
    }
}

private class NavigationDelegate: NSObject, WKNavigationDelegate {
    private var continuation: CheckedContinuation<Bool, Never>?
    init(continuation: CheckedContinuation<Bool, Never>) {
        self.continuation = continuation
    }
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        continuation?.resume(returning: true); continuation = nil
    }
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        continuation?.resume(returning: false); continuation = nil
    }
    func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        continuation?.resume(returning: false); continuation = nil
    }
}
