#if os(macOS)
import AppKit
import Foundation

public struct CodexWebDashboardStrategy: ProviderFetchStrategy {
    public let id: String = "codex.web.dashboard"
    public let kind: ProviderFetchKind = .webDashboard

    public init() {}

    public func isAvailable(_ context: ProviderFetchContext) async -> Bool {
        context.sourceMode.usesWeb &&
            !Self.managedAccountStoreIsUnreadable(context) &&
            !Self.managedAccountTargetIsUnavailable(context) &&
            (!Self.profileAccountTargetIsUnavailable(context) || context.sourceMode == .web)
    }

    public func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
        guard !Self.managedAccountStoreIsUnreadable(context) else {
            // A fail-closed placeholder CODEX_HOME does not identify a target account. If the managed store
            // itself is unreadable, web import must not fall back to "any signed-in browser account".
            throw OpenAIDashboardFetcher.FetchError.loginRequired
        }
        guard !Self.managedAccountTargetIsUnavailable(context) else {
            // If the selected managed account no longer exists in a readable store, web import must not
            // fall back to "any signed-in browser account" for that stale selection.
            throw OpenAIDashboardFetcher.FetchError.loginRequired
        }
        guard !Self.profileAccountTargetIsUnavailable(context) else {
            // A profile without an auth-backed email cannot safely target browser cookie import.
            throw OpenAIDashboardFetcher.FetchError.loginRequired
        }

        let options = OpenAIWebOptions(
            deadline: OpenAIDashboardFetcher.deadline(startingAt: Date(), timeout: context.webTimeout),
            debugDumpHTML: context.webDebugDumpHTML,
            verbose: context.verbose)

        // Ensure AppKit is initialized before using WebKit in a CLI.
        await MainActor.run {
            _ = NSApplication.shared
        }

        let result: OpenAIWebCodexResult
        do {
            result = try await Self.fetchOpenAIWebCodex(
                context: context,
                options: options,
                browserDetection: context.browserDetection)
        } catch let error as URLError where error.code == .timedOut {
            throw OpenAIWebCodexError.timedOut(seconds: context.webTimeout)
        }
        return self.makeResult(
            usage: result.usage,
            credits: result.credits,
            dashboard: result.dashboard,
            sourceLabel: "openai-web")
    }

    public func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
        _ = error
        return context.sourceMode == .auto
    }

    private static func managedAccountStoreIsUnreadable(_ context: ProviderFetchContext) -> Bool {
        context.settings?.codex?.managedAccountStoreUnreadable == true
    }

    private static func managedAccountTargetIsUnavailable(_ context: ProviderFetchContext) -> Bool {
        context.settings?.codex?.managedAccountTargetUnavailable == true
    }

    private static func profileAccountTargetIsUnavailable(_ context: ProviderFetchContext) -> Bool {
        context.settings?.codex?.profileAccountTargetUnavailable == true
    }
}

struct OpenAIWebCodexResult {
    let usage: UsageSnapshot
    let credits: CreditsSnapshot?
    let dashboard: OpenAIDashboardSnapshot
}

enum OpenAIWebCodexError: LocalizedError, Equatable {
    case missingUsage
    case policyRejected(CodexDashboardAuthorityDecision)
    case timedOut(seconds: TimeInterval)

    var errorDescription: String? {
        switch self {
        case .missingUsage:
            return "OpenAI web dashboard did not include usage limits."
        case let .timedOut(seconds):
            return "OpenAI web dashboard fetch timed out after \(seconds.formatted()) seconds."
        case let .policyRejected(decision):
            switch decision.reason {
            case let .wrongEmail(expected, actual):
                var details: [String] = []
                if let expected {
                    details.append("expected \(expected)")
                }
                if let actual {
                    details.append("got \(actual)")
                }
                if details.isEmpty {
                    return "OpenAI web dashboard belonged to the wrong account."
                }
                return "OpenAI web dashboard belonged to the wrong account (\(details.joined(separator: ", ")))."
            case .unresolvedWithoutTrustedEvidence:
                return "Active Codex identity is unresolved and no trusted auth-backed continuity exists."
            case .providerAccountMissingScopedEmail:
                return "Active Codex provider account is missing its scoped email, " +
                    "so dashboard ownership cannot be proven."
            case .providerAccountLacksExactOwnershipProof:
                return "OpenAI web dashboard could not be proven to belong to the active provider account."
            case .missingDashboardSignedInEmail:
                return "OpenAI web dashboard did not expose a signed-in email."
            case let .sameEmailAmbiguity(email):
                return "OpenAI web dashboard email \(email) is ambiguous across multiple known owners."
            default:
                return "OpenAI web dashboard was rejected by Codex dashboard authority."
            }
        }
    }
}

private struct OpenAIWebOptions {
    let deadline: Date
    let debugDumpHTML: Bool
    let verbose: Bool
}

@MainActor
private final class WebLogBuffer {
    private var lines: [String] = []
    private let maxCount: Int
    private let verbose: Bool
    /// Provider-specific by design: The Codex dashboard strategy logs its OpenAI web integration separately.
    private let logger = CodexBarLog.logger(LogCategories.provider(.openai, scope: "web"))

    init(maxCount: Int = 300, verbose: Bool) {
        self.maxCount = maxCount
        self.verbose = verbose
    }

    func append(_ line: String) {
        self.lines.append(line)
        if self.lines.count > self.maxCount {
            self.lines.removeFirst(self.lines.count - self.maxCount)
        }
        if self.verbose {
            self.logger.verbose(line)
        }
    }

    func snapshot() -> [String] {
        self.lines
    }
}

extension CodexWebDashboardStrategy {
    @MainActor
    fileprivate static func fetchOpenAIWebCodex(
        context: ProviderFetchContext,
        options: OpenAIWebOptions,
        browserDetection: BrowserDetection) async throws -> OpenAIWebCodexResult
    {
        let logger = WebLogBuffer(verbose: options.verbose)
        let log: @MainActor (String) -> Void = { line in
            logger.append(line)
        }
        do {
            let result = try await Self.fetchOpenAIWebDashboard(
                context: context,
                options: options,
                browserDetection: browserDetection,
                preferCachedCookieHeader: true,
                logger: log)
            return try Self.makeAuthorizedDashboardResult(
                dashboard: result.dashboard,
                context: context,
                routingTargetEmail: result.routingTargetEmail)
        } catch {
            guard Self.shouldRetryWithFreshBrowserImport(after: error) else {
                throw error
            }
            _ = try OpenAIDashboardBrowserCookieImporter.remainingTimeout(until: options.deadline)
            log("Retrying OpenAI web dashboard with a fresh browser cookie import.")
            let result = try await Self.fetchOpenAIWebDashboard(
                context: context,
                options: options,
                browserDetection: browserDetection,
                preferCachedCookieHeader: false,
                logger: log)
            return try Self.makeAuthorizedDashboardResult(
                dashboard: result.dashboard,
                context: context,
                routingTargetEmail: result.routingTargetEmail)
        }
    }

    nonisolated static func shouldRetryWithFreshBrowserImport(after error: Error) -> Bool {
        if error is OpenAIWebCodexError {
            return error as? OpenAIWebCodexError == .missingUsage
        }
        if case OpenAIDashboardFetcher.FetchError.noDashboardData = error {
            return true
        }
        return false
    }

    @MainActor
    static func makeAuthorizedDashboardResultForTesting(
        dashboard: OpenAIDashboardSnapshot,
        context: ProviderFetchContext,
        routingTargetEmail: String?)
        throws -> OpenAIWebCodexResult
    {
        try self.makeAuthorizedDashboardResult(
            dashboard: dashboard,
            context: context,
            routingTargetEmail: routingTargetEmail)
    }

    @MainActor
    private static func makeAuthorizedDashboardResult(
        dashboard: OpenAIDashboardSnapshot,
        context: ProviderFetchContext,
        routingTargetEmail: String?) throws -> OpenAIWebCodexResult
    {
        let input = CodexCLIDashboardAuthorityContext.makeLiveWebInput(
            dashboard: dashboard,
            context: context,
            routingTargetEmail: routingTargetEmail)
        let decision = CodexDashboardAuthority.evaluate(input)

        switch decision.disposition {
        case .attach:
            let attachedAccountEmail = CodexCLIDashboardAuthorityContext.attachmentEmail(from: input)
            let credits = dashboard.toCreditsSnapshot()
            let usage = dashboard.toUsageSnapshot(provider: .codex, accountEmail: attachedAccountEmail)
                ?? Self.makeCreditsOnlyUsageSnapshot(
                    dashboard: dashboard,
                    attachedAccountEmail: attachedAccountEmail,
                    credits: credits)
            guard let usage else {
                throw OpenAIWebCodexError.missingUsage
            }
            if let attachedAccountEmail {
                OpenAIDashboardCacheStore.save(OpenAIDashboardCache(
                    accountEmail: attachedAccountEmail,
                    snapshot: dashboard))
            }
            return OpenAIWebCodexResult(usage: usage, credits: credits, dashboard: dashboard)
        case .displayOnly:
            if decision.cleanup.contains(.dashboardCache) {
                OpenAIDashboardCacheStore.clear()
            }
            throw CodexDashboardPolicyError.displayOnly(decision)
        case .failClosed:
            if decision.cleanup.contains(.dashboardCache) {
                OpenAIDashboardCacheStore.clear()
            }
            throw OpenAIWebCodexError.policyRejected(decision)
        }
    }

    private static func makeCreditsOnlyUsageSnapshot(
        dashboard: OpenAIDashboardSnapshot,
        attachedAccountEmail: String?,
        credits: CreditsSnapshot?) -> UsageSnapshot?
    {
        guard credits != nil else { return nil }
        return UsageSnapshot(
            primary: nil,
            secondary: nil,
            tertiary: nil,
            updatedAt: dashboard.updatedAt,
            identity: ProviderIdentitySnapshot(
                providerID: .codex,
                accountEmail: attachedAccountEmail ?? dashboard.signedInEmail,
                accountOrganization: nil,
                loginMethod: dashboard.accountPlan))
    }

    private struct OpenAIWebDashboardFetchResult {
        let dashboard: OpenAIDashboardSnapshot
        let routingTargetEmail: String?
    }

    @MainActor
    private static func fetchOpenAIWebDashboard(
        context: ProviderFetchContext,
        options: OpenAIWebOptions,
        browserDetection: BrowserDetection,
        preferCachedCookieHeader: Bool,
        logger: @MainActor @escaping (String) -> Void) async throws -> OpenAIWebDashboardFetchResult
    {
        let auth = context.fetcher.loadAuthBackedCodexAccount()
        let routingTargetEmail = auth.email?.trimmingCharacters(in: .whitespacesAndNewlines)
        let allowAnyAccount = routingTargetEmail == nil

        let importResult = try await OpenAIDashboardBrowserCookieImporter(browserDetection: browserDetection)
            .importBestCookies(
                intoAccountEmail: routingTargetEmail,
                allowAnyAccount: allowAnyAccount,
                preferCachedCookieHeader: preferCachedCookieHeader,
                cacheScope: context.settings?.codex?.openAIWebCacheScope,
                deadline: options.deadline,
                logger: logger)
        let effectiveEmail = routingTargetEmail ?? importResult.signedInEmail?
            .trimmingCharacters(in: .whitespacesAndNewlines)

        let dashboard = try await OpenAIDashboardFetcher().loadLatestDashboard(
            accountEmail: effectiveEmail,
            cacheScope: context.settings?.codex?.openAIWebCacheScope,
            logger: logger,
            debugDumpHTML: options.debugDumpHTML,
            timeout: OpenAIDashboardBrowserCookieImporter.remainingTimeout(until: options.deadline))
        return OpenAIWebDashboardFetchResult(
            dashboard: dashboard,
            routingTargetEmail: routingTargetEmail)
    }
}
#else
public struct CodexWebDashboardStrategy: ProviderFetchStrategy {
    public let id: String = "codex.web.dashboard"
    public let kind: ProviderFetchKind = .webDashboard

    public init() {}

    public func isAvailable(_: ProviderFetchContext) async -> Bool {
        false
    }

    public func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult {
        throw ProviderFetchError.noAvailableStrategy(.codex)
    }

    public func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
        false
    }
}
#endif
