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

public enum QoderWebSite: CaseIterable, Sendable {
    case international
    case china

    var usageURL: URL {
        switch self {
        case .international:
            URL(string: "https://qoder.com/api/v2/me/usages/big_model_credits")!
        case .china:
            URL(string: "https://qoder.com.cn/api/v2/me/usages/big_model_credits")!
        }
    }

    public var dashboardURL: URL {
        switch self {
        case .international:
            URL(string: "https://qoder.com/account/usage")!
        case .china:
            URL(string: "https://qoder.com.cn/account/usage")!
        }
    }

    var webOrigin: String {
        switch self {
        case .international:
            "https://qoder.com"
        case .china:
            "https://qoder.com.cn"
        }
    }

    var cookieDomains: [String] {
        switch self {
        case .international:
            ["qoder.com", "www.qoder.com"]
        case .china:
            ["qoder.com.cn", "www.qoder.com.cn"]
        }
    }
}

public enum QoderUsageFetcher {
    private static let log = CodexBarLog.logger(LogCategories.provider(.qoder, scope: "usage"))
    private static let userAgent =
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"

    public static func fetchUsage(
        cookieHeader: String,
        site: QoderWebSite = .international,
        transport: any ProviderHTTPTransport = ProviderHTTPClient.shared,
        now: Date = Date(),
        timeout: TimeInterval = 15) async throws -> QoderUsageSnapshot
    {
        let data = try await self.send(
            cookieHeader: cookieHeader,
            site: site,
            transport: transport,
            timeout: timeout)
        return try self.parseUsage(data: data, now: now)
    }

    private static func send(
        cookieHeader: String,
        site: QoderWebSite,
        transport: any ProviderHTTPTransport,
        timeout: TimeInterval) async throws -> Data
    {
        var request = URLRequest(url: site.usageURL)
        request.httpMethod = "GET"
        request.timeoutInterval = timeout
        request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")
        request.setValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept")
        request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
        request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
        request.setValue(site.webOrigin, forHTTPHeaderField: "Origin")
        request.setValue("\(site.webOrigin)/account/usage", forHTTPHeaderField: "Referer")
        request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
        request.setValue("2.5.35", forHTTPHeaderField: "Bx-V")

        let response: ProviderHTTPResponse
        do {
            response = try await transport.response(for: request)
        } catch is CancellationError {
            throw CancellationError()
        } catch let error as URLError where error.code == .cancelled {
            throw CancellationError()
        } catch {
            throw QoderUsageError.networkError(error.localizedDescription)
        }
        if response.statusCode == 401 || response.statusCode == 403 {
            throw QoderUsageError.invalidCredentials
        }
        guard (200..<300).contains(response.statusCode) else {
            Self.log.error("Qoder usage API returned \(response.statusCode)")
            throw QoderUsageError.apiError(response.statusCode)
        }
        return response.data
    }

    static func parseUsage(data: Data, now: Date = Date()) throws -> QoderUsageSnapshot {
        let response: QoderUsageResponse
        do {
            response = try JSONDecoder().decode(QoderUsageResponse.self, from: data)
        } catch {
            throw QoderUsageError.parseFailed("invalid JSON: \(error.localizedDescription)")
        }

        guard let summary = response.totalQuota?.quotaSummary else {
            throw QoderUsageError.parseFailed("missing totalQuota.quotaSummary")
        }

        let merged = try Self.mergedQuota(
            base: summary,
            shared: response.sharedQuota?.quotaSummary)

        return QoderUsageSnapshot(
            usedCredits: merged.usedCredits,
            totalCredits: merged.totalCredits,
            remainingCredits: merged.remainingCredits,
            usagePercentage: merged.usagePercentage,
            unit: merged.unit,
            resetsAt: response.nextResetAt,
            updatedAt: now)
    }

    private struct MergedQuota {
        let usedCredits: Double
        let totalCredits: Double
        let remainingCredits: Double
        let usagePercentage: Double
        let unit: String?
    }

    private static func mergedQuota(
        base: QoderQuotaSummary,
        shared: QoderQuotaSummary?) throws -> MergedQuota
    {
        let baseUsed = base.usedValue
        let baseTotal = base.limitValue
        let baseRemaining = try Self.remainingCredits(for: base)

        guard let shared else {
            let percentage = try Self.usagePercentage(
                used: baseUsed,
                total: baseTotal,
                remaining: baseRemaining,
                provided: base.usagePercentage)
            return MergedQuota(
                usedCredits: baseUsed,
                totalCredits: baseTotal,
                remainingCredits: baseRemaining,
                usagePercentage: percentage,
                unit: base.unit)
        }

        let sharedUsed = shared.usedValue
        let sharedTotal = shared.limitValue
        let sharedRemaining = try Self.remainingCredits(for: shared)
        let used = baseUsed + sharedUsed
        let total = baseTotal + sharedTotal
        let remaining = baseRemaining + sharedRemaining
        let percentage = try Self.usagePercentage(
            used: used,
            total: total,
            remaining: remaining,
            provided: nil)
        return MergedQuota(
            usedCredits: used,
            totalCredits: total,
            remainingCredits: remaining,
            usagePercentage: percentage,
            unit: base.unit ?? shared.unit)
    }

    private static func remainingCredits(for summary: QoderQuotaSummary) throws -> Double {
        guard summary.usedValue >= 0,
              summary.limitValue >= 0,
              summary.remainingValue.map({ $0 >= 0 }) ?? true
        else {
            throw QoderUsageError.parseFailed("quota values must be nonnegative")
        }
        return summary.remainingValue ?? max(0, summary.limitValue - summary.usedValue)
    }

    private static func usagePercentage(
        used: Double,
        total: Double,
        remaining: Double,
        provided: Double?) throws -> Double
    {
        guard used >= 0, total >= 0, remaining >= 0 else {
            throw QoderUsageError.parseFailed("quota values must be nonnegative")
        }
        guard total > 0 else {
            guard used == 0, remaining == 0 else {
                throw QoderUsageError.parseFailed("zero total quota must have zero usage and remaining")
            }
            return provided ?? 100
        }
        return provided ?? (used / total) * 100
    }
}

private struct QoderUsageResponse: Decodable {
    let totalQuota: QoderQuotaContainer?
    let sharedQuota: QoderQuotaContainer?
    let nextResetAt: Date?

    private enum CodingKeys: String, CodingKey {
        case totalQuota
        case totalQuotaSnake = "total_quota"
        case sharedQuota
        case sharedQuotaSnake = "shared_quota"
        case nextResetAt
        case nextResetAtSnake = "next_reset_at"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.totalQuota =
            try container.decodeIfPresent(QoderQuotaContainer.self, forKey: .totalQuota) ??
            container.decodeIfPresent(QoderQuotaContainer.self, forKey: .totalQuotaSnake)
        self.sharedQuota =
            try container.decodeIfPresent(QoderQuotaContainer.self, forKey: .sharedQuota) ??
            container.decodeIfPresent(QoderQuotaContainer.self, forKey: .sharedQuotaSnake)
        self.nextResetAt =
            Self.decodeDate(from: container, forKey: .nextResetAt) ??
            Self.decodeDate(from: container, forKey: .nextResetAtSnake)
    }

    private static func decodeDate(
        from container: KeyedDecodingContainer<CodingKeys>,
        forKey key: CodingKeys) -> Date?
    {
        if let value = try? container.decode(String.self, forKey: key) {
            return self.parseISO8601Date(value)
        }
        if let value = try? container.decode(Double.self, forKey: key) {
            let seconds = value > 10_000_000_000 ? value / 1000 : value
            return Date(timeIntervalSince1970: seconds)
        }
        return nil
    }

    private static func parseISO8601Date(_ value: String) -> Date? {
        let fractional = ISO8601DateFormatter()
        fractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        if let date = fractional.date(from: value) {
            return date
        }

        let plain = ISO8601DateFormatter()
        plain.formatOptions = [.withInternetDateTime]
        return plain.date(from: value)
    }
}

private struct QoderQuotaContainer: Decodable {
    let quotaSummary: QoderQuotaSummary?

    private enum CodingKeys: String, CodingKey {
        case quotaSummary
        case quotaSummarySnake = "quota_summary"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.quotaSummary =
            try container.decodeIfPresent(QoderQuotaSummary.self, forKey: .quotaSummary) ??
            container.decodeIfPresent(QoderQuotaSummary.self, forKey: .quotaSummarySnake)
    }
}

private struct QoderQuotaSummary: Decodable {
    let usedValue: Double
    let limitValue: Double
    let remainingValue: Double?
    let usagePercentage: Double?
    let unit: String?

    private enum CodingKeys: String, CodingKey {
        case usedValue
        case usedValueSnake = "used_value"
        case limitValue
        case limitValueSnake = "limit_value"
        case remainingValue
        case remainingValueSnake = "remaining_value"
        case usagePercentage
        case usagePercentageSnake = "usage_percentage"
        case unit
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.usedValue =
            try container.decodeIfPresent(Double.self, forKey: .usedValue) ??
            container.decode(Double.self, forKey: .usedValueSnake)
        self.limitValue =
            try container.decodeIfPresent(Double.self, forKey: .limitValue) ??
            container.decode(Double.self, forKey: .limitValueSnake)
        self.remainingValue =
            try container.decodeIfPresent(Double.self, forKey: .remainingValue) ??
            container.decodeIfPresent(Double.self, forKey: .remainingValueSnake)
        self.usagePercentage =
            try container.decodeIfPresent(Double.self, forKey: .usagePercentage) ??
            container.decodeIfPresent(Double.self, forKey: .usagePercentageSnake)
        self.unit = try container.decodeIfPresent(String.self, forKey: .unit)
    }
}
