import Foundation

public struct MiniMaxBillingSummary: Sendable {
    public let todayTokens: Int
    public let last30DaysTokens: Int
    public let todayCash: Double?
    public let last30DaysCash: Double?
    public let daily: [MiniMaxBillingDay]
    public let topMethods: [MiniMaxBillingBreakdown]
    public let topModels: [MiniMaxBillingBreakdown]
    public let updatedAt: Date

    public init(
        todayTokens: Int,
        last30DaysTokens: Int,
        todayCash: Double?,
        last30DaysCash: Double?,
        daily: [MiniMaxBillingDay],
        topMethods: [MiniMaxBillingBreakdown],
        topModels: [MiniMaxBillingBreakdown],
        updatedAt: Date)
    {
        self.todayTokens = todayTokens
        self.last30DaysTokens = last30DaysTokens
        self.todayCash = todayCash
        self.last30DaysCash = last30DaysCash
        self.daily = daily
        self.topMethods = topMethods
        self.topModels = topModels
        self.updatedAt = updatedAt
    }
}

public struct MiniMaxBillingDay: Sendable, Equatable {
    public let day: String
    public let tokens: Int
    public let cash: Double?

    public init(day: String, tokens: Int, cash: Double?) {
        self.day = day
        self.tokens = tokens
        self.cash = cash
    }
}

public struct MiniMaxBillingBreakdown: Sendable, Equatable {
    public let name: String
    public let tokens: Int
    public let cash: Double?

    public init(name: String, tokens: Int, cash: Double?) {
        self.name = name
        self.tokens = tokens
        self.cash = cash
    }
}

struct MiniMaxBillingHistoryPayload: Decodable {
    let baseResp: MiniMaxBaseResponse?
    let chargeRecords: [MiniMaxBillingRecord]
    let totalCount: Int?

    private enum CodingKeys: String, CodingKey {
        case baseResp = "base_resp"
        case chargeRecords = "charge_records"
        case totalCount = "total_cnt"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.baseResp = try container.decodeIfPresent(MiniMaxBaseResponse.self, forKey: .baseResp)
        self.chargeRecords = try container.decodeIfPresent([MiniMaxBillingRecord].self, forKey: .chargeRecords) ?? []
        self.totalCount = MiniMaxDecoding.decodeInt(container, forKey: .totalCount)
    }
}

struct MiniMaxBillingRecord: Decodable {
    let consumeToken: Int?
    let consumeInputToken: Int?
    let consumeOutputToken: Int?
    let consumeCash: Double?
    let consumeCashAfterVoucher: Double?
    let createdAt: Int?
    let ymd: String?
    let consumeTime: String?
    let method: String?
    let model: String?
    let result: String?
    let status: String?

    private enum CodingKeys: String, CodingKey {
        case consumeToken = "consume_token"
        case consumeInputToken = "consume_input_token"
        case consumeOutputToken = "consume_output_token"
        case consumeCash = "consume_cash"
        case consumeCashAfterVoucher = "consume_cash_after_voucher"
        case createdAt = "created_at"
        case ymd
        case consumeTime = "consume_time"
        case method
        case model
        case result
        case status
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.consumeToken = MiniMaxDecoding.decodeInt(container, forKey: .consumeToken)
        self.consumeInputToken = MiniMaxDecoding.decodeInt(container, forKey: .consumeInputToken)
        self.consumeOutputToken = MiniMaxDecoding.decodeInt(container, forKey: .consumeOutputToken)
        self.consumeCash = MiniMaxDecoding.decodeDouble(container, forKey: .consumeCash)
        self.consumeCashAfterVoucher = MiniMaxDecoding.decodeDouble(container, forKey: .consumeCashAfterVoucher)
        self.createdAt = MiniMaxDecoding.decodeInt(container, forKey: .createdAt)
        self.ymd = try container.decodeIfPresent(String.self, forKey: .ymd)
        self.consumeTime = try container.decodeIfPresent(String.self, forKey: .consumeTime)
        self.method = try container.decodeIfPresent(String.self, forKey: .method)
        self.model = try container.decodeIfPresent(String.self, forKey: .model)
        self.result = Self.decodeOptionalScalarString(container, forKey: .result)
        self.status = Self.decodeOptionalScalarString(container, forKey: .status)
    }

    var recordResult: String? {
        if let result = self.result?.trimmingCharacters(in: .whitespacesAndNewlines), !result.isEmpty {
            return result
        }
        if let status = self.status?.trimmingCharacters(in: .whitespacesAndNewlines), !status.isEmpty {
            return status
        }
        return nil
    }

    var tokenCount: Int {
        if let consumeToken, consumeToken > 0 { return consumeToken }
        return max(0, (self.consumeInputToken ?? 0) + (self.consumeOutputToken ?? 0))
    }

    var cashValue: Double? {
        self.consumeCashAfterVoucher ?? self.consumeCash
    }

    private static func decodeOptionalScalarString<K: CodingKey>(
        _ container: KeyedDecodingContainer<K>,
        forKey key: K) -> String?
    {
        if let value = try? container.decodeIfPresent(String.self, forKey: key) {
            return value
        }
        if let value = try? container.decodeIfPresent(Int.self, forKey: key) {
            return String(value)
        }
        if let value = try? container.decodeIfPresent(Double.self, forKey: key) {
            return String(value)
        }
        if let value = try? container.decodeIfPresent(Bool.self, forKey: key) {
            return String(value)
        }
        return nil
    }
}

enum MiniMaxBillingHistoryParser {
    static func decodePayload(data: Data) throws -> MiniMaxBillingHistoryPayload {
        try JSONDecoder().decode(MiniMaxBillingHistoryPayload.self, from: data)
    }

    static func parse(
        data: Data,
        now: Date = Date(),
        calendar: Calendar = .current) throws -> MiniMaxBillingSummary
    {
        let payload = try self.decodePayload(data: data)
        if let status = payload.baseResp?.statusCode, status != 0 {
            let message = payload.baseResp?.statusMessage ?? "status_code \(status)"
            throw MiniMaxUsageError.apiError(message)
        }
        guard !payload.chargeRecords.isEmpty || (payload.totalCount ?? 0) == 0 else {
            throw MiniMaxUsageError.parseFailed("Missing MiniMax billing records.")
        }
        return self.aggregate(records: payload.chargeRecords, now: now, calendar: calendar)
    }

    static func aggregate(
        records: [MiniMaxBillingRecord],
        now: Date = Date(),
        calendar inputCalendar: Calendar = .current) -> MiniMaxBillingSummary
    {
        var calendar = inputCalendar
        calendar.timeZone = inputCalendar.timeZone

        let startOfToday = calendar.startOfDay(for: now)
        let startOf30Days = calendar.date(byAdding: .day, value: -29, to: startOfToday) ?? startOfToday
        var daily: [String: (date: Date, tokens: Int, cash: Double, hasCash: Bool)] = [:]
        var methodTotals: [String: (tokens: Int, cash: Double, hasCash: Bool)] = [:]
        var modelTotals: [String: (tokens: Int, cash: Double, hasCash: Bool)] = [:]

        for record in records {
            if let recordResult = record.recordResult,
               recordResult.caseInsensitiveCompare("SUCCESS") != .orderedSame
            {
                continue
            }

            guard let date = self.recordDate(record, calendar: calendar),
                  date >= startOf30Days,
                  date <= now
            else {
                continue
            }

            let day = self.dayString(date, calendar: calendar)
            let tokens = record.tokenCount
            let cash = record.cashValue
            var bucket = daily[day] ?? (calendar.startOfDay(for: date), 0, 0, false)
            bucket.tokens += tokens
            if let cash {
                bucket.cash += cash
                bucket.hasCash = true
            }
            daily[day] = bucket

            self.add(record, tokens: tokens, cash: cash, keyPath: \.method, totals: &methodTotals)
            self.add(record, tokens: tokens, cash: cash, keyPath: \.model, totals: &modelTotals)
        }

        let sortedDays = daily
            .sorted { $0.value.date < $1.value.date }
            .map { key, value in
                MiniMaxBillingDay(
                    day: key,
                    tokens: value.tokens,
                    cash: value.hasCash ? value.cash : nil)
            }
        let todayKey = self.dayString(now, calendar: calendar)
        let today = daily[todayKey]
        let last30Tokens = sortedDays.reduce(0) { $0 + $1.tokens }
        let last30CashValues = sortedDays.compactMap(\.cash)
        let last30Cash = last30CashValues.isEmpty ? nil : last30CashValues.reduce(0, +)

        return MiniMaxBillingSummary(
            todayTokens: today?.tokens ?? 0,
            last30DaysTokens: last30Tokens,
            todayCash: (today?.hasCash == true) ? today?.cash : nil,
            last30DaysCash: last30Cash,
            daily: sortedDays,
            topMethods: self.breakdowns(from: methodTotals),
            topModels: self.breakdowns(from: modelTotals),
            updatedAt: now)
    }

    private static func add(
        _ record: MiniMaxBillingRecord,
        tokens: Int,
        cash: Double?,
        keyPath: KeyPath<MiniMaxBillingRecord, String?>,
        totals: inout [String: (tokens: Int, cash: Double, hasCash: Bool)])
    {
        let rawName = record[keyPath: keyPath]?.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let rawName, !rawName.isEmpty else { return }
        var total = totals[rawName] ?? (0, 0, false)
        total.tokens += tokens
        if let cash {
            total.cash += cash
            total.hasCash = true
        }
        totals[rawName] = total
    }

    private static func breakdowns(from totals: [String: (tokens: Int, cash: Double, hasCash: Bool)])
        -> [MiniMaxBillingBreakdown]
    {
        totals
            .map { name, value in
                MiniMaxBillingBreakdown(
                    name: name,
                    tokens: value.tokens,
                    cash: value.hasCash ? value.cash : nil)
            }
            .sorted {
                if $0.tokens == $1.tokens {
                    return $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
                }
                return $0.tokens > $1.tokens
            }
            .prefix(3)
            .map(\.self)
    }

    static func containsRecordBefore30DayWindow(
        _ records: [MiniMaxBillingRecord],
        now: Date = Date(),
        calendar inputCalendar: Calendar = .current) -> Bool
    {
        var calendar = inputCalendar
        calendar.timeZone = inputCalendar.timeZone
        let startOfToday = calendar.startOfDay(for: now)
        let startOf30Days = calendar.date(byAdding: .day, value: -29, to: startOfToday) ?? startOfToday
        return records.contains { record in
            guard let date = self.recordDate(record, calendar: calendar) else { return false }
            return date < startOf30Days
        }
    }

    private static func recordDate(_ record: MiniMaxBillingRecord, calendar: Calendar) -> Date? {
        if let createdAt = record.createdAt {
            let interval = createdAt > 1_000_000_000_000
                ? TimeInterval(createdAt) / 1000
                : TimeInterval(createdAt)
            return Date(timeIntervalSince1970: interval)
        }
        if let ymd = record.ymd {
            return self.parseDateOnly(ymd, calendar: calendar)
        }
        if let consumeTime = record.consumeTime {
            return self.parseDate(consumeTime, formats: [
                "yyyy-MM-dd HH:mm:ss",
                "yyyy/MM/dd HH:mm:ss",
                "yyyy-MM-dd'T'HH:mm:ssXXXXX",
            ])
        }
        return nil
    }

    private static func parseDateOnly(_ text: String, calendar: Calendar) -> Date? {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return nil }
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.calendar = calendar
        formatter.timeZone = calendar.timeZone
        for format in ["yyyy-MM-dd", "yyyyMMdd", "yyyy/MM/dd"] {
            formatter.dateFormat = format
            if let date = formatter.date(from: trimmed) {
                return calendar.startOfDay(for: date)
            }
        }
        return nil
    }

    private static func parseDate(_ text: String, formats: [String]) -> Date? {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return nil }
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        for format in formats {
            formatter.dateFormat = format
            if let date = formatter.date(from: trimmed) { return date }
        }
        return nil
    }

    private static func dayString(_ date: Date, calendar: Calendar) -> String {
        let components = calendar.dateComponents([.year, .month, .day], from: date)
        guard let year = components.year,
              let month = components.month,
              let day = components.day
        else { return "" }
        return String(format: "%04d-%02d-%02d", year, month, day)
    }
}
