import Foundation
import Combine

/// Persists a route-explorer `/api/token` value (with expiry) that the
/// user captured from Safari via the bookmarklet flow. Backed by
/// `UserDefaults` because the data is small (~250 bytes) and survives
/// process restarts.
///
/// Why this exists: route-explorer's edge gates `/api/token` behind a
/// Cloudflare Turnstile challenge that requires Apple's Private Access
/// Token. PAT issuance is restricted to apps with the
/// `com.apple.developer.web-browser` entitlement (Safari, Chrome, Brave,
/// DuckDuckGo, etc.) — third-party apps don't qualify, so our WKWebView
/// can never mint a token. Safari on the same device *can*, so we let
/// the user trip Turnstile in Safari with a bookmarklet, send the freshly
/// minted token back to the app via the `flights://routeexplorer-token`
/// URL scheme, and use that token from URLSession until it expires.
@MainActor
final class RouteExplorerTokenStore: ObservableObject {
    static let shared = RouteExplorerTokenStore()

    private let defaults = UserDefaults.standard

    @Published private(set) var token: String?
    @Published private(set) var expiresAt: Date?
    /// Optional cookie jar captured at the same time as the token. Some
    /// route-explorer endpoints may also gate on `rex_clearance` /
    /// `am_user_session`; if the bookmarklet manages to capture them
    /// (they need to be non-HttpOnly for `document.cookie` to read them),
    /// we attach them on outgoing requests.
    @Published private(set) var capturedCookieHeader: String?

    private init() {
        if let stored = defaults.string(forKey: Keys.token),
           let expEpoch = defaults.object(forKey: Keys.expiresAt) as? TimeInterval {
            self.token = stored
            self.expiresAt = Date(timeIntervalSince1970: expEpoch)
        }
        self.capturedCookieHeader = defaults.string(forKey: Keys.cookieHeader)
    }

    var isValid: Bool {
        guard let token, !token.isEmpty,
              let expiresAt, expiresAt > Date()
        else { return false }
        _ = token
        return true
    }

    var timeRemaining: TimeInterval {
        guard let expiresAt else { return 0 }
        return max(0, expiresAt.timeIntervalSinceNow)
    }

    /// Store a token captured from the Safari bookmarklet flow.
    /// `expiresInSeconds` defaults to 30 minutes (route-explorer's
    /// typical token TTL); the caller can override if the bookmarklet
    /// surfaces a precise expiry.
    func store(token: String,
               expiresInSeconds: TimeInterval = 30 * 60,
               cookieHeader: String? = nil) {
        let exp = Date(timeIntervalSinceNow: expiresInSeconds)
        self.token = token
        self.expiresAt = exp
        self.capturedCookieHeader = cookieHeader
        defaults.set(token, forKey: Keys.token)
        defaults.set(exp.timeIntervalSince1970, forKey: Keys.expiresAt)
        if let cookieHeader, !cookieHeader.isEmpty {
            defaults.set(cookieHeader, forKey: Keys.cookieHeader)
        } else {
            defaults.removeObject(forKey: Keys.cookieHeader)
        }
        DiagnosticLogger.shared.log("RETOK", "stored", [
            "expiresAt": exp.timeIntervalSince1970,
            "cookieLen": cookieHeader?.count ?? 0,
        ])
    }

    func clear() {
        token = nil
        expiresAt = nil
        capturedCookieHeader = nil
        defaults.removeObject(forKey: Keys.token)
        defaults.removeObject(forKey: Keys.expiresAt)
        defaults.removeObject(forKey: Keys.cookieHeader)
        DiagnosticLogger.shared.log("RETOK", "cleared", [:])
    }

    // MARK: - URL scheme ingest

    /// Returns true if `url` is the route-explorer token deep link and
    /// the credentials were successfully extracted + stored.
    @discardableResult
    func ingest(url: URL) -> Bool {
        guard url.scheme == "flights",
              url.host == "routeexplorer-token"
        else { return false }
        let comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
        let items = comps?.queryItems ?? []
        func val(_ k: String) -> String? { items.first { $0.name == k }?.value }
        guard let token = val("token"), !token.isEmpty else {
            DiagnosticLogger.shared.log("RETOK", "ingestNoToken", [
                "url": url.absoluteString,
            ])
            return false
        }
        let exp: TimeInterval = {
            if let expStr = val("exp"), let expVal = TimeInterval(expStr) {
                return max(0, expVal - Date().timeIntervalSince1970)
            }
            return 30 * 60
        }()
        let cookie = val("cookie")?.removingPercentEncoding
        store(token: token, expiresInSeconds: exp, cookieHeader: cookie)
        return true
    }

    // MARK: - Storage keys

    private enum Keys {
        static let token = "re.token.value"
        static let expiresAt = "re.token.expiresAt"
        static let cookieHeader = "re.token.cookieHeader"
    }
}
