import Foundation

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

#if os(macOS)
import SweetCookieKit

// MARK: - Abacus Usage Fetcher

public enum AbacusUsageFetcher {
    private struct BrowserFetchRequest {
        let browserDetection: BrowserDetection
        let preferredBrowsers: [Browser]
        let label: String
        let timeout: TimeInterval
        let logger: ((String) -> Void)?
    }

    /// Parsed JSON dictionaries are treated as immutable snapshots here and are
    /// only moved between sibling fetch tasks before being consumed locally.
    private struct JSONDictionaryBox: @unchecked Sendable {
        let value: [String: Any]
    }

    private static let log = CodexBarLog.logger(LogCategories.provider(.abacus, scope: "usage"))
    private static let computePointsURL =
        URL(string: "https://apps.abacus.ai/api/_getOrganizationComputePoints")!
    private static let billingInfoURL =
        URL(string: "https://apps.abacus.ai/api/_getBillingInfo")!

    public static func fetchUsage(
        cookieHeaderOverride: String? = nil,
        browserDetection: BrowserDetection = BrowserDetection(),
        timeout: TimeInterval = 15.0,
        logger: ((String) -> Void)? = nil) async throws -> AbacusUsageSnapshot
    {
        // Manual cookie header — no fallback, errors propagate directly
        if let override = CookieHeaderNormalizer.normalize(cookieHeaderOverride) {
            self.emit("Using manual cookie header", logger: logger)
            return try await self.fetchWithCookieHeader(override, timeout: timeout, logger: logger)
        }

        // Cached cookie header — fall back to a fresh browser import when the
        // cached session is rejected or looks stale.
        if let cached = CookieHeaderCache.load(provider: .abacus),
           !cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        {
            self.emit("Using cached cookie header from \(cached.sourceLabel)", logger: logger)
            do {
                return try await self.fetchWithCookieHeader(
                    cached.cookieHeader, timeout: timeout, logger: logger)
            } catch let error as AbacusUsageError where error.shouldTryNextImportedSession {
                if error.shouldClearCachedCookie {
                    CookieHeaderCache.clear(provider: .abacus)
                    self.emit(
                        "Cached cookie failed (\(error.localizedDescription)); cleared, trying fresh import",
                        logger: logger)
                } else {
                    self.emit(
                        "Cached cookie failed (\(error.localizedDescription)); trying fresh import",
                        logger: logger)
                }
            }
        }

        // Fresh browser import — try Chrome first (AGENTS.md default), then broaden
        // to all browsers if Chrome has no sessions OR if every imported Chrome
        // session is exhausted without a successful fetch.
        var lastError: AbacusUsageError = .noSessionCookie
        if let snapshot = try await self.tryFetchFromBrowsers(
            BrowserFetchRequest(
                browserDetection: browserDetection,
                preferredBrowsers: [.chrome],
                label: "Chrome",
                timeout: timeout,
                logger: logger),
            lastError: &lastError)
        {
            return snapshot
        }

        self.emit("Chrome sessions exhausted; falling back to all browsers", logger: logger)
        if let snapshot = try await self.tryFetchFromBrowsers(
            BrowserFetchRequest(
                browserDetection: browserDetection,
                preferredBrowsers: [],
                label: "all browsers",
                timeout: timeout,
                logger: logger),
            lastError: &lastError)
        {
            return snapshot
        }

        throw lastError
    }

    /// Tries to import sessions from `preferredBrowsers` and fetch usage. Returns
    /// the snapshot on success, nil if no sessions were available or every
    /// imported session was exhausted without success.
    private static func tryFetchFromBrowsers(
        _ request: BrowserFetchRequest,
        lastError: inout AbacusUsageError) async throws -> AbacusUsageSnapshot?
    {
        let sessions: [AbacusCookieImporter.SessionInfo]
        do {
            sessions = try AbacusCookieImporter.importSessions(
                browserDetection: request.browserDetection,
                preferredBrowsers: request.preferredBrowsers,
                logger: request.logger)
        } catch {
            BrowserCookieAccessGate.recordIfNeeded(error)
            self.emit(
                "\(request.label) cookie import failed: \(error.localizedDescription)",
                logger: request.logger)
            return nil
        }

        for session in sessions {
            self.emit("Trying cookies from \(session.sourceLabel)", logger: request.logger)
            do {
                let snapshot = try await self.fetchWithCookieHeader(
                    session.cookieHeader,
                    timeout: request.timeout,
                    logger: request.logger)
                CookieHeaderCache.store(
                    provider: .abacus,
                    cookieHeader: session.cookieHeader,
                    sourceLabel: session.sourceLabel)
                return snapshot
            } catch let error as AbacusUsageError where error.shouldTryNextImportedSession {
                self.emit(
                    "\(session.sourceLabel): \(error.localizedDescription), trying next source",
                    logger: request.logger)
                lastError = error
                continue
            } catch {
                self.emit(
                    "\(session.sourceLabel): \(error.localizedDescription), trying next source",
                    logger: request.logger)
                lastError = .networkError(error.localizedDescription)
                continue
            }
        }
        return nil
    }

    // MARK: - API Requests

    private static func fetchWithCookieHeader(
        _ cookieHeader: String,
        timeout: TimeInterval,
        logger: ((String) -> Void)? = nil) async throws -> AbacusUsageSnapshot
    {
        enum FetchPart: Sendable {
            case computePoints(JSONDictionaryBox)
            case billingInfoSuccess(JSONDictionaryBox)
            case billingInfoFailure(String)
        }

        // Fetch compute points (required, full timeout) and billing info
        // (optional, shorter budget) concurrently. Billing is bounded so a
        // slow/flaky billing endpoint can't delay credit rendering.
        let billingBudget = min(timeout, 5.0)

        var computePointsResult: [String: Any]?
        var billingInfoResult: [String: Any] = [:]

        try await withThrowingTaskGroup(of: FetchPart.self) { group in
            group.addTask {
                let result = try await self.fetchJSON(
                    url: self.computePointsURL,
                    method: "GET",
                    cookieHeader: cookieHeader,
                    timeout: timeout)
                return .computePoints(JSONDictionaryBox(value: result))
            }
            group.addTask {
                do {
                    let result = try await self.fetchJSON(
                        url: self.billingInfoURL,
                        method: "POST",
                        cookieHeader: cookieHeader,
                        timeout: billingBudget)
                    return .billingInfoSuccess(JSONDictionaryBox(value: result))
                } catch {
                    return .billingInfoFailure(error.localizedDescription)
                }
            }

            while let result = try await group.next() {
                switch result {
                case let .computePoints(value):
                    computePointsResult = value.value
                case let .billingInfoSuccess(value):
                    billingInfoResult = value.value
                case let .billingInfoFailure(message):
                    self.emit(
                        "Billing info fetch failed: \(message); credits shown without plan/reset",
                        logger: logger)
                }
            }
        }

        guard let computePointsResult else {
            throw AbacusUsageError.networkError("Abacus compute points fetch did not complete")
        }

        return try self.parseResults(computePoints: computePointsResult, billingInfo: billingInfoResult)
    }

    private static func fetchJSON(
        url: URL, method: String, cookieHeader: String, timeout: TimeInterval) async throws -> [String: Any]
    {
        var request = URLRequest(url: url)
        request.httpMethod = method
        request.timeoutInterval = timeout
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")
        if method == "POST" {
            request.httpBody = Data("{}".utf8)
        }

        let response = try await ProviderHTTPClient.shared.response(for: request)
        let data = response.data
        let statusCode = response.statusCode

        if statusCode == 401 || statusCode == 403 {
            throw AbacusUsageError.unauthorized
        }

        guard statusCode == 200 else {
            let body = String(data: data.prefix(200), encoding: .utf8) ?? ""
            throw AbacusUsageError.networkError("HTTP \(statusCode): \(body)")
        }

        let parsed: Any
        do {
            parsed = try JSONSerialization.jsonObject(with: data)
        } catch {
            let preview = String(data: data.prefix(200), encoding: .utf8) ?? "<non-UTF8>"
            throw AbacusUsageError.parseFailed(
                "\(url.lastPathComponent): \(error.localizedDescription) — preview: \(preview)")
        }

        guard let root = parsed as? [String: Any] else {
            throw AbacusUsageError.parseFailed("\(url.lastPathComponent): top-level JSON is not a dictionary")
        }

        guard root["success"] as? Bool == true,
              let result = root["result"] as? [String: Any]
        else {
            let errorMsg = (root["error"] as? String ?? "Unknown error").lowercased()
            if errorMsg.contains("expired") || errorMsg.contains("session")
                || errorMsg.contains("login") || errorMsg.contains("authenticate")
                || errorMsg.contains("unauthorized") || errorMsg.contains("unauthenticated")
                || errorMsg.contains("forbidden")
            {
                throw AbacusUsageError.unauthorized
            }
            throw AbacusUsageError.parseFailed("\(url.lastPathComponent): \(errorMsg)")
        }

        return result
    }

    // MARK: - Parsing

    private static func parseResults(
        computePoints: [String: Any], billingInfo: [String: Any]) throws -> AbacusUsageSnapshot
    {
        let totalCredits = self.double(from: computePoints["totalComputePoints"])
        let creditsLeft = self.double(from: computePoints["computePointsLeft"])

        guard let totalCredits, let creditsLeft else {
            let keys = computePoints.keys.sorted().joined(separator: ", ")
            throw AbacusUsageError.parseFailed(
                "Missing credit fields in compute points response. Keys: [\(keys)]")
        }

        let creditsUsed = totalCredits - creditsLeft

        let nextBillingDate = billingInfo["nextBillingDate"] as? String
        let currentTier = billingInfo["currentTier"] as? String
        let resetsAt = self.parseDate(nextBillingDate)

        return AbacusUsageSnapshot(
            creditsUsed: creditsUsed,
            creditsTotal: totalCredits,
            resetsAt: resetsAt,
            planName: currentTier)
    }

    private static func double(from value: Any?) -> Double? {
        if let d = value as? Double { return d }
        if let i = value as? Int { return Double(i) }
        if let n = value as? NSNumber { return n.doubleValue }
        return nil
    }

    private static func parseDate(_ isoString: String?) -> Date? {
        guard let isoString else { return nil }
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        if let date = formatter.date(from: isoString) { return date }
        formatter.formatOptions = [.withInternetDateTime]
        return formatter.date(from: isoString)
    }

    // MARK: - Logging

    private static func emit(_ message: String, logger: ((String) -> Void)?) {
        logger?("[abacus] \(message)")
        self.log.debug(message)
    }
}

#else

// MARK: - Abacus (Unsupported)

public enum AbacusUsageFetcher {
    public static func fetchUsage(
        cookieHeaderOverride _: String? = nil,
        browserDetection _: BrowserDetection = BrowserDetection(),
        timeout _: TimeInterval = 15.0,
        logger _: ((String) -> Void)? = nil) async throws -> AbacusUsageSnapshot
    {
        throw AbacusUsageError.notSupported
    }
}

#endif
