import CodexBarCore
import Foundation

struct OpenAIWebRefreshGateContext {
    let force: Bool
    let accountDidChange: Bool
    let lastError: String?
    let lastSnapshotAt: Date?
    let lastAttemptAt: Date?
    let now: Date
    let refreshInterval: TimeInterval
}

struct OpenAIWebRefreshPolicyContext {
    let accessEnabled: Bool
    let batterySaverEnabled: Bool
    let force: Bool
    let refreshPhase: ProviderRefreshPhase
}

// MARK: - OpenAI web lifecycle

extension UsageStore {
    private struct OpenAIDashboardRefreshContext {
        let targetEmail: String?
        let allowCurrentSnapshotFallback: Bool
        let expectedGuard: CodexAccountScopedRefreshGuard?
        let refreshTaskToken: UUID
        let allowCodexUsageBackfill: Bool
        let force: Bool
    }

    private struct OpenAIDashboardCookieImportRequest {
        let normalizedTarget: String?
        let allowAnyAccount: Bool
        let cookieSource: ProviderCookieSource
        let cacheScope: CookieHeaderCache.Scope?
        let preferCachedCookieHeader: Bool?
        let force: Bool
    }

    private static let openAIWebRefreshMultiplier: TimeInterval = 5
    private static let openAIWebPrimaryFetchTimeout: TimeInterval = 25
    private static let openAIWebRetryFetchTimeout: TimeInterval = 8
    private static let openAIWebPostImportFetchTimeout: TimeInterval = 25

    static func openAIWebDashboardFetchTimeout(didImportCookies: Bool) -> TimeInterval {
        didImportCookies ? self.openAIWebPostImportFetchTimeout : self.openAIWebPrimaryFetchTimeout
    }

    static func openAIWebRetryDashboardFetchTimeout(afterCookieImport: Bool) -> TimeInterval {
        afterCookieImport ? self.openAIWebPostImportFetchTimeout : self.openAIWebRetryFetchTimeout
    }

    nonisolated static func refreshPhase(
        hasCompletedInitialRefresh: Bool) -> ProviderRefreshPhase
    {
        hasCompletedInitialRefresh ? .regular : .startup
    }

    nonisolated static func openAIWebRefreshPhase(
        providerRefreshPhase: ProviderRefreshPhase,
        startupConnectivityRetryAttempt: Int?) -> ProviderRefreshPhase
    {
        startupConnectivityRetryAttempt == nil ? providerRefreshPhase : .startup
    }

    func openAIWebRefreshIntervalSeconds() -> TimeInterval {
        let base = max(self.normalRefreshIntervalForHeuristics() ?? 0, 120)
        return base * Self.openAIWebRefreshMultiplier
    }

    func requestOpenAIDashboardRefreshIfStale(reason: String) {
        // Provider-specific by design: the OpenAI WKWebView session lifecycle attaches only to Codex consumer usage.
        guard self.isEnabled(.codex),
              self.settings.openAIWebAccessEnabled,
              self.settings.codexCookieSource.isEnabled
        else { return }
        let now = Date()
        let refreshInterval = self.openAIWebRefreshIntervalSeconds()
        let dashboard = self.openAIDashboard ?? self.lastOpenAIDashboardSnapshot
        let lastUpdatedAt = dashboard?.updatedAt
        let needsMenuHistoryRefresh = dashboard?.dailyBreakdown.isEmpty == true &&
            dashboard?.usageBreakdown.isEmpty == true
        if needsMenuHistoryRefresh,
           Self.shouldSkipOpenAIWebEmptyHistoryRetry(.init(
               force: false,
               accountDidChange: self.openAIWebAccountDidChange,
               lastError: self.lastOpenAIDashboardError,
               lastSnapshotAt: lastUpdatedAt,
               lastAttemptAt: self.lastOpenAIDashboardAttemptAt,
               now: now,
               refreshInterval: refreshInterval))
        {
            return
        }
        if let lastUpdatedAt, now.timeIntervalSince(lastUpdatedAt) < refreshInterval, !needsMenuHistoryRefresh {
            return
        }
        let stamp = now.formatted(date: .abbreviated, time: .shortened)
        self.logOpenAIWeb("[\(stamp)] OpenAI web refresh request: \(reason)")
        let forceRefresh = Self.forceOpenAIWebRefreshForStaleRequest(
            batterySaverEnabled: self.settings.effectiveOpenAIWebBatterySaverEnabled) || needsMenuHistoryRefresh
        self.openAIWebLogger.info(
            "OpenAI web stale refresh gate",
            metadata: [
                "reason": reason,
                "force": forceRefresh ? "1" : "0",
                "batterySaverEnabled": self.settings.effectiveOpenAIWebBatterySaverEnabled ? "1" : "0",
                "interaction": ProviderInteractionContext.current == .userInitiated ? "user" : "background",
            ])
        let expectedGuard = self.freshCodexOpenAIWebRefreshGuard()
        Task { await self.refreshOpenAIDashboardIfNeeded(force: forceRefresh, expectedGuard: expectedGuard) }
    }

    func applyOpenAIDashboard(
        _ dash: OpenAIDashboardSnapshot,
        targetEmail: String?,
        expectedGuard: CodexAccountScopedRefreshGuard? = nil,
        refreshTaskToken: UUID? = nil,
        allowCodexUsageBackfill: Bool = true) async
    {
        guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
        self.settings.invalidateCodexAccountReconciliationSnapshotCache()
        let authority = self.evaluateCodexDashboardAuthority(
            dashboard: dash,
            sourceKind: .liveWeb,
            routingTargetEmail: targetEmail)
        if let expectedGuard {
            let shouldApply = switch authority.decision.disposition {
            case .attach:
                self.shouldApplyOpenAIDashboardRefreshGuard(
                    expectedGuard: expectedGuard,
                    routingTargetEmail: targetEmail)
            case .displayOnly, .failClosed:
                self.shouldApplyOpenAIDashboardPolicyResult(
                    expectedGuard: expectedGuard,
                    routingTargetEmail: targetEmail)
            }
            guard shouldApply else { return }
        }

        let attachedAccountEmail = self.codexDashboardAttachmentEmail(from: authority.input)
        self.reconcileCodexPublishedUsageOwner(with: self.freshCodexAccountScopedRefreshGuard(
            preferCurrentSnapshot: true,
            allowLastKnownLiveFallback: false))

        await self.applyOpenAIDashboardAuthorityDecision(
            authority.decision,
            dashboard: dash,
            authorityInput: authority.input,
            attachedAccountEmail: attachedAccountEmail,
            allowCodexUsageBackfill: allowCodexUsageBackfill)
    }

    func applyOpenAIDashboardFailure(
        message: String,
        expectedGuard: CodexAccountScopedRefreshGuard? = nil,
        refreshTaskToken: UUID? = nil,
        routingTargetEmail: String? = nil) async
    {
        guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
        if let expectedGuard,
           !self.shouldApplyOpenAIWebNonSuccessResult(
               expectedGuard: expectedGuard,
               routingTargetEmail: routingTargetEmail)
        {
            return
        }
        if self.openAIWebManagedTargetStoreIsUnreadable() {
            await self.failClosedRefreshForUnreadableManagedCodexStore()
            return
        }
        if self.openAIWebManagedTargetIsMissing() {
            await self.failClosedRefreshForMissingManagedCodexTarget()
            return
        }
        if self.openAIWebProfileTargetEmailIsMissing() {
            await self.failClosedRefreshForMissingProfileCodexTarget()
            return
        }

        OpenAIDashboardFetcher.evictAllCachedWebViews()
        await MainActor.run {
            if let cached = self.lastOpenAIDashboardSnapshot {
                self.openAIDashboard = cached
                self.openAIDashboardAttachmentAuthorized = self.lastOpenAIDashboardAttachmentAuthorized
                let stamp = cached.updatedAt.formatted(date: .abbreviated, time: .shortened)
                self.lastOpenAIDashboardError =
                    "Last OpenAI dashboard refresh failed: \(message). Cached values from \(stamp)."
            } else {
                self.lastOpenAIDashboardError = message
                self.openAIDashboard = nil
                self.openAIDashboardAttachmentAuthorized = false
            }
        }
    }

    func applyOpenAIDashboardLoginRequiredFailure(
        expectedGuard: CodexAccountScopedRefreshGuard? = nil,
        refreshTaskToken: UUID? = nil,
        routingTargetEmail: String? = nil) async
    {
        guard self.shouldApplyOpenAIDashboardRefreshTask(token: refreshTaskToken) else { return }
        if let expectedGuard,
           !self.shouldApplyOpenAIWebNonSuccessResult(
               expectedGuard: expectedGuard,
               routingTargetEmail: routingTargetEmail)
        {
            return
        }
        if self.openAIWebManagedTargetStoreIsUnreadable() {
            await self.failClosedRefreshForUnreadableManagedCodexStore()
            return
        }
        if self.openAIWebManagedTargetIsMissing() {
            await self.failClosedRefreshForMissingManagedCodexTarget()
            return
        }
        if self.openAIWebProfileTargetEmailIsMissing() {
            await self.failClosedRefreshForMissingProfileCodexTarget()
            return
        }

        OpenAIDashboardFetcher.evictAllCachedWebViews()
        await MainActor.run {
            self.lastOpenAIDashboardError = [
                "OpenAI web access requires a signed-in chatgpt.com session.",
                "Sign in using \(self.codexBrowserCookieOrder.loginHint), " +
                    "then update OpenAI cookies in Providers → Codex.",
            ].joined(separator: " ")
            self.openAIDashboard = self.lastOpenAIDashboardSnapshot
            self.openAIDashboardAttachmentAuthorized = self.lastOpenAIDashboardAttachmentAuthorized
            self.openAIDashboardRequiresLogin = true
        }
    }

    private func failClosedOpenAIDashboardSnapshot() {
        self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
        self.openAIDashboardRequiresLogin = true
    }

    private func applyOpenAIDashboardAuthorityDecision(
        _ decision: CodexDashboardAuthorityDecision,
        dashboard: OpenAIDashboardSnapshot,
        authorityInput: CodexDashboardAuthorityInput,
        attachedAccountEmail: String?,
        allowCodexUsageBackfill: Bool) async
    {
        switch decision.disposition {
        case .attach:
            self.openAIDashboard = dashboard
            self.openAIDashboardAttachmentAuthorized = true
            self.lastOpenAIDashboardSnapshot = dashboard
            self.lastOpenAIDashboardAttachmentAuthorized = true
            self.lastOpenAIDashboardError = nil
            self.openAIDashboardRequiresLogin = false

            // Provider-specific by design: an authorized OpenAI dashboard attaches metadata/backfill to Codex usage.
            if let currentUsage = self.snapshots[.codex] {
                self.snapshots[.codex] = currentUsage.withSubscriptionMetadata(
                    expiresAt: dashboard.subscriptionExpiresAt,
                    renewsAt: dashboard.subscriptionRenewsAt)
            }

            if decision.allowedEffects.contains(.usageBackfill),
               allowCodexUsageBackfill,
               self.snapshots[.codex] == nil,
               let usage = dashboard.toUsageSnapshot(provider: .codex, accountEmail: attachedAccountEmail),
               CodexWeeklyResetConfirmation.initialDecision(previous: nil, initial: usage) == .publishInitial
            {
                self.snapshots[.codex] = usage
                self.errors[.codex] = nil
                self.failureGates[.codex]?.recordSuccess()
                self.lastSourceLabels[.codex] = "openai-web"
                self.lastCodexUsagePublicationGuard = self.currentCodexAccountScopedRefreshGuard(
                    preferCurrentSnapshot: true,
                    allowLastKnownLiveFallback: false)
            }

            if decision.allowedEffects.contains(.creditsAttachment),
               self.credits == nil,
               let credits = dashboard.toCreditsSnapshot()
            {
                self.credits = credits
                self.lastCreditsSnapshot = credits
                self.lastCreditsSnapshotAccountKey = Self.normalizeCodexAccountScopedKey(attachedAccountEmail)
                self.lastCreditsSource = .dashboardWeb
                self.lastCreditsError = nil
                self.creditsFailureStreak = 0
            }

            if decision.allowedEffects.contains(.refreshGuardSeed) {
                self.seedCodexAccountScopedRefreshGuard(accountEmail: attachedAccountEmail)
            }

            if let attachedAccountEmail, !attachedAccountEmail.isEmpty {
                OpenAIDashboardCacheStore.save(OpenAIDashboardCache(
                    accountEmail: attachedAccountEmail,
                    snapshot: dashboard))
            }

            if decision.allowedEffects.contains(.historicalBackfill) {
                self.backfillCodexHistoricalFromDashboardIfNeeded(
                    dashboard,
                    authorityDecision: decision,
                    attachedAccountEmail: attachedAccountEmail)
            }

        case .displayOnly:
            self.applyOpenAIDashboardCleanup(decision.cleanup, preserveVisibleDashboard: true)
            self.openAIDashboard = dashboard
            self.openAIDashboardAttachmentAuthorized = false
            self.lastOpenAIDashboardSnapshot = dashboard
            self.lastOpenAIDashboardAttachmentAuthorized = false
            self.lastOpenAIDashboardError = nil
            self.openAIDashboardRequiresLogin = false

        case .failClosed:
            self.applyOpenAIDashboardCleanup(decision.cleanup, preserveVisibleDashboard: false)
            self.lastOpenAIDashboardError = self.openAIDashboardPolicyFailureMessage(
                for: decision,
                authorityInput: authorityInput)
            self.openAIDashboardRequiresLogin = true
        }
    }

    private func applyOpenAIDashboardCleanup(
        _ cleanup: Set<CodexDashboardCleanup>,
        preserveVisibleDashboard: Bool)
    {
        if cleanup.contains(.dashboardDerivedUsage) {
            self.clearDashboardDerivedCodexUsageIfNeeded()
        }
        if cleanup.contains(.dashboardDerivedCredits) {
            self.clearDashboardDerivedCreditsIfNeeded()
        }
        if cleanup.contains(.dashboardRefreshGuardSeed) {
            self.clearDashboardRefreshGuardSeedIfNeeded()
        }
        if cleanup.contains(.dashboardCache) {
            OpenAIDashboardCacheStore.clear()
        }
        if cleanup.contains(.dashboardSnapshot), !preserveVisibleDashboard {
            self.openAIDashboard = nil
            self.openAIDashboardAttachmentAuthorized = false
            self.lastOpenAIDashboardSnapshot = nil
            self.lastOpenAIDashboardAttachmentAuthorized = false
        }
    }

    private func clearDashboardDerivedCodexUsageIfNeeded() {
        guard self.lastSourceLabels[.codex] == "openai-web" else { return }
        self.clearCodexPublishedUsageState()
    }

    private func clearDashboardDerivedCreditsIfNeeded() {
        guard self.lastCreditsSource == .dashboardWeb else { return }
        self.credits = nil
        self.lastCreditsError = nil
        self.lastCreditsSnapshot = nil
        self.lastCreditsSnapshotAccountKey = nil
        self.lastCreditsSource = .none
        self.creditsFailureStreak = 0
    }

    private func clearDashboardRefreshGuardSeedIfNeeded() {
        let currentGuard = self.currentCodexAccountScopedRefreshGuard(
            preferCurrentSnapshot: false,
            allowLastKnownLiveFallback: false)
        if self.snapshots[.codex] != nil,
           self.lastCodexUsagePublicationGuard.map({
               !Self.codexScopedRefreshGuardsMatchAccount($0, currentGuard)
           }) ?? true
        {
            self.clearCodexPublishedUsageState()
        }
        self.lastCodexAccountScopedRefreshGuard = currentGuard
    }

    private func openAIDashboardPolicyFailureMessage(
        for decision: CodexDashboardAuthorityDecision,
        authorityInput: CodexDashboardAuthorityInput) -> String
    {
        switch decision.reason {
        case let .wrongEmail(expected, actual):
            [
                "OpenAI dashboard signed in as \(actual ?? "unknown"), but Codex uses \(expected ?? "unknown").",
                "Switch accounts in your browser and update OpenAI cookies in Providers → Codex.",
            ].joined(separator: " ")
        case let .sameEmailAmbiguity(email):
            "OpenAI dashboard ownership is ambiguous for \(email); Codex will not attach dashboard data."
        case .missingDashboardSignedInEmail:
            "OpenAI dashboard did not report a signed-in account. Refresh OpenAI cookies and try again."
        case .unresolvedWithoutTrustedEvidence:
            "OpenAI dashboard ownership could not be verified for the active Codex account."
        case .providerAccountMissingScopedEmail:
            "Codex account ownership could not be verified because the scoped email is unavailable."
        case .providerAccountLacksExactOwnershipProof:
            [
                "OpenAI dashboard ownership could not be matched to the active Codex account.",
                "Refresh Codex account data, then retry OpenAI web access.",
            ].joined(separator: " ")
        case .exactProviderAccountMatch,
             .trustedEmailMatchNoCompetingOwner,
             .trustedContinuityNoCompetingOwner:
            "OpenAI dashboard ownership policy blocked this dashboard."
        }
    }

    func refreshOpenAIDashboardIfNeeded(
        force: Bool = false,
        expectedGuard: CodexAccountScopedRefreshGuard? = nil,
        bypassCoalescing: Bool = false,
        allowCodexUsageBackfill: Bool = true) async
    {
        self.syncOpenAIWebState()
        guard self.isEnabled(.codex),
              self.settings.openAIWebAccessEnabled,
              self.settings.codexCookieSource.isEnabled
        else { return }
        if self.openAIWebManagedTargetStoreIsUnreadable() {
            await self.failClosedRefreshForUnreadableManagedCodexStore()
            return
        }
        if self.openAIWebManagedTargetIsMissing() {
            await self.failClosedRefreshForMissingManagedCodexTarget()
            return
        }
        if self.openAIWebProfileTargetEmailIsMissing() {
            await self.failClosedRefreshForMissingProfileCodexTarget()
            return
        }

        let allowCurrentSnapshotFallback = expectedGuard?.source == .liveSystem && expectedGuard?
            .identity == .unresolved
        let targetEmail = self.currentCodexOpenAIWebTargetEmail(
            allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
            allowLastKnownLiveFallback: expectedGuard?.identity != .unresolved)
        let refreshKey = self.openAIDashboardRefreshKey(targetEmail: targetEmail, expectedGuard: expectedGuard)
        if !bypassCoalescing,
           let task = self.openAIDashboardRefreshTask,
           self.openAIDashboardRefreshTaskKey == refreshKey
        {
            await task.value
            return
        }
        if bypassCoalescing {
            self.openAIDashboardBackgroundRefreshTask?.cancel()
            self.openAIDashboardRefreshTask?.cancel()
        }
        self.handleOpenAIWebTargetEmailChangeIfNeeded(
            targetEmail: targetEmail,
            targetScope: self.codexCookieCacheScopeForOpenAIWeb())

        let now = Date()
        let minInterval = self.openAIWebRefreshIntervalSeconds()
        let refreshGate = OpenAIWebRefreshGateContext(
            force: force,
            accountDidChange: self.openAIWebAccountDidChange,
            lastError: self.lastOpenAIDashboardError,
            lastSnapshotAt: self.lastOpenAIDashboardSnapshot?.updatedAt,
            lastAttemptAt: self.lastOpenAIDashboardAttemptAt,
            now: now,
            refreshInterval: minInterval)
        if Self.shouldSkipOpenAIWebRefresh(refreshGate) {
            return
        }
        self.lastOpenAIDashboardAttemptAt = now

        let taskToken = UUID()
        let context = OpenAIDashboardRefreshContext(
            targetEmail: targetEmail,
            allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
            expectedGuard: expectedGuard,
            refreshTaskToken: taskToken,
            allowCodexUsageBackfill: allowCodexUsageBackfill,
            force: force)
        let task = Task { [weak self] in
            guard let self else { return }
            await self.performOpenAIDashboardRefreshIfNeeded(context)
        }
        self.openAIDashboardRefreshTask = task
        self.openAIDashboardRefreshTaskKey = refreshKey
        self.openAIDashboardRefreshTaskToken = taskToken
        await task.value
        if self.openAIDashboardRefreshTaskToken == taskToken {
            self.openAIDashboardRefreshTask = nil
            self.openAIDashboardRefreshTaskKey = nil
            self.openAIDashboardRefreshTaskToken = nil
        }
    }

    func scheduleOpenAIDashboardRefreshIfNeeded(expectedGuard: CodexAccountScopedRefreshGuard? = nil) {
        self.syncOpenAIWebState()
        let allowCurrentSnapshotFallback = expectedGuard?.source == .liveSystem && expectedGuard?
            .identity == .unresolved
        let targetEmail = self.currentCodexOpenAIWebTargetEmail(
            allowCurrentSnapshotFallback: allowCurrentSnapshotFallback,
            allowLastKnownLiveFallback: expectedGuard?.identity != .unresolved)
        let refreshKey = self.openAIDashboardRefreshKey(targetEmail: targetEmail, expectedGuard: expectedGuard)
        if let task = self.openAIDashboardBackgroundRefreshTask,
           !task.isCancelled,
           self.openAIDashboardBackgroundRefreshTaskKey == refreshKey
        {
            return
        }

        if self.openAIDashboardBackgroundRefreshTaskKey != nil,
           self.openAIDashboardBackgroundRefreshTaskKey != refreshKey
        {
            self.invalidateOpenAIDashboardRefreshTask()
        }
        self.openAIDashboardBackgroundRefreshTask?.cancel()
        self.openAIDashboardBackgroundRefreshTaskKey = refreshKey
        self.openAIDashboardBackgroundRefreshTask = Task(priority: .utility) { @MainActor [weak self] in
            guard let self else { return }
            defer {
                if self.openAIDashboardBackgroundRefreshTaskKey == refreshKey {
                    self.openAIDashboardBackgroundRefreshTask = nil
                    self.openAIDashboardBackgroundRefreshTaskKey = nil
                }
            }

            guard !Task.isCancelled else { return }
            await self.refreshOpenAIDashboardIfNeeded(force: false, expectedGuard: expectedGuard)
            guard !Task.isCancelled else { return }
            self.persistWidgetSnapshot(reason: "dashboard")
        }
    }

    private func performOpenAIDashboardRefreshIfNeeded(_ context: OpenAIDashboardRefreshContext) async {
        guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
        self.openAIDashboardCookieImportStatus = nil
        var latestCookieImportStatus: String?
        if self.openAIWebDebugLines.isEmpty {
            self.resetOpenAIWebDebugLog(context: "refresh")
        } else {
            let stamp = Date().formatted(date: .abbreviated, time: .shortened)
            self.logOpenAIWeb("[\(stamp)] OpenAI web refresh start")
        }
        let log: (String) -> Void = { [weak self] line in
            guard let self else { return }
            self.logOpenAIWeb(line)
        }

        do {
            let normalized = context.targetEmail?
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .lowercased()
            var effectiveEmail = context.targetEmail

            // Use a per-email persistent `WKWebsiteDataStore` so multiple dashboard sessions can coexist.
            // Strategy:
            // - Try the existing per-email WebKit cookie store first (fast; avoids Keychain prompts).
            // - On login-required or account mismatch, import cookies from the configured browser order and retry once.
            var didImportCookiesForRefresh = false
            if self.openAIWebAccountDidChange, let targetEmail = context.targetEmail, !targetEmail.isEmpty {
                // On account switches, proactively re-import cookies so we don't show stale data from the previous
                // user.
                let imported = await self.importOpenAIDashboardCookiesIfNeeded(
                    targetEmail: targetEmail,
                    force: true)
                guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
                didImportCookiesForRefresh = true
                latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
                if await self.abortOpenAIDashboardRetryAfterImportFailure(
                    importedEmail: imported,
                    targetEmail: targetEmail,
                    expectedGuard: context.expectedGuard,
                    cookieImportStatus: latestCookieImportStatus,
                    refreshTaskToken: context.refreshTaskToken)
                {
                    self.openAIWebAccountDidChange = false
                    return
                }
                if let imported {
                    effectiveEmail = imported
                }
                self.openAIWebAccountDidChange = false
            }

            var dash = try await self.loadLatestOpenAIDashboard(
                accountEmail: effectiveEmail,
                logger: log,
                allowNavigationTimeoutRetry: context.force,
                timeout: Self.openAIWebDashboardFetchTimeout(didImportCookies: didImportCookiesForRefresh),
                force: context.force)
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }

            if self.dashboardEmailMismatch(expected: normalized, actual: dash.signedInEmail) {
                if let imported = await self.importOpenAIDashboardCookiesIfNeeded(
                    targetEmail: context.targetEmail,
                    force: true)
                {
                    guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
                    effectiveEmail = imported
                }
                latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
                dash = try await self.loadLatestOpenAIDashboard(
                    accountEmail: effectiveEmail,
                    logger: log,
                    allowNavigationTimeoutRetry: context.force,
                    timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true),
                    force: context.force)
                guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            }

            await self.applyOpenAIDashboard(
                dash,
                targetEmail: effectiveEmail,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                allowCodexUsageBackfill: context.allowCodexUsageBackfill)
        } catch let OpenAIDashboardFetcher.FetchError.noDashboardData(body) {
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            await self.retryOpenAIDashboardAfterNoData(
                body: body,
                context: context,
                latestCookieImportStatus: &latestCookieImportStatus,
                logger: log)
        } catch OpenAIDashboardFetcher.FetchError.loginRequired {
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            await self.retryOpenAIDashboardAfterLoginRequired(
                context: context,
                latestCookieImportStatus: &latestCookieImportStatus,
                logger: log)
        } catch {
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            if Self.isOpenAIDashboardTimeout(error) {
                await self.retryOpenAIDashboardAfterTimeout(
                    context: context,
                    latestCookieImportStatus: &latestCookieImportStatus,
                    logger: log)
                return
            }
            let message = self.preferredOpenAIDashboardFailureMessage(
                error: error,
                targetEmail: context.targetEmail,
                cookieImportStatus: latestCookieImportStatus)
            await self.applyOpenAIDashboardFailure(
                message: message,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                routingTargetEmail: context.targetEmail)
        }

        OpenAIDashboardFetcher.evictIdleCachedWebViews()
    }

    private func retryOpenAIDashboardAfterTimeout(
        context: OpenAIDashboardRefreshContext,
        latestCookieImportStatus: inout String?,
        logger: @escaping (String) -> Void) async
    {
        if !context.force {
            OpenAIDashboardFetcher.evictAllCachedWebViews()
            logger("OpenAI web refresh timed out; skipping immediate background retry.")
            await self.applyOpenAIDashboardFailure(
                message: L(
                    "OpenAI web dashboard refresh timed out. CodexBar will retry after the refresh cooldown."),
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                routingTargetEmail: context.targetEmail)
            return
        }

        let targetEmail = self.currentCodexOpenAIWebTargetEmail(
            allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
            allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
        var effectiveEmail = targetEmail
        let imported = await self.importOpenAIDashboardCookiesIfNeeded(
            targetEmail: targetEmail,
            force: true,
            preferCachedCookieHeader: true)
        guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
        latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
        if await self.abortOpenAIDashboardRetryAfterImportFailure(
            importedEmail: imported,
            targetEmail: targetEmail,
            expectedGuard: context.expectedGuard,
            cookieImportStatus: latestCookieImportStatus,
            refreshTaskToken: context.refreshTaskToken)
        {
            return
        }
        if let imported {
            effectiveEmail = imported
        }
        do {
            let dash = try await self.loadLatestOpenAIDashboard(
                accountEmail: effectiveEmail,
                logger: logger,
                allowNavigationTimeoutRetry: context.force,
                timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true),
                force: context.force)
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            await self.applyOpenAIDashboard(
                dash,
                targetEmail: effectiveEmail,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                allowCodexUsageBackfill: context.allowCodexUsageBackfill)
        } catch {
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            let message = self.preferredOpenAIDashboardFailureMessage(
                error: error,
                targetEmail: targetEmail,
                cookieImportStatus: latestCookieImportStatus)
            await self.applyOpenAIDashboardFailure(
                message: message,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                routingTargetEmail: targetEmail)
        }
    }

    private func retryOpenAIDashboardAfterNoData(
        body: String,
        context: OpenAIDashboardRefreshContext,
        latestCookieImportStatus: inout String?,
        logger: @escaping (String) -> Void) async
    {
        let targetEmail = self.currentCodexOpenAIWebTargetEmail(
            allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
            allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
        var effectiveEmail = targetEmail
        let imported = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
        guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
        latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
        if await self.abortOpenAIDashboardRetryAfterImportFailure(
            importedEmail: imported,
            targetEmail: targetEmail,
            expectedGuard: context.expectedGuard,
            cookieImportStatus: latestCookieImportStatus,
            refreshTaskToken: context.refreshTaskToken)
        {
            return
        }
        if let imported {
            effectiveEmail = imported
        }
        do {
            let dash = try await self.loadLatestOpenAIDashboard(
                accountEmail: effectiveEmail,
                logger: logger,
                allowNavigationTimeoutRetry: context.force,
                timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true),
                force: context.force)
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            await self.applyOpenAIDashboard(
                dash,
                targetEmail: effectiveEmail,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                allowCodexUsageBackfill: context.allowCodexUsageBackfill)
        } catch let OpenAIDashboardFetcher.FetchError.noDashboardData(retryBody) {
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            let finalBody = retryBody.isEmpty ? body : retryBody
            let message = self.openAIDashboardFriendlyError(
                body: finalBody,
                targetEmail: targetEmail,
                cookieImportStatus: latestCookieImportStatus)
                ?? OpenAIDashboardFetcher.FetchError.noDashboardData(body: finalBody).localizedDescription
            await self.applyOpenAIDashboardFailure(
                message: message,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                routingTargetEmail: targetEmail)
        } catch {
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            let message = self.preferredOpenAIDashboardFailureMessage(
                error: error,
                targetEmail: targetEmail,
                cookieImportStatus: latestCookieImportStatus)
            await self.applyOpenAIDashboardFailure(
                message: message,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                routingTargetEmail: targetEmail)
        }
    }

    private func retryOpenAIDashboardAfterLoginRequired(
        context: OpenAIDashboardRefreshContext,
        latestCookieImportStatus: inout String?,
        logger: @escaping (String) -> Void) async
    {
        let targetEmail = self.currentCodexOpenAIWebTargetEmail(
            allowCurrentSnapshotFallback: context.allowCurrentSnapshotFallback,
            allowLastKnownLiveFallback: context.expectedGuard?.identity != .unresolved)
        var effectiveEmail = targetEmail
        let imported = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
        guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
        latestCookieImportStatus = self.currentOpenAIDashboardCookieImportStatus()
        if await self.abortOpenAIDashboardRetryAfterImportFailure(
            importedEmail: imported,
            targetEmail: targetEmail,
            expectedGuard: context.expectedGuard,
            cookieImportStatus: latestCookieImportStatus,
            refreshTaskToken: context.refreshTaskToken)
        {
            return
        }
        if let imported {
            effectiveEmail = imported
        }
        do {
            let dash = try await self.loadLatestOpenAIDashboard(
                accountEmail: effectiveEmail,
                logger: logger,
                allowNavigationTimeoutRetry: context.force,
                timeout: Self.openAIWebRetryDashboardFetchTimeout(afterCookieImport: true),
                force: context.force)
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            await self.applyOpenAIDashboard(
                dash,
                targetEmail: effectiveEmail,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                allowCodexUsageBackfill: context.allowCodexUsageBackfill)
        } catch OpenAIDashboardFetcher.FetchError.loginRequired {
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            await self.applyOpenAIDashboardLoginRequiredFailure(
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                routingTargetEmail: targetEmail)
        } catch {
            guard self.shouldContinueOpenAIDashboardRefresh(token: context.refreshTaskToken) else { return }
            let message = self.preferredOpenAIDashboardFailureMessage(
                error: error,
                targetEmail: targetEmail,
                cookieImportStatus: latestCookieImportStatus)
            await self.applyOpenAIDashboardFailure(
                message: message,
                expectedGuard: context.expectedGuard,
                refreshTaskToken: context.refreshTaskToken,
                routingTargetEmail: targetEmail)
        }
    }

    // MARK: - OpenAI web account switching

    /// Detect Codex account-source changes and clear stale OpenAI web state so the UI can't show the wrong user.
    /// This does not delete other isolated WebKit cookie stores (we keep multiple accounts around).
    func handleOpenAIWebTargetEmailChangeIfNeeded(
        targetEmail: String?,
        targetScope: CookieHeaderCache.Scope? = nil)
    {
        let normalized = targetEmail?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .lowercased()

        guard let normalized, !normalized.isEmpty else { return }

        let isolationKey = Self.openAIWebTargetIsolationKey(email: normalized, scope: targetScope)
        let previousIsolationKey = self.lastOpenAIDashboardTargetIsolationKey
        self.lastOpenAIDashboardTargetEmail = normalized
        self.lastOpenAIDashboardTargetIsolationKey = isolationKey

        if let previousIsolationKey,
           previousIsolationKey != isolationKey
        {
            let stamp = Date().formatted(date: .abbreviated, time: .shortened)
            self.logOpenAIWeb(
                "[\(stamp)] Codex account source changed; clearing OpenAI web snapshot")
            self.openAIWebAccountDidChange = true
            self.openAIDashboard = nil
            self.openAIDashboardAttachmentAuthorized = false
            self.lastOpenAIDashboardSnapshot = nil
            self.lastOpenAIDashboardAttachmentAuthorized = false
            self.lastOpenAIDashboardError = nil
            self.lastOpenAIDashboardAttemptAt = nil
            self.lastOpenAIDashboardPageScrapeAt = nil
            self.openAIDashboardRequiresLogin = true
            self.openAIDashboardCookieImportStatus = L("Codex account changed; importing browser cookies…")
            self.lastOpenAIDashboardCookieImportAttemptAt = nil
            self.lastOpenAIDashboardCookieImportEmail = nil
        }
    }

    nonisolated static func openAIWebTargetIsolationKey(
        email: String,
        scope: CookieHeaderCache.Scope?) -> String
    {
        "\(email.lowercased())|\(scope?.isolationIdentifier ?? "live")"
    }

    func importOpenAIDashboardBrowserCookiesNow() async {
        self.resetOpenAIWebDebugLog(context: "manual import")
        let targetEmail = self.currentCodexOpenAIWebTargetEmail(
            allowCurrentSnapshotFallback: true,
            allowLastKnownLiveFallback: false)
        _ = await self.importOpenAIDashboardCookiesIfNeeded(targetEmail: targetEmail, force: true)
        let expectedGuard = self.freshCodexOpenAIWebRefreshGuard()
        await self.refreshOpenAIDashboardIfNeeded(
            force: true,
            expectedGuard: expectedGuard,
            bypassCoalescing: true)
    }

    func currentCodexOpenAIWebTargetEmail(
        allowCurrentSnapshotFallback: Bool,
        allowLastKnownLiveFallback: Bool) -> String?
    {
        switch self.settings.codexResolvedActiveSource {
        case .liveSystem:
            let liveSystem = self.settings.codexAccountReconciliationSnapshot.liveSystemAccount?.email
                .trimmingCharacters(in: .whitespacesAndNewlines)
            if let liveSystem, !liveSystem.isEmpty {
                self.lastKnownLiveSystemCodexEmail = liveSystem
                return liveSystem
            }

            if allowCurrentSnapshotFallback,
               // Provider-specific by design: OpenAI web attachment falls back to the current Codex owner email.
               let snapshotEmail = self.snapshots[.codex]?.accountEmail(for: .codex)?
                   .trimmingCharacters(in: .whitespacesAndNewlines),
                   !snapshotEmail.isEmpty
            {
                self.lastKnownLiveSystemCodexEmail = snapshotEmail
                return snapshotEmail
            }

            if allowLastKnownLiveFallback {
                let lastKnown = self.lastKnownLiveSystemCodexEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
                if let lastKnown, !lastKnown.isEmpty {
                    return lastKnown
                }
            }
            return nil
        case .managedAccount:
            return self.codexAccountEmailForOpenAIDashboard()
        case let .profileHome(path):
            return self.currentProfileCodexRuntimeEmail(path: path)
        }
    }

    private func openAIDashboardRefreshKey(
        targetEmail: String?,
        expectedGuard: CodexAccountScopedRefreshGuard?) -> String
    {
        let source = String(describing: expectedGuard?.source ?? self.settings.codexResolvedActiveSource)
        let identityKey = Self.codexIdentityGuardKey(expectedGuard?.identity ?? .unresolved) ?? "unresolved"
        let accountKey = Self.normalizeCodexAccountScopedKey(targetEmail) ?? "unknown"
        let authFingerprint = CodexAuthFingerprint.normalize(expectedGuard?.authFingerprint) ?? "nil"
        return "\(source)|\(identityKey)|\(accountKey)|auth:\(authFingerprint)"
    }

    private func actionableOpenAIDashboardImportFailure(targetEmail: String?) -> String? {
        self.actionableOpenAIDashboardImportFailure(
            targetEmail: targetEmail,
            cookieImportStatus: self.openAIDashboardCookieImportStatus)
    }

    private func actionableOpenAIDashboardImportFailure(
        targetEmail: String?,
        cookieImportStatus: String?) -> String?
    {
        let status = cookieImportStatus?.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let status, !status.isEmpty else { return nil }

        if status.localizedCaseInsensitiveContains("openai cookies are for") {
            return "\(status) Switch chatgpt.com account, then refresh OpenAI cookies."
        }
        if status.localizedCaseInsensitiveContains("no signed-in openai web session found")
            || status.localizedCaseInsensitiveContains("no matching openai web session found")
        {
            let targetLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
            let accountLabel = (targetLabel?.isEmpty == false) ? targetLabel! : "your OpenAI account"
            return "\(status) Sign in to chatgpt.com as \(accountLabel), then refresh OpenAI cookies."
        }
        if status.localizedCaseInsensitiveContains("openai cookie import failed")
            || status.localizedCaseInsensitiveContains("browser cookie import failed")
        {
            return status
        }
        return nil
    }

    private func preferredOpenAIDashboardFailureMessage(
        error: Error,
        targetEmail: String?,
        cookieImportStatus: String?) -> String
    {
        if let actionable = self.actionableOpenAIDashboardImportFailure(
            targetEmail: targetEmail,
            cookieImportStatus: cookieImportStatus)
        {
            return actionable
        }
        return error.localizedDescription
    }

    private static func isOpenAIDashboardTimeout(_ error: Error) -> Bool {
        let nsError = error as NSError
        return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut
    }

    private func abortOpenAIDashboardRetryAfterImportFailure(
        importedEmail: String?,
        targetEmail: String?,
        expectedGuard: CodexAccountScopedRefreshGuard?,
        cookieImportStatus: String?,
        refreshTaskToken: UUID) async -> Bool
    {
        guard importedEmail == nil,
              let message = self.actionableOpenAIDashboardImportFailure(
                  targetEmail: targetEmail,
                  cookieImportStatus: cookieImportStatus)
        else {
            return false
        }
        await self.applyOpenAIDashboardFailure(
            message: message,
            expectedGuard: expectedGuard,
            refreshTaskToken: refreshTaskToken,
            routingTargetEmail: targetEmail)
        return true
    }

    private func shouldApplyOpenAIDashboardRefreshTask(token: UUID?) -> Bool {
        guard let token else { return true }
        return self.openAIDashboardRefreshTaskToken == token
    }

    private func shouldContinueOpenAIDashboardRefresh(token: UUID?) -> Bool {
        !Task.isCancelled && self.shouldApplyOpenAIDashboardRefreshTask(token: token)
    }

    func invalidateOpenAIDashboardRefreshTask() {
        self.openAIDashboardBackgroundRefreshTask?.cancel()
        self.openAIDashboardBackgroundRefreshTask = nil
        self.openAIDashboardBackgroundRefreshTaskKey = nil
        self.openAIDashboardRefreshTask?.cancel()
        self.openAIDashboardRefreshTask = nil
        self.openAIDashboardRefreshTaskKey = nil
        self.openAIDashboardRefreshTaskToken = nil
    }

    private func currentOpenAIDashboardCookieImportStatus() -> String? {
        self.openAIDashboardCookieImportStatus
    }

    private func loadLatestOpenAIDashboard(
        accountEmail: String?,
        logger: @escaping (String) -> Void,
        allowNavigationTimeoutRetry: Bool,
        timeout: TimeInterval,
        force: Bool) async throws -> OpenAIDashboardSnapshot
    {
        if let override = self._test_openAIDashboardLoaderOverride {
            return try await override(accountEmail, logger, allowNavigationTimeoutRetry, timeout)
        }
        let previousSnapshot = self.openAIDashboard ?? self.lastOpenAIDashboardSnapshot
        let allowPageScrape = Self.shouldAllowOpenAIDashboardPageScrape(.init(
            force: force,
            userInitiated: ProviderInteractionContext.current == .userInitiated,
            hasHistory: Self.dashboardHasPageHistory(previousSnapshot),
            lastPageScrapeAt: self.lastOpenAIDashboardPageScrapeAt,
            historyUpdatedAt: previousSnapshot?.updatedAt,
            now: Date(),
            refreshInterval: self.openAIWebRefreshIntervalSeconds()))
        logger("dashboard page scrape=\(allowPageScrape ? "1" : "0")")
        let snapshot = try await OpenAIDashboardFetcher().loadLatestDashboard(
            accountEmail: accountEmail,
            cacheScope: self.codexCookieCacheScopeForOpenAIWeb(),
            logger: logger,
            debugDumpHTML: timeout != Self.openAIWebPrimaryFetchTimeout,
            allowNavigationTimeoutRetry: allowNavigationTimeoutRetry,
            timeout: timeout,
            previousSnapshot: previousSnapshot,
            allowPageScrape: allowPageScrape)
        if allowPageScrape, Self.dashboardHasPageHistory(snapshot) {
            self.lastOpenAIDashboardPageScrapeAt = Date()
        }
        return snapshot
    }

    private func failClosedForUnreadableManagedCodexStore() async -> String? {
        self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
        self.openAIDashboardRequiresLogin = true
        self.openAIDashboardCookieImportStatus = [
            L("Managed Codex account data is unavailable."),
            L("Fix the managed account store before importing OpenAI cookies."),
        ].joined(separator: " ")
        return nil
    }

    private func failClosedRefreshForUnreadableManagedCodexStore() async {
        self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
        self.openAIDashboardRequiresLogin = true
        self.lastOpenAIDashboardError = [
            L("Managed Codex account data is unavailable."),
            L("Fix the managed account store before refreshing OpenAI web data."),
        ].joined(separator: " ")
    }

    private func failClosedForMissingManagedCodexTarget() async -> String? {
        self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
        self.openAIDashboardRequiresLogin = true
        self.openAIDashboardCookieImportStatus = [
            L("The selected managed Codex account is unavailable."),
            L("Pick another Codex account before importing OpenAI cookies."),
        ].joined(separator: " ")
        return nil
    }

    private func failClosedRefreshForMissingManagedCodexTarget() async {
        self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
        self.openAIDashboardRequiresLogin = true
        self.lastOpenAIDashboardError = [
            L("The selected managed Codex account is unavailable."),
            L("Pick another Codex account before refreshing OpenAI web data."),
        ].joined(separator: " ")
    }

    private func failClosedForMissingProfileCodexTarget() async -> String? {
        self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
        self.openAIDashboardRequiresLogin = true
        self.openAIDashboardCookieImportStatus = [
            L("The selected Codex profile has no verified account email."),
            L("Refresh the profile before importing OpenAI cookies."),
        ].joined(separator: " ")
        return nil
    }

    private func failClosedRefreshForMissingProfileCodexTarget() async {
        self.applyOpenAIDashboardCleanup(Set(CodexDashboardCleanup.allCases), preserveVisibleDashboard: false)
        self.openAIDashboardRequiresLogin = true
        self.lastOpenAIDashboardError = [
            L("The selected Codex profile has no verified account email."),
            L("Refresh the profile before refreshing OpenAI web data."),
        ].joined(separator: " ")
    }

    private func openAIWebCookieImportShouldFailClosed() async -> Bool {
        if self.openAIWebManagedTargetStoreIsUnreadable() {
            _ = await self.failClosedForUnreadableManagedCodexStore()
            return true
        }
        if self.openAIWebManagedTargetIsMissing() {
            _ = await self.failClosedForMissingManagedCodexTarget()
            return true
        }
        if self.openAIWebProfileTargetEmailIsMissing() {
            _ = await self.failClosedForMissingProfileCodexTarget()
            return true
        }
        return false
    }

    private func openAIDashboardCookieImportResult(
        request: OpenAIDashboardCookieImportRequest,
        logger: @escaping (String) -> Void) async throws -> OpenAIDashboardBrowserCookieImporter.ImportResult
    {
        if let override = self._test_openAIDashboardCookieImportOverride {
            return try await override(
                request.normalizedTarget,
                request.allowAnyAccount,
                request.cookieSource,
                request.cacheScope,
                logger)
        }

        let importer = OpenAIDashboardBrowserCookieImporter(browserDetection: self.browserDetection)
        switch request.cookieSource {
        case .manual:
            self.settings.ensureCodexCookieLoaded()
            // Manual OpenAI cookies still come from one provider-level setting. Auto-imported cookies are
            // isolated per managed account, but a manual header is an explicit override owned by settings,
            // so switching managed accounts does not currently swap it underneath the user.
            let manualHeader = self.settings.codexCookieHeader
            guard CookieHeaderNormalizer.normalize(manualHeader) != nil else {
                throw OpenAIDashboardBrowserCookieImporter.ImportError.manualCookieHeaderInvalid
            }
            return try await importer.importManualCookies(
                cookieHeader: manualHeader,
                intoAccountEmail: request.normalizedTarget,
                allowAnyAccount: request.allowAnyAccount,
                cacheScope: request.cacheScope,
                logger: logger)
        case .auto:
            return try await importer.importBestCookies(
                intoAccountEmail: request.normalizedTarget,
                allowAnyAccount: request.allowAnyAccount,
                preferCachedCookieHeader: request.preferCachedCookieHeader ?? !request.force,
                cacheScope: request.cacheScope,
                logger: logger)
        case .off:
            return OpenAIDashboardBrowserCookieImporter.ImportResult(
                sourceLabel: "Off",
                cookieCount: 0,
                signedInEmail: request.normalizedTarget,
                matchesCodexEmail: true)
        }
    }

    func importOpenAIDashboardCookiesIfNeeded(
        targetEmail: String?,
        force: Bool,
        preferCachedCookieHeader: Bool? = nil) async -> String?
    {
        guard !Task.isCancelled else { return nil }
        if await self.openAIWebCookieImportShouldFailClosed() {
            return nil
        }
        guard !Task.isCancelled else { return nil }

        let normalizedTarget = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
        let allowAnyAccount = normalizedTarget == nil || normalizedTarget?.isEmpty == true
        let cookieSource = self.settings.codexCookieSource
        let cacheScope = self.codexCookieCacheScopeForOpenAIWeb()

        let now = Date()
        let lastEmail = self.lastOpenAIDashboardCookieImportEmail
        let lastAttempt = self.lastOpenAIDashboardCookieImportAttemptAt ?? .distantPast

        let shouldAttempt: Bool = if force {
            true
        } else {
            if allowAnyAccount {
                now.timeIntervalSince(lastAttempt) > 300
            } else {
                self.openAIDashboardRequiresLogin &&
                    (
                        lastEmail?.lowercased() != normalizedTarget?.lowercased() || now
                            .timeIntervalSince(lastAttempt) > 300)
            }
        }

        guard shouldAttempt else { return normalizedTarget }
        self.lastOpenAIDashboardCookieImportEmail = normalizedTarget
        self.lastOpenAIDashboardCookieImportAttemptAt = now

        let stamp = now.formatted(date: .abbreviated, time: .shortened)
        let targetLabel = normalizedTarget ?? "unknown"
        self.logOpenAIWeb("[\(stamp)] import start (target=\(targetLabel))")

        do {
            let log: (String) -> Void = { [weak self] message in
                guard let self else { return }
                self.logOpenAIWeb(message)
            }

            let request = OpenAIDashboardCookieImportRequest(
                normalizedTarget: normalizedTarget,
                allowAnyAccount: allowAnyAccount,
                cookieSource: cookieSource,
                cacheScope: cacheScope,
                preferCachedCookieHeader: preferCachedCookieHeader,
                force: force)
            let result = try await self.openAIDashboardCookieImportResult(
                request: request,
                logger: log)
            guard !Task.isCancelled else { return nil }
            let effectiveEmail = result.signedInEmail?
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .isEmpty == false
                ? result.signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
                : normalizedTarget
            self.lastOpenAIDashboardCookieImportEmail = effectiveEmail ?? normalizedTarget
            await MainActor.run {
                let signed = result.signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
                let matchText = result.matchesCodexEmail ? "matches Codex" : "does not match Codex"
                let sourceLabel = switch cookieSource {
                case .manual:
                    "Manual cookie header"
                case .auto:
                    "\(result.sourceLabel) cookies"
                case .off:
                    "OpenAI cookies disabled"
                }
                if let signed, !signed.isEmpty {
                    self.openAIDashboardCookieImportStatus =
                        allowAnyAccount
                            ? [
                                "Using \(sourceLabel) (\(result.cookieCount)).",
                                "Signed in as \(signed).",
                            ].joined(separator: " ")
                            : [
                                "Using \(sourceLabel) (\(result.cookieCount)).",
                                "Signed in as \(signed) (\(matchText)).",
                            ].joined(separator: " ")
                } else {
                    self.openAIDashboardCookieImportStatus =
                        "Using \(sourceLabel) (\(result.cookieCount))."
                }
            }
            return effectiveEmail
        } catch let err as OpenAIDashboardBrowserCookieImporter.ImportError {
            guard !Task.isCancelled else { return nil }
            switch err {
            case let .noMatchingAccount(found):
                let foundText: String = if found.isEmpty {
                    "no signed-in session detected in \(self.codexBrowserCookieOrder.loginHint)"
                } else {
                    found
                        .sorted { lhs, rhs in
                            if lhs.sourceLabel == rhs.sourceLabel {
                                return lhs.email < rhs.email
                            }
                            return lhs.sourceLabel < rhs.sourceLabel
                        }
                        .map { "\($0.sourceLabel): \($0.email)" }
                        .joined(separator: " • ")
                }
                self.logOpenAIWeb("[\(stamp)] import mismatch: \(foundText)")
                await MainActor.run {
                    self.openAIDashboardCookieImportStatus = allowAnyAccount
                        ? [
                            "No signed-in OpenAI web session found.",
                            "Found \(foundText).",
                        ].joined(separator: " ")
                        : Self.conciseOpenAICookieMismatchStatus(
                            found: found.map(\.email),
                            targetEmail: normalizedTarget)
                    self.failClosedOpenAIDashboardSnapshot()
                }
            case .noCookiesFound,
                 .browserAccessDenied,
                 .browserCookieLoadTimedOut,
                 .dashboardStillRequiresLogin,
                 .manualCookieHeaderInvalid:
                self.logOpenAIWeb("[\(stamp)] import failed: \(err.localizedDescription)")
                await MainActor.run {
                    self.openAIDashboardCookieImportStatus =
                        "OpenAI cookie import failed: \(err.localizedDescription)"
                    self.openAIDashboardRequiresLogin = true
                }
            }
        } catch {
            guard !Task.isCancelled else { return nil }
            self.logOpenAIWeb("[\(stamp)] import failed: \(error.localizedDescription)")
            await MainActor.run {
                self.openAIDashboardCookieImportStatus =
                    "Browser cookie import failed: \(error.localizedDescription)"
            }
        }
        return nil
    }

    private func resetOpenAIWebDebugLog(context: String) {
        let stamp = Date().formatted(date: .abbreviated, time: .shortened)
        self.openAIWebDebugLines.removeAll(keepingCapacity: true)
        self.openAIDashboardCookieImportDebugLog = nil
        self.logOpenAIWeb("[\(stamp)] OpenAI web \(context) start")
    }

    private func logOpenAIWeb(_ message: String) {
        let safeMessage = LogRedactor.redact(message)
        self.openAIWebLogger.debug(safeMessage)
        self.openAIWebDebugLines.append(safeMessage)
        if self.openAIWebDebugLines.count > 240 {
            self.openAIWebDebugLines.removeFirst(self.openAIWebDebugLines.count - 240)
        }
        self.openAIDashboardCookieImportDebugLog = self.openAIWebDebugLines.joined(separator: "\n")
    }

    func resetOpenAIWebState() {
        self.invalidateOpenAIDashboardRefreshTask()
        OpenAIDashboardFetcher.evictAllCachedWebViews()
        self.openAIDashboard = nil
        self.openAIDashboardAttachmentAuthorized = false
        self.lastOpenAIDashboardError = nil
        self.lastOpenAIDashboardSnapshot = nil
        self.lastOpenAIDashboardAttachmentAuthorized = false
        self.lastOpenAIDashboardTargetEmail = nil
        self.lastOpenAIDashboardTargetIsolationKey = nil
        self.lastOpenAIDashboardAttemptAt = nil
        self.lastOpenAIDashboardPageScrapeAt = nil
        self.openAIDashboardRequiresLogin = false
        self.openAIDashboardCookieImportStatus = nil
        self.openAIDashboardCookieImportDebugLog = nil
        self.lastOpenAIDashboardCookieImportAttemptAt = nil
        self.lastOpenAIDashboardCookieImportEmail = nil
        self.lastKnownLiveSystemCodexEmail = nil
    }

    /// Routing-only optimization: this detects whether the fetched browser session appears to be for a
    /// different account than the route target, so we can retry after cookie import. Ownership proof
    /// happens exclusively through CodexDashboardAuthority.
    private func dashboardEmailMismatch(expected: String?, actual: String?) -> Bool {
        guard let expected, !expected.isEmpty else { return false }
        guard let raw = actual?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return false }
        return raw.lowercased() != expected.lowercased()
    }

    private func openAIWebManagedTargetStoreIsUnreadable() -> Bool {
        guard case .managedAccount = self.settings.codexResolvedActiveSource else {
            return false
        }
        return self.settings.codexSettingsSnapshot(tokenOverride: nil).managedAccountStoreUnreadable
    }

    private func openAIWebManagedTargetIsMissing() -> Bool {
        guard case .managedAccount = self.settings.codexResolvedActiveSource else {
            return false
        }
        return self.selectedManagedCodexAccountForOpenAIWeb() == nil
    }

    private func openAIWebProfileTargetEmailIsMissing() -> Bool {
        guard case let .profileHome(path) = self.settings.codexResolvedActiveSource else {
            return false
        }
        return self.currentProfileCodexRuntimeEmail(path: path) == nil
    }

    private func selectedManagedCodexAccountForOpenAIWeb() -> ManagedCodexAccount? {
        guard case let .managedAccount(id) = self.settings.codexResolvedActiveSource else {
            return nil
        }

        let snapshot = self.settings.codexAccountReconciliationSnapshot
        return snapshot.storedAccounts.first { $0.id == id }
    }

    func codexAccountEmailForOpenAIDashboard(allowLastKnownLiveFallback: Bool = true) -> String? {
        switch self.settings.codexResolvedActiveSource {
        case .liveSystem:
            let liveSystem = self.settings.codexAccountReconciliationSnapshot.liveSystemAccount?.email
                .trimmingCharacters(in: .whitespacesAndNewlines)
            if let liveSystem, !liveSystem.isEmpty {
                self.lastKnownLiveSystemCodexEmail = liveSystem
                return liveSystem
            }

            guard allowLastKnownLiveFallback else { return nil }
            let lastKnown = self.lastKnownLiveSystemCodexEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
            if let lastKnown, !lastKnown.isEmpty {
                return lastKnown
            }
            return nil
        case .managedAccount:
            if self.openAIWebManagedTargetStoreIsUnreadable() {
                return nil
            }

            let managed = self.currentManagedCodexRuntimeEmail()?
                .trimmingCharacters(in: .whitespacesAndNewlines)
            if let managed, !managed.isEmpty {
                return managed
            }
            return nil
        case let .profileHome(path):
            let profile = self.currentProfileCodexRuntimeEmail(path: path)?
                .trimmingCharacters(in: .whitespacesAndNewlines)
            if let profile, !profile.isEmpty {
                return profile
            }
            return nil
        }
    }

    func codexCookieCacheScopeForOpenAIWeb() -> CookieHeaderCache.Scope? {
        switch self.settings.codexResolvedActiveSource {
        case .liveSystem:
            nil
        case let .managedAccount(id):
            self.openAIWebManagedTargetStoreIsUnreadable() ? .managedStoreUnreadable : .managedAccount(id)
        case let .profileHome(path):
            .profileHome(path)
        }
    }
}

// MARK: - OpenAI web error messaging

extension UsageStore {
    nonisolated static func shouldRunOpenAIWebRefresh(_ context: OpenAIWebRefreshPolicyContext) -> Bool {
        guard context.accessEnabled else { return false }
        guard context.force || context.refreshPhase != .startup else { return false }
        return context.force || !context.batterySaverEnabled
    }

    nonisolated static func forceOpenAIWebRefreshForStaleRequest(batterySaverEnabled: Bool) -> Bool {
        !batterySaverEnabled
    }

    nonisolated static func shouldSkipOpenAIWebRefresh(_ context: OpenAIWebRefreshGateContext) -> Bool {
        if context.force || context.accountDidChange {
            return false
        }
        if let lastAttemptAt = context.lastAttemptAt,
           context.now.timeIntervalSince(lastAttemptAt) < context.refreshInterval
        {
            return true
        }
        if context.lastError == nil,
           let lastSnapshotAt = context.lastSnapshotAt,
           context.now.timeIntervalSince(lastSnapshotAt) < context.refreshInterval
        {
            return true
        }
        return false
    }

    struct OpenAIDashboardPageScrapeGate {
        let force: Bool
        let userInitiated: Bool
        let hasHistory: Bool
        let lastPageScrapeAt: Date?
        let historyUpdatedAt: Date?
        let now: Date
        let refreshInterval: TimeInterval
    }

    nonisolated static func shouldAllowOpenAIDashboardPageScrape(_ gate: OpenAIDashboardPageScrapeGate) -> Bool {
        if !gate.hasHistory {
            return true
        }
        if gate.force, gate.userInitiated {
            return true
        }
        guard let lastPageScrapeAt = gate.lastPageScrapeAt ?? gate.historyUpdatedAt else {
            return false
        }
        return gate.now.timeIntervalSince(lastPageScrapeAt) >= gate.refreshInterval
    }

    nonisolated static func dashboardHasPageHistory(_ snapshot: OpenAIDashboardSnapshot?) -> Bool {
        guard let snapshot else { return false }
        return !snapshot.dailyBreakdown.isEmpty || !snapshot.usageBreakdown.isEmpty
    }

    nonisolated static func shouldSkipOpenAIWebEmptyHistoryRetry(_ context: OpenAIWebRefreshGateContext) -> Bool {
        if context.force || context.accountDidChange {
            return false
        }
        guard let lastAttemptAt = context.lastAttemptAt,
              context.now.timeIntervalSince(lastAttemptAt) < context.refreshInterval
        else { return false }
        guard let lastSnapshotAt = context.lastSnapshotAt else { return true }
        return lastAttemptAt >= lastSnapshotAt
    }

    func syncOpenAIWebState() {
        // Provider-specific by design: the OpenAI WKWebView lifecycle is enabled only for Codex consumer access.
        guard self.isEnabled(.codex),
              self.settings.openAIWebAccessEnabled,
              self.settings.codexCookieSource.isEnabled
        else {
            self.resetOpenAIWebState()
            return
        }

        let targetEmail = self.currentCodexOpenAIWebTargetEmail(
            allowCurrentSnapshotFallback: true,
            allowLastKnownLiveFallback: true)
        self.handleOpenAIWebTargetEmailChangeIfNeeded(
            targetEmail: targetEmail,
            targetScope: self.codexCookieCacheScopeForOpenAIWeb())
    }

    func openAIDashboardFriendlyError(
        body: String,
        targetEmail: String?,
        cookieImportStatus: String?) -> String?
    {
        let trimmed = body.trimmingCharacters(in: .whitespacesAndNewlines)
        let status = cookieImportStatus?.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.isEmpty {
            return [
                "OpenAI web dashboard returned an empty page.",
                "Sign in to chatgpt.com and update OpenAI cookies in Providers → Codex.",
            ].joined(separator: " ")
        }

        let lower = trimmed.lowercased()
        let looksLikePublicLanding = lower.contains("skip to content")
            && (lower.contains("about") || lower.contains("openai") || lower.contains("chatgpt"))
        let looksLoggedOut = lower.contains("sign in")
            || lower.contains("log in")
            || lower.contains("create account")
            || lower.contains("continue with google")
            || lower.contains("continue with apple")
            || lower.contains("continue with microsoft")

        guard looksLikePublicLanding || looksLoggedOut else { return nil }
        let emailLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
        let targetLabel = (emailLabel?.isEmpty == false) ? emailLabel! : "your OpenAI account"
        if let status, !status.isEmpty {
            if status.contains("cookies do not match Codex account")
                || status.localizedCaseInsensitiveContains("openai cookies are for")
                || status.localizedCaseInsensitiveContains("cookie import failed")
            {
                return "\(status) Switch chatgpt.com account, then refresh OpenAI cookies."
            }
        }
        return [
            "OpenAI web dashboard returned a public page (not signed in).",
            "Sign in to chatgpt.com as \(targetLabel), then update OpenAI cookies in Providers → Codex.",
        ].joined(separator: " ")
    }

    private static func conciseOpenAICookieMismatchStatus(
        found: [String],
        targetEmail: String?)
        -> String
    {
        let normalizedFound = Array(Set(
            found
                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
                .filter { !$0.isEmpty }))
            .sorted()

        let foundLabel: String = switch normalizedFound.count {
        case 0:
            ""
        case 1:
            normalizedFound[0]
        case 2:
            "\(normalizedFound[0]) or \(normalizedFound[1])"
        default:
            "\(normalizedFound[0]) or \(normalizedFound.count - 1) other accounts"
        }

        let targetLabel = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
        if normalizedFound.isEmpty {
            guard let targetLabel, !targetLabel.isEmpty else {
                return L("No matching OpenAI web session found.")
            }
            return L("No matching OpenAI web session found for %@.", targetLabel)
        }
        guard let targetLabel, !targetLabel.isEmpty else {
            return L("OpenAI cookies are for %@.", foundLabel)
        }
        return L("OpenAI cookies are for %1$@, not %2$@.", foundLabel, targetLabel)
    }
}
