import SwiftUI
import WebKit

/// Diagnostic harness for the route-explorer Cloudflare Turnstile gate.
/// Surfaces every signal that's hidden inside the production
/// ``RouteExplorerGateSheet``:
///
///   * The WKWebView itself (visible & interactive — so we can see if
///     the Turnstile widget even renders).
///   * Live cookie dump for `route-explorer.com` from the shared
///     `WKWebsiteDataStore.default()`.
///   * `/api/token` probe each tick — both via in-page `fetch()` and via
///     URLSession-with-replayed-cookies, so we can spot the case where
///     the WebView gets cleared but URLSession still gets 403.
///   * Console messages (`console.log` etc.) bridged through a
///     `WKScriptMessageHandler` so JS errors aren't invisible.
///   * Knobs to flip the variables we think might matter (UA flavour,
///     pre-warm, webdriver override) without rebuilding.
///
/// Goal: identify *which* of three failure modes we're in —
///   A. Turnstile widget doesn't render in WKWebView at all.
///   B. Widget renders, can be solved, cookie lands — but URLSession
///      replay still fails (cookie scope / TLS fingerprint issue).
///   C. Widget renders and the cookie correctly carries into both the
///      in-page probe AND URLSession replay — meaning gate sheet should
///      already work and the bug is elsewhere.
struct TurnstileDebugView: View {

    // MARK: - Knobs

    enum UAFlavour: String, CaseIterable, Identifiable {
        case iOSSafari17 = "iOS Safari 17.5"
        case iOSSafari18 = "iOS Safari 18.5"
        case macSafari = "Mac Safari 17.5"
        case webViewDefault = "WKWebView default"
        var id: String { rawValue }
        var ua: String? {
            switch self {
            case .iOSSafari17:
                return "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) "
                    + "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 "
                    + "Mobile/15E148 Safari/604.1"
            case .iOSSafari18:
                return "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) "
                    + "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 "
                    + "Mobile/15E148 Safari/604.1"
            case .macSafari:
                return "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) "
                    + "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 "
                    + "Safari/605.1.15"
            case .webViewDefault:
                return nil
            }
        }
    }

    @State private var ua: UAFlavour = .iOSSafari17
    @State private var injectWebdriverOverride: Bool = true
    @State private var injectLanguageHeader: Bool = true
    @State private var prewarmHomepage: Bool = false

    @State private var status: String = "Idle"
    @State private var cookies: [String: String] = [:]
    @State private var lastTokenStatus: Int = -2
    @State private var lastURLSessionStatus: Int = -2
    @State private var consoleLines: [String] = []
    @State private var probeTick: Int = 0
    @State private var seed: Int = 0   // bumping this rebuilds the WebView

    /// Every state-change writes a single line to ~/Documents/turnstile.log
    /// inside the app's container so the CLI harness can `tail -F` it from
    /// outside the sim. Format: `ISO8601\tevent\tkey=value\t...`.
    @State private var logFileURL: URL? = {
        guard let docs = FileManager.default.urls(
            for: .documentDirectory, in: .userDomainMask
        ).first else { return nil }
        let url = docs.appendingPathComponent("turnstile.log")
        // Truncate on each fresh session so we don't read stale history.
        try? "".write(to: url, atomically: true, encoding: .utf8)
        return url
    }()

    private func appendLog(_ line: String) {
        guard let url = logFileURL else { return }
        let ts = ISO8601DateFormatter().string(from: Date())
        let entry = "\(ts)\t\(line)\n"
        if let data = entry.data(using: .utf8) {
            if let handle = try? FileHandle(forWritingTo: url) {
                _ = try? handle.seekToEnd()
                try? handle.write(contentsOf: data)
                try? handle.close()
            } else {
                try? data.write(to: url)
            }
        }
    }

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                knobsCard
                statusCard
                cookieCard
                consoleCard
                webViewCard
                replayCard
            }
            .padding()
        }
        .navigationTitle("Turnstile Diagnostics")
        .navigationBarTitleDisplayMode(.inline)
    }

    // MARK: - Cards

    private var knobsCard: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("KNOBS").font(.caption.weight(.heavy)).foregroundStyle(.secondary)
            Picker("User agent", selection: $ua) {
                ForEach(UAFlavour.allCases) { Text($0.rawValue).tag($0) }
            }
            Toggle("Override navigator.webdriver = undefined", isOn: $injectWebdriverOverride)
                .font(.footnote)
            Toggle("Inject Accept-Language: en-US,en;q=0.9", isOn: $injectLanguageHeader)
                .font(.footnote)
            Toggle("Pre-warm apple.com before route-explorer.com", isOn: $prewarmHomepage)
                .font(.footnote)
            Button("Rebuild WebView with current knobs") {
                consoleLines = []
                cookies = [:]
                status = "Rebuilding…"
                seed += 1
            }
            .buttonStyle(.borderedProminent)
            Button("Wipe route-explorer cookies (data store)") {
                Task { await wipeCookies() }
            }
            .buttonStyle(.bordered)
        }
        .padding()
        .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
    }

    private var statusCard: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("STATUS").font(.caption.weight(.heavy)).foregroundStyle(.secondary)
            HStack {
                Image(systemName: lastTokenStatus == 200 ? "checkmark.seal.fill" : "shield.lefthalf.filled")
                    .foregroundStyle(lastTokenStatus == 200 ? .green : .orange)
                Text(status).font(.footnote.monospaced())
            }
            HStack {
                tag("WebView /api/token", value: tokenLabel(lastTokenStatus))
                tag("URLSession /api/token", value: tokenLabel(lastURLSessionStatus))
            }
            Text("probe #\(probeTick)").font(.caption2).foregroundStyle(.secondary)
        }
        .padding()
        .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
    }

    private var cookieCard: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("COOKIES (route-explorer.com)").font(.caption.weight(.heavy)).foregroundStyle(.secondary)
            if cookies.isEmpty {
                Text("none").font(.footnote.monospaced()).foregroundStyle(.secondary)
            } else {
                ForEach(cookies.keys.sorted(), id: \.self) { name in
                    HStack(alignment: .firstTextBaseline) {
                        Text(name).font(.caption.monospaced().weight(.semibold))
                        Spacer()
                        Text(cookies[name]?.prefix(40).description ?? "")
                            .font(.caption2.monospaced()).foregroundStyle(.secondary)
                            .lineLimit(1).truncationMode(.middle)
                    }
                }
            }
        }
        .padding()
        .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
    }

    private var consoleCard: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("JS CONSOLE / NAV LOG").font(.caption.weight(.heavy)).foregroundStyle(.secondary)
            if consoleLines.isEmpty {
                Text("no messages yet").font(.footnote.monospaced()).foregroundStyle(.secondary)
            } else {
                ForEach(consoleLines.suffix(14), id: \.self) { line in
                    Text(line).font(.caption2.monospaced()).foregroundStyle(.secondary)
                        .lineLimit(2).truncationMode(.tail)
                }
            }
        }
        .padding()
        .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
    }

    private var webViewCard: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("WEBVIEW").font(.caption.weight(.heavy)).foregroundStyle(.secondary)
            Text("Solve Turnstile in this view if it renders. The card auto-polls.")
                .font(.caption).foregroundStyle(.secondary)
            DebugTurnstileWebView(
                seed: seed,
                ua: ua.ua,
                injectWebdriverOverride: injectWebdriverOverride,
                injectLanguageHeader: injectLanguageHeader,
                prewarmHomepage: prewarmHomepage,
                onTokenStatus: { status, tick in
                    self.lastTokenStatus = status
                    self.probeTick = tick
                    self.status = "WebView /api/token → \(self.tokenLabel(status)) (probe #\(tick))"
                    self.appendLog("probe\ttick=\(tick)\twebview_status=\(status)")
                },
                onCookies: { dict in
                    self.cookies = dict
                    let summary = dict.keys.sorted().joined(separator: ",")
                    self.appendLog("cookies\tcount=\(dict.count)\tnames=\(summary)")
                },
                onConsole: { msg in
                    self.consoleLines.append(msg)
                    self.appendLog("console\t\(msg.prefix(200))")
                }
            )
            .frame(height: 480)
            .clipShape(RoundedRectangle(cornerRadius: 10))
        }
        .padding()
        .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
    }

    private var replayCard: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("URLSESSION REPLAY").font(.caption.weight(.heavy)).foregroundStyle(.secondary)
            Text("Hits /api/token via URLSession (not the WebView), copying the current WKWebsiteDataStore cookies into HTTPCookieStorage. Tells us if a cleared cookie travels.")
                .font(.caption).foregroundStyle(.secondary)
            Button("Run URLSession replay") {
                Task { await runURLSessionReplay() }
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
    }

    // MARK: - Actions

    private func wipeCookies() async {
        let store = WKWebsiteDataStore.default()
        let types: Set<String> = [WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage,
                                  WKWebsiteDataTypeSessionStorage]
        await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
            store.removeData(ofTypes: types, modifiedSince: .distantPast) {
                cont.resume()
            }
        }
        cookies = [:]
        status = "Cookies wiped"
    }

    private func runURLSessionReplay() async {
        let store = WKWebsiteDataStore.default()
        let wkCookies = await store.httpCookieStore.allCookies()
        let storage = HTTPCookieStorage()
        for c in wkCookies where c.domain.contains("route-explorer.com") {
            storage.setCookie(c)
        }
        let config = URLSessionConfiguration.default
        config.httpCookieStorage = storage
        config.httpCookieAcceptPolicy = .always
        if injectLanguageHeader {
            config.httpAdditionalHeaders = ["Accept-Language": "en-US,en;q=0.9"]
        }
        let session = URLSession(configuration: config)
        var req = URLRequest(url: URL(string: "https://route-explorer.com/api/token")!)
        if let ua = ua.ua { req.setValue(ua, forHTTPHeaderField: "User-Agent") }
        req.setValue("application/json", forHTTPHeaderField: "Accept")
        req.setValue("https://route-explorer.com/", forHTTPHeaderField: "Referer")
        req.setValue("https://route-explorer.com", forHTTPHeaderField: "Origin")
        do {
            let (data, response) = try await session.data(for: req)
            let http = response as? HTTPURLResponse
            lastURLSessionStatus = http?.statusCode ?? -1
            let body = String(data: data, encoding: .utf8) ?? ""
            consoleLines.append("URLSession replay → \(lastURLSessionStatus): \(body.prefix(160))")
        } catch {
            lastURLSessionStatus = -1
            consoleLines.append("URLSession replay → error: \(error.localizedDescription)")
        }
    }

    // MARK: - Helpers

    private func tokenLabel(_ s: Int) -> String {
        switch s {
        case -2: return "—"
        case -1: return "err"
        default: return "\(s)"
        }
    }

    private func tag(_ label: String, value: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label).font(.caption2).foregroundStyle(.secondary)
            Text(value).font(.footnote.monospaced().weight(.bold))
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }
}

// MARK: - WKWebView host

private struct DebugTurnstileWebView: UIViewRepresentable {
    let seed: Int
    let ua: String?
    let injectWebdriverOverride: Bool
    let injectLanguageHeader: Bool
    let prewarmHomepage: Bool
    let onTokenStatus: (Int, Int) -> Void
    let onCookies: ([String: String]) -> Void
    let onConsole: (String) -> Void

    func makeCoordinator() -> Coordinator {
        Coordinator(onTokenStatus: onTokenStatus,
                    onCookies: onCookies,
                    onConsole: onConsole)
    }

    func makeUIView(context: Context) -> WKWebView {
        rebuild(context: context)
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        // Seed changing → wipe and re-create. We can't mutate the WKWebView
        // in place because some config knobs (data store, content controller)
        // are immutable post-init.
        if context.coordinator.lastSeed != seed {
            context.coordinator.lastSeed = seed
            context.coordinator.detach()
            context.coordinator.attach(rebuild(context: context))
        }
    }

    private func rebuild(context: Context) -> WKWebView {
        let config = WKWebViewConfiguration()
        config.websiteDataStore = .default()
        let contentController = WKUserContentController()
        // Bridge console.log → coordinator
        let consoleBridge = """
        (function() {
            const orig = window.console;
            const send = (lvl, args) => {
                try {
                    window.webkit.messageHandlers.console.postMessage(
                        lvl + ": " + Array.from(args).map(a =>
                            (typeof a === 'object' ? JSON.stringify(a) : String(a))
                        ).join(' ')
                    );
                } catch (e) {}
            };
            ['log','info','warn','error','debug'].forEach(lvl => {
                const f = orig[lvl];
                orig[lvl] = function(...args) { send(lvl, args); return f.apply(orig, args); };
            });
        })();
        """
        contentController.addUserScript(WKUserScript(
            source: consoleBridge,
            injectionTime: .atDocumentStart,
            forMainFrameOnly: false
        ))
        if injectWebdriverOverride {
            let override = """
            Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
            """
            contentController.addUserScript(WKUserScript(
                source: override,
                injectionTime: .atDocumentStart,
                forMainFrameOnly: false
            ))
        }
        contentController.add(context.coordinator, name: "console")
        config.userContentController = contentController

        let webView = WKWebView(frame: .zero, configuration: config)
        webView.customUserAgent = ua
        webView.navigationDelegate = context.coordinator
        context.coordinator.attach(webView)

        var req = URLRequest(url: URL(string:
            prewarmHomepage ? "https://www.apple.com/" : "https://route-explorer.com/"
        )!)
        if injectLanguageHeader {
            req.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
        }
        context.coordinator.targetURL = URL(string: "https://route-explorer.com/")!
        context.coordinator.prewarming = prewarmHomepage
        webView.load(req)
        return webView
    }

    final class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
        var lastSeed: Int = 0
        var prewarming: Bool = false
        var targetURL: URL?
        private weak var webView: WKWebView?
        private var pollTimer: Timer?
        private var pollingInFlight = false
        private var tick = 0
        let onTokenStatus: (Int, Int) -> Void
        let onCookies: ([String: String]) -> Void
        let onConsole: (String) -> Void

        init(onTokenStatus: @escaping (Int, Int) -> Void,
             onCookies: @escaping ([String: String]) -> Void,
             onConsole: @escaping (String) -> Void) {
            self.onTokenStatus = onTokenStatus
            self.onCookies = onCookies
            self.onConsole = onConsole
        }

        func attach(_ webView: WKWebView) {
            self.webView = webView
        }

        func detach() {
            pollTimer?.invalidate()
            pollTimer = nil
            webView?.stopLoading()
            webView = nil
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            onConsole("nav: didFinish url=\(webView.url?.absoluteString ?? "?")")
            // If we pre-warmed apple.com, jump to route-explorer now.
            if prewarming, let target = targetURL, webView.url?.host?.contains("apple.com") == true {
                prewarming = false
                webView.load(URLRequest(url: target))
                return
            }
            startPolling()
        }

        func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
            onConsole("nav: didFail \(error.localizedDescription)")
        }

        func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
            onConsole("nav: didFailProvisional \(error.localizedDescription)")
        }

        // MARK: WKScriptMessageHandler
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            if message.name == "console", let body = message.body as? String {
                onConsole("js: \(body.prefix(180))")
            }
        }

        // MARK: Polling
        private func startPolling() {
            guard pollTimer == nil else { return }
            pollTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { [weak self] _ in
                Task { @MainActor in self?.probe() }
            }
            // First probe immediately.
            Task { @MainActor in self.probe() }
        }

        @MainActor
        private func probe() {
            guard let webView, !pollingInFlight else { return }
            pollingInFlight = true
            tick += 1
            let myTick = tick
            let js = """
            return await new Promise((res) => {
                fetch('/api/token', { credentials: 'include' })
                    .then(r => r.text().then(t => res({status: r.status, body: t})))
                    .catch(e => res({status: -1, body: String(e)}));
            });
            """
            Task {
                let result = try? await webView.callAsyncJavaScript(
                    js, contentWorld: .page
                )
                let dict = result as? [String: Any]
                let status = dict?["status"] as? Int ?? -1
                self.onTokenStatus(status, myTick)
                // Refresh cookie snapshot
                let wkCookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
                var snapshot: [String: String] = [:]
                for c in wkCookies where c.domain.contains("route-explorer.com") {
                    snapshot[c.name] = c.value
                }
                self.onCookies(snapshot)
                self.pollingInFlight = false
            }
        }
    }
}
