import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public enum ClaudeAdminAPIUsageError: LocalizedError, Sendable, Equatable {
    case missingCredentials
    case networkError(String)
    case apiError(endpoint: String, statusCode: Int)
    case parseFailed(endpoint: String, message: String)

    public var errorDescription: String? {
        switch self {
        case .missingCredentials:
            "Missing Anthropic Admin API key."
        case let .networkError(message):
            "Claude API usage network error: \(message)"
        case let .apiError(endpoint, statusCode):
            "Claude API usage \(endpoint) error: HTTP \(statusCode)"
        case let .parseFailed(endpoint, message):
            "Failed to parse Claude API usage \(endpoint): \(message)"
        }
    }
}

public enum ClaudeAdminAPIUsageFetcher {
    public static let costReportURL = URL(string: "https://api.anthropic.com/v1/organizations/cost_report")!
    public static let messagesUsageURL =
        URL(string: "https://api.anthropic.com/v1/organizations/usage_report/messages")!

    private static let anthropicVersion = "2023-06-01"
    private static let timeoutSeconds: TimeInterval = 20
    private static let maxDailyBuckets = 31

    public static func fetchUsage(
        apiKey: String,
        costURL: URL = Self.costReportURL,
        messagesURL: URL = Self.messagesUsageURL,
        session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared,
        now: Date = Date()) async throws -> ClaudeAdminAPIUsageSnapshot
    {
        let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else {
            throw ClaudeAdminAPIUsageError.missingCredentials
        }

        let calendar = Self.utcCalendar
        let range = Self.dailyRange(now: now, calendar: calendar)
        let costs = try await Self.fetchCostReport(
            apiKey: trimmed,
            baseURL: costURL,
            range: range,
            transport: transport)
        let messages = try await Self.fetchMessagesUsage(
            apiKey: trimmed,
            baseURL: messagesURL,
            range: range,
            transport: transport)

        return Self.makeSnapshot(costs: costs, messages: messages, now: now, calendar: calendar)
    }

    static func _parseSnapshotForTesting(
        costs: Data,
        messages: Data,
        now: Date,
        calendar: Calendar = Self.utcCalendar) throws -> ClaudeAdminAPIUsageSnapshot
    {
        let costs = try Self.decodeCosts(costs)
        let messages = try Self.decodeMessages(messages)
        return Self.makeSnapshot(costs: costs, messages: messages, now: now, calendar: calendar)
    }

    private static func fetchCostReport(
        apiKey: String,
        baseURL: URL,
        range: DateRange,
        transport: any ProviderHTTPTransport) async throws -> CostReportResponse
    {
        let url = Self.url(
            baseURL: baseURL,
            range: range,
            queryItems: [
                URLQueryItem(name: "group_by[]", value: "description"),
            ])
        let data = try await Self.fetchData(url: url, apiKey: apiKey, endpoint: "cost_report", transport: transport)
        return try Self.decodeCosts(data)
    }

    private static func fetchMessagesUsage(
        apiKey: String,
        baseURL: URL,
        range: DateRange,
        transport: any ProviderHTTPTransport) async throws -> MessagesUsageResponse
    {
        let url = Self.url(
            baseURL: baseURL,
            range: range,
            queryItems: [
                URLQueryItem(name: "group_by[]", value: "model"),
            ])
        let data = try await Self.fetchData(url: url, apiKey: apiKey, endpoint: "messages", transport: transport)
        return try Self.decodeMessages(data)
    }

    private static func fetchData(
        url: URL,
        apiKey: String,
        endpoint: String,
        transport: any ProviderHTTPTransport) async throws -> Data
    {
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.timeoutInterval = Self.timeoutSeconds
        request.setValue(Self.anthropicVersion, forHTTPHeaderField: "anthropic-version")
        request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("CodexBar/1.0", forHTTPHeaderField: "User-Agent")

        let response: ProviderHTTPResponse
        do {
            response = try await transport.response(for: request)
        } catch {
            throw ClaudeAdminAPIUsageError.networkError(error.localizedDescription)
        }

        guard response.statusCode == 200 else {
            throw ClaudeAdminAPIUsageError.apiError(endpoint: endpoint, statusCode: response.statusCode)
        }
        return response.data
    }

    private static func decodeCosts(_ data: Data) throws -> CostReportResponse {
        do {
            return try JSONDecoder().decode(CostReportResponse.self, from: data)
        } catch {
            throw ClaudeAdminAPIUsageError.parseFailed(endpoint: "cost_report", message: error.localizedDescription)
        }
    }

    private static func decodeMessages(_ data: Data) throws -> MessagesUsageResponse {
        do {
            return try JSONDecoder().decode(MessagesUsageResponse.self, from: data)
        } catch {
            throw ClaudeAdminAPIUsageError.parseFailed(endpoint: "messages", message: error.localizedDescription)
        }
    }

    private static func makeSnapshot(
        costs: CostReportResponse,
        messages: MessagesUsageResponse,
        now: Date,
        calendar: Calendar) -> ClaudeAdminAPIUsageSnapshot
    {
        var accumulators: [String: DailyAccumulator] = [:]

        for bucket in costs.data {
            var accumulator = accumulators[bucket.startingAt] ?? DailyAccumulator(
                startingAt: bucket.startingAt,
                endingAt: bucket.endingAt)
            for result in bucket.results {
                // Anthropic Usage & Cost API docs define `amount` as a decimal string in lowest USD units.
                let value = Self.usdFromAnthropicLowestUnitAmount(result.amount)
                accumulator.costUSD += value
                let item = Self.displayName(result.description ?? result.costType, fallback: "Claude API")
                accumulator.costItems[item, default: 0] += value
            }
            accumulators[bucket.startingAt] = accumulator
        }

        for bucket in messages.data {
            var accumulator = accumulators[bucket.startingAt] ?? DailyAccumulator(
                startingAt: bucket.startingAt,
                endingAt: bucket.endingAt)
            for result in bucket.results {
                let input = result.uncachedInputTokens ?? 0
                let cacheCreation = result.cacheCreation?.totalInputTokens ?? 0
                let cacheRead = result.cacheReadInputTokens ?? 0
                let output = result.outputTokens ?? 0
                let total = input + cacheCreation + cacheRead + output
                accumulator.inputTokens += input
                accumulator.cacheCreationInputTokens += cacheCreation
                accumulator.cacheReadInputTokens += cacheRead
                accumulator.outputTokens += output
                accumulator.totalTokens += total
                let modelName = Self.displayName(result.model, fallback: "Claude API")
                accumulator.models[modelName, default: ModelAccumulator()].add(
                    inputTokens: input,
                    cacheCreationInputTokens: cacheCreation,
                    cacheReadInputTokens: cacheRead,
                    outputTokens: output,
                    totalTokens: total)
            }
            accumulators[bucket.startingAt] = accumulator
        }

        let daily = accumulators.values
            .compactMap { $0.makeBucket(calendar: calendar) }
            .filter { $0.startTime <= now }
            .sorted { $0.startTime < $1.startTime }
        return ClaudeAdminAPIUsageSnapshot(daily: daily, updatedAt: now)
    }

    private static func displayName(_ raw: String?, fallback: String) -> String {
        guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
            return fallback
        }
        return trimmed
    }

    private static func usdFromAnthropicLowestUnitAmount(_ raw: String) -> Double {
        (Double(raw) ?? 0) / 100
    }

    private static func url(baseURL: URL, range: DateRange, queryItems extraItems: [URLQueryItem]) -> URL {
        var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
        components.queryItems = [
            URLQueryItem(name: "starting_at", value: Self.rfc3339String(from: range.start)),
            URLQueryItem(name: "ending_at", value: Self.rfc3339String(from: range.end)),
            URLQueryItem(name: "bucket_width", value: "1d"),
            URLQueryItem(name: "limit", value: String(Self.maxDailyBuckets)),
        ] + extraItems
        return components.url!
    }

    private static func dailyRange(now: Date, calendar: Calendar) -> DateRange {
        let today = calendar.startOfDay(for: now)
        let start = calendar.date(byAdding: .day, value: -(Self.maxDailyBuckets - 1), to: today) ?? today
        let end = calendar.date(byAdding: .day, value: 1, to: today) ?? now
        return DateRange(start: start, end: end)
    }

    private static var utcCalendar: Calendar {
        var calendar = Calendar(identifier: .gregorian)
        calendar.locale = Locale(identifier: "en_US_POSIX")
        calendar.timeZone = TimeZone(secondsFromGMT: 0)!
        return calendar
    }

    private static func rfc3339Formatter() -> ISO8601DateFormatter {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime]
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        return formatter
    }

    private static func rfc3339String(from date: Date) -> String {
        self.rfc3339Formatter().string(from: date)
    }

    fileprivate static func dayKey(from date: Date, calendar: Calendar) -> String {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = calendar.timeZone
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter.string(from: date)
    }

    fileprivate static func parseDate(_ raw: String) -> Date? {
        self.rfc3339Formatter().date(from: raw)
    }
}

private struct DateRange {
    let start: Date
    let end: Date
}

private struct DailyAccumulator {
    let startingAt: String
    let endingAt: String
    var costUSD: Double = 0
    var inputTokens: Int = 0
    var cacheCreationInputTokens: Int = 0
    var cacheReadInputTokens: Int = 0
    var outputTokens: Int = 0
    var totalTokens: Int = 0
    var costItems: [String: Double] = [:]
    var models: [String: ModelAccumulator] = [:]

    func makeBucket(calendar: Calendar) -> ClaudeAdminAPIUsageSnapshot.DailyBucket? {
        guard let start = ClaudeAdminAPIUsageFetcher.parseDate(self.startingAt),
              let end = ClaudeAdminAPIUsageFetcher.parseDate(self.endingAt)
        else { return nil }
        return ClaudeAdminAPIUsageSnapshot.DailyBucket(
            day: ClaudeAdminAPIUsageFetcher.dayKey(from: start, calendar: calendar),
            startTime: start,
            endTime: end,
            costUSD: self.costUSD,
            inputTokens: self.inputTokens,
            cacheCreationInputTokens: self.cacheCreationInputTokens,
            cacheReadInputTokens: self.cacheReadInputTokens,
            outputTokens: self.outputTokens,
            totalTokens: self.totalTokens,
            costItems: self.costItems
                .map { ClaudeAdminAPIUsageSnapshot.CostBreakdown(name: $0.key, costUSD: $0.value) }
                .sorted {
                    if $0.costUSD == $1.costUSD { return $0.name < $1.name }
                    return $0.costUSD > $1.costUSD
                },
            models: self.models
                .map { name, total in total.makeModel(name: name) }
                .sorted {
                    if $0.totalTokens == $1.totalTokens { return $0.name < $1.name }
                    return $0.totalTokens > $1.totalTokens
                })
    }
}

private struct ModelAccumulator {
    var inputTokens = 0
    var cacheCreationInputTokens = 0
    var cacheReadInputTokens = 0
    var outputTokens = 0
    var totalTokens = 0

    mutating func add(
        inputTokens: Int,
        cacheCreationInputTokens: Int,
        cacheReadInputTokens: Int,
        outputTokens: Int,
        totalTokens: Int)
    {
        self.inputTokens += inputTokens
        self.cacheCreationInputTokens += cacheCreationInputTokens
        self.cacheReadInputTokens += cacheReadInputTokens
        self.outputTokens += outputTokens
        self.totalTokens += totalTokens
    }

    func makeModel(name: String) -> ClaudeAdminAPIUsageSnapshot.ModelBreakdown {
        ClaudeAdminAPIUsageSnapshot.ModelBreakdown(
            name: name,
            inputTokens: self.inputTokens,
            cacheCreationInputTokens: self.cacheCreationInputTokens,
            cacheReadInputTokens: self.cacheReadInputTokens,
            outputTokens: self.outputTokens,
            totalTokens: self.totalTokens)
    }
}

private struct CostReportResponse: Decodable {
    let data: [CostBucket]
    let hasMore: Bool?
    let nextPage: String?

    private enum CodingKeys: String, CodingKey {
        case data
        case hasMore = "has_more"
        case nextPage = "next_page"
    }
}

private struct CostBucket: Decodable {
    let startingAt: String
    let endingAt: String
    let results: [CostResult]

    private enum CodingKeys: String, CodingKey {
        case startingAt = "starting_at"
        case endingAt = "ending_at"
        case results
    }
}

private struct CostResult: Decodable {
    let currency: String?
    let amount: String
    let description: String?
    let costType: String?

    private enum CodingKeys: String, CodingKey {
        case currency
        case amount
        case description
        case costType = "cost_type"
    }
}

private struct MessagesUsageResponse: Decodable {
    let data: [MessagesBucket]
    let hasMore: Bool?
    let nextPage: String?

    private enum CodingKeys: String, CodingKey {
        case data
        case hasMore = "has_more"
        case nextPage = "next_page"
    }
}

private struct MessagesBucket: Decodable {
    let startingAt: String
    let endingAt: String
    let results: [MessagesResult]

    private enum CodingKeys: String, CodingKey {
        case startingAt = "starting_at"
        case endingAt = "ending_at"
        case results
    }
}

private struct MessagesResult: Decodable {
    let uncachedInputTokens: Int?
    let cacheCreation: CacheCreation?
    let cacheReadInputTokens: Int?
    let outputTokens: Int?
    let model: String?

    private enum CodingKeys: String, CodingKey {
        case uncachedInputTokens = "uncached_input_tokens"
        case cacheCreation = "cache_creation"
        case cacheReadInputTokens = "cache_read_input_tokens"
        case outputTokens = "output_tokens"
        case model
    }
}

private struct CacheCreation: Decodable {
    let ephemeral1HInputTokens: Int?
    let ephemeral5MInputTokens: Int?

    var totalInputTokens: Int {
        (self.ephemeral1HInputTokens ?? 0) + (self.ephemeral5MInputTokens ?? 0)
    }

    private enum CodingKeys: String, CodingKey {
        case ephemeral1HInputTokens = "ephemeral_1h_input_tokens"
        case ephemeral5MInputTokens = "ephemeral_5m_input_tokens"
    }
}
