import Foundation

public enum ResetTimeDisplayStyle: String, Codable, Sendable {
    case countdown
    case absolute
}

public enum UsageFormatter {
    private final class BundleToken {}

    private static let localizationLock = NSLock()
    private nonisolated(unsafe) static var localizationProvider: (@Sendable (String) -> String)?
    private nonisolated(unsafe) static var localeProvider: (@Sendable () -> Locale)?

    public static func setLocalizationProvider(_ provider: @escaping @Sendable (String) -> String) {
        self.localizationLock.lock()
        self.localizationProvider = provider
        self.localizationLock.unlock()
    }

    public static func clearLocalizationProvider() {
        self.localizationLock.lock()
        self.localizationProvider = nil
        self.localizationLock.unlock()
    }

    public static func setLocaleProvider(_ provider: @escaping @Sendable () -> Locale) {
        self.localizationLock.lock()
        self.localeProvider = provider
        self.localizationLock.unlock()
    }

    public static func clearLocaleProvider() {
        self.localizationLock.lock()
        self.localeProvider = nil
        self.localizationLock.unlock()
    }

    private static func currentLocale() -> Locale {
        self.localizationLock.lock()
        let provider = self.localeProvider
        self.localizationLock.unlock()
        return provider?() ?? Locale(identifier: "en_US_POSIX")
    }

    private static func localized(_ key: String) -> String {
        self.localizationLock.lock()
        let provider = self.localizationProvider
        self.localizationLock.unlock()
        if let provider {
            return provider(key)
        }
        #if canImport(ObjectiveC)
        // Bundle(for:) requires Objective-C bundle introspection. Linux uses the English
        // fallback below; app localization is injected through localizationProvider.
        let coreBundle = Bundle(for: BundleToken.self)
        let coreValue = NSLocalizedString(key, tableName: "Localizable", bundle: coreBundle, value: key, comment: "")
        if coreValue != key { return coreValue }

        let mainValue = NSLocalizedString(key, tableName: "Localizable", bundle: .main, value: key, comment: "")
        if mainValue != key { return mainValue }
        #endif

        switch key {
        case "Updated relative %@": return "Updated %@"
        case "Updated absolute %@": return "Updated %@"
        case "usage_percent_suffix_left": return "left"
        case "usage_percent_suffix_used": return "used"
        case "reset_tomorrow_format": return "tomorrow, %@"
        case "byte_unit_byte": return "byte"
        case "byte_unit_bytes": return "bytes"
        case "byte_unit_kilobyte": return "kilobyte"
        case "byte_unit_kilobytes": return "kilobytes"
        case "byte_unit_megabyte": return "megabyte"
        case "byte_unit_megabytes": return "megabytes"
        case "byte_unit_gigabyte": return "gigabyte"
        case "byte_unit_gigabytes": return "gigabytes"
        default: return key
        }
    }

    private static func localized(_ key: String, _ args: CVarArg...) -> String {
        let format = self.localized(key)
        return String(format: format, locale: self.currentLocale(), arguments: args)
    }

    public static func percentText(_ percent: Double, suffix: String) -> String {
        let clamped = min(100, max(0, percent))
        if clamped > 0, clamped < 1 {
            return self.localized("<1%% %@", suffix)
        }
        return self.localized("%.0f%% %@", clamped, suffix)
    }

    public static func usageLine(remaining: Double, used: Double, showUsed: Bool) -> String {
        let percent = showUsed ? used : remaining
        let suffix = showUsed
            ? self.localized("usage_percent_suffix_used")
            : self.localized("usage_percent_suffix_left")
        return self.percentText(percent, suffix: suffix)
    }

    public static func percentString(_ percent: Double) -> String {
        let clamped = min(100, max(0, percent))
        if clamped > 0, clamped < 1 { return "<1%" }
        return String(format: "%.0f%%", clamped)
    }

    public static func resetCountdownDescription(from date: Date, now: Date = .init()) -> String {
        let seconds = max(0, date.timeIntervalSince(now))
        if seconds < 1 { return "now" }

        let totalMinutes = max(1, Int(ceil(seconds / 60.0)))
        let days = totalMinutes / (24 * 60)
        let hours = (totalMinutes / 60) % 24
        let minutes = totalMinutes % 60

        if days > 0 {
            if hours > 0 { return "in \(days)d \(hours)h" }
            if minutes > 0 { return "in \(days)d \(minutes)m" }
            return "in \(days)d"
        }
        if hours > 0 {
            if minutes > 0 { return "in \(hours)h \(minutes)m" }
            return "in \(hours)h"
        }
        return "in \(totalMinutes)m"
    }

    public static func resetDescription(from date: Date, now: Date = .init()) -> String {
        // Human-friendly phrasing: today / tomorrow / date+time.
        let calendar = Calendar.current
        if calendar.isDate(date, inSameDayAs: now) {
            return date.formatted(.dateTime.hour().minute().locale(self.currentLocale()))
        }
        if let tomorrow = calendar.date(byAdding: .day, value: 1, to: now),
           calendar.isDate(date, inSameDayAs: tomorrow)
        {
            let timeStr = date.formatted(.dateTime.hour().minute().locale(self.currentLocale()))
            return self.localized("reset_tomorrow_format", timeStr)
        }
        return date.formatted(.dateTime.month(.abbreviated).day().hour().minute().locale(self.currentLocale()))
    }

    public static func resetLine(
        for window: RateWindow,
        style: ResetTimeDisplayStyle,
        now: Date = .init()) -> String?
    {
        if let date = window.resetsAt {
            if style == .countdown {
                let countdown = self.resetCountdownDescription(from: date, now: now)
                if countdown == "now" {
                    return self.localized("Resets now")
                }
                if countdown.hasPrefix("in ") {
                    return self.localized("Resets in %@", String(countdown.dropFirst(3)))
                }
                return self.localized("Resets %@", countdown)
            }
            let text = self.resetDescription(from: date, now: now)
            return self.localized("Resets %@", text)
        }

        if let desc = window.resetDescription {
            let trimmed = desc.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !trimmed.isEmpty else { return nil }
            if trimmed.lowercased().hasPrefix("resets in ") {
                return self.localized("Resets in %@", String(trimmed.dropFirst("Resets in ".count)))
            }
            if trimmed.lowercased().hasPrefix("resets ") {
                return self.localized("Resets %@", String(trimmed.dropFirst("Resets ".count)))
            }
            return self.localized("Resets %@", trimmed)
        }
        return nil
    }

    public static func updatedString(from date: Date, now: Date = .init()) -> String {
        let delta = now.timeIntervalSince(date)
        if abs(delta) < 60 {
            return self.localized("Updated just now")
        }
        if let hours = Calendar.current.dateComponents([.hour], from: date, to: now).hour, hours < 24 {
            #if os(macOS)
            let rel = RelativeDateTimeFormatter()
            rel.locale = self.currentLocale()
            rel.unitsStyle = .abbreviated
            return self.localized("Updated relative %@", rel.localizedString(for: date, relativeTo: now))
            #else
            let seconds = max(0, Int(now.timeIntervalSince(date)))
            if seconds < 3600 {
                let minutes = max(1, seconds / 60)
                return self.localized("Updated %@m ago", String(minutes))
            }
            let wholeHours = max(1, seconds / 3600)
            return self.localized("Updated %@h ago", String(wholeHours))
            #endif
        } else {
            return self.localized(
                "Updated absolute %@",
                date.formatted(.dateTime.hour().minute().locale(self.currentLocale())))
        }
    }

    public static func creditsString(from value: Double) -> String {
        self.localized("%@ left", self.creditsNumberString(from: value))
    }

    public static func creditsNumberString(from value: Double) -> String {
        let number = NumberFormatter()
        number.numberStyle = .decimal
        number.maximumFractionDigits = 2
        // Use explicit locale for consistent formatting on all systems
        number.locale = Locale(identifier: "en_US_POSIX")
        return number.string(from: NSNumber(value: value)) ?? String(format: "%.2f", value)
    }

    public static func kiroCreditNumber(_ value: Double) -> String {
        let rounded = value.rounded()
        if abs(value - rounded) < 0.005 {
            return String(format: "%.0f", rounded)
        }
        return String(format: "%.2f", value)
    }

    /// Formats a USD value into a target currency code with exchange rate conversion applied.
    public static func convertedCostString(_ usdValue: Double, targetCurrency: String) -> String {
        let converted = Self.convertedCost(
            usdValue,
            preferredCurrency: targetCurrency,
            providerCurrency: "USD")
        return self.currencyString(converted.value, currencyCode: converted.currencyCode)
    }

    /// Formats a value from one currency into another via USD pivot conversion.
    /// Useful when displaying provider costs that are denominated in non-USD currencies
    /// (e.g., Anthropic extra usage returned in GBP) under the user's preferred currency.
    public static func convertedCostString(
        _ value: Double,
        fromCurrency: String,
        targetCurrency: String) -> String
    {
        guard let converted = CurrencyExchange.shared.convert(
            amount: value,
            from: fromCurrency,
            to: targetCurrency)
        else {
            return self.currencyString(value, currencyCode: fromCurrency)
        }
        return self.currencyString(converted, currencyCode: targetCurrency)
    }

    /// Resolves the effective currency code for cost display given user preference
    /// and an optional provider currency. Returns the provider currency when preference
    /// is "auto", otherwise returns the explicit preference.
    public static func effectiveCurrencyCode(
        preferred: String,
        providerCurrency: String?) -> String
    {
        guard preferred != "auto", !preferred.isEmpty else {
            return providerCurrency ?? "USD"
        }
        return preferred
    }

    /// Formats a cost value with smart currency conversion.
    /// - When `preferredCurrency` is "auto", renders in `providerCurrency` (or USD fallback) without conversion.
    /// - When `preferredCurrency` is an explicit code, converts from `providerCurrency` to the target.
    public static func convertedCostString(
        _ value: Double,
        preferredCurrency: String,
        providerCurrency: String?) -> String
    {
        let converted = Self.convertedCost(
            value,
            preferredCurrency: preferredCurrency,
            providerCurrency: providerCurrency)
        return Self.currencyString(converted.value, currencyCode: converted.currencyCode)
    }

    /// Resolves and converts a numeric cost while preserving its source currency
    /// when the requested exchange rate is unavailable.
    public static func convertedCost(
        _ value: Double,
        preferredCurrency: String,
        providerCurrency: String?) -> (value: Double, currencyCode: String)
    {
        let sourceCurrency = providerCurrency ?? "USD"
        let targetCurrency = Self.effectiveCurrencyCode(
            preferred: preferredCurrency,
            providerCurrency: providerCurrency)
        guard targetCurrency != sourceCurrency,
              let converted = CurrencyExchange.shared.convert(
                  amount: value,
                  from: sourceCurrency,
                  to: targetCurrency)
        else {
            return (value, sourceCurrency)
        }
        return (converted, targetCurrency)
    }

    /// Formats a USD value with proper negative handling and thousand separators.
    /// Uses Swift's modern FormatStyle API (iOS 15+/macOS 12+) for robust, locale-aware formatting.
    public static func usdString(_ value: Double) -> String {
        value.formatted(.currency(code: "USD").locale(Locale(identifier: "en_US")))
    }

    public static let costEstimateHint = "Estimated from local logs · may differ from your bill"

    public static func costEstimateHint(provider: UsageProvider) -> String {
        ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.estimateDisclaimer
    }

    /// Formats a currency value with the specified currency code.
    /// Uses FormatStyle with explicit en_US locale to ensure consistent formatting
    /// regardless of the user's system locale (e.g., pt-BR users see $54.72 not US$ 54,72).
    public static func currencyString(_ value: Double, currencyCode: String) -> String {
        value.formatted(.currency(code: currencyCode).locale(Locale(identifier: "en_US")))
    }

    public static func compactCurrencyString(_ value: Double, currencyCode: String) -> String {
        if value != 0, abs(value) < 1 {
            return self.currencyString(value, currencyCode: currencyCode)
        }
        return value.formatted(
            .currency(code: currencyCode)
                .precision(.fractionLength(0))
                .locale(Locale(identifier: "en_US")))
    }

    public static func tokenCountString(_ value: Int) -> String {
        let absValue = abs(value)
        let sign = value < 0 ? "-" : ""

        let units: [(threshold: Int, divisor: Double, suffix: String)] = [
            (1_000_000_000, 1_000_000_000, "B"),
            (1_000_000, 1_000_000, "M"),
            (1000, 1000, "K"),
        ]

        for unit in units where absValue >= unit.threshold {
            let scaled = Double(absValue) / unit.divisor
            let formatted: String
            if scaled >= 10 {
                formatted = String(format: "%.0f", scaled)
            } else {
                var s = String(format: "%.1f", scaled)
                if s.hasSuffix(".0") { s.removeLast(2) }
                formatted = s
            }
            return "\(sign)\(formatted)\(unit.suffix)"
        }

        return "\(value)"
    }

    public static func byteCountString(_ bytes: Int64) -> String {
        let sign = bytes < 0 ? "-" : ""
        let absBytes = Double(bytes.magnitude)
        let units: [(threshold: Double, divisor: Double, suffix: String)] = [
            (1024 * 1024 * 1024, 1024 * 1024 * 1024, "GB"),
            (1024 * 1024, 1024 * 1024, "MB"),
            (1024, 1024, "KB"),
        ]

        for unit in units where absBytes >= unit.threshold {
            let scaled = absBytes / unit.divisor
            let format = scaled >= 10 || scaled.rounded(.towardZero) == scaled ? "%.0f" : "%.1f"
            let formatted = String(format: format, scaled)
            return "\(sign)\(formatted) \(unit.suffix)"
        }

        return "\(bytes) B"
    }

    /// Same magnitudes as `byteCountString`, but spelled out ("megabytes" instead of "MB").
    public static func byteCountStringLong(_ bytes: Int64) -> String {
        let sign = bytes < 0 ? "-" : ""
        let absBytes = Double(bytes.magnitude)
        let units: [(threshold: Double, divisor: Double, singularKey: String, pluralKey: String)] = [
            (1024 * 1024 * 1024, 1024 * 1024 * 1024, "byte_unit_gigabyte", "byte_unit_gigabytes"),
            (1024 * 1024, 1024 * 1024, "byte_unit_megabyte", "byte_unit_megabytes"),
            (1024, 1024, "byte_unit_kilobyte", "byte_unit_kilobytes"),
        ]

        for unit in units where absBytes >= unit.threshold {
            let scaled = absBytes / unit.divisor
            let format = scaled >= 10 || scaled.rounded(.towardZero) == scaled ? "%.0f" : "%.1f"
            let formatted = String(format: format, locale: self.currentLocale(), scaled)
            let displayScale = format == "%.0f" ? 1.0 : 10.0
            let displayedValue = (scaled * displayScale).rounded() / displayScale
            let word = self.localized(displayedValue == 1 ? unit.singularKey : unit.pluralKey)
            return "\(sign)\(formatted) \(word)"
        }

        let word = self.localized(bytes.magnitude == 1 ? "byte_unit_byte" : "byte_unit_bytes")
        return "\(bytes) \(word)"
    }

    public static func creditEventSummary(_ event: CreditEvent) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        let number = NumberFormatter()
        number.numberStyle = .decimal
        number.maximumFractionDigits = 2
        let credits = number.string(from: NSNumber(value: event.creditsUsed)) ?? "0"
        return "\(formatter.string(from: event.date)) · \(event.service) · \(credits) credits"
    }

    public static func creditEventCompact(_ event: CreditEvent) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMM d"
        let number = NumberFormatter()
        number.numberStyle = .decimal
        number.maximumFractionDigits = 2
        let credits = number.string(from: NSNumber(value: event.creditsUsed)) ?? "0"
        return "\(formatter.string(from: event.date)) — \(event.service): \(credits)"
    }

    public static func creditShort(_ value: Double) -> String {
        if value >= 1000 {
            let k = value / 1000
            return String(format: "%.1fk", k)
        }
        return String(format: "%.0f", value)
    }

    public static func truncatedSingleLine(_ text: String, max: Int = 80) -> String {
        let single = text
            .replacingOccurrences(of: "\n", with: " ")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        guard single.count > max else { return single }
        let idx = single.index(single.startIndex, offsetBy: max)
        return "\(single[..<idx])…"
    }

    public static func modelDisplayName(_ raw: String) -> String {
        var cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !cleaned.isEmpty else { return raw }
        if cleaned == "codex-auto-review" { return "Codex Auto Review" }
        if CostUsagePricing.isCodexUnattributedModel(cleaned) { return "Unknown model" }

        let patterns = [
            #"(?:-|\s)\d{8}$"#,
            #"(?:-|\s)\d{4}-\d{2}-\d{2}$"#,
            #"\s\d{4}\s\d{4}$"#,
        ]

        for pattern in patterns {
            if let range = cleaned.range(of: pattern, options: .regularExpression) {
                cleaned.removeSubrange(range)
                break
            }
        }

        if let trailing = cleaned.range(of: #"[ \t-]+$"#, options: .regularExpression) {
            cleaned.removeSubrange(trailing)
        }

        return cleaned.isEmpty ? raw : cleaned
    }

    public static func modelCostDetail(
        _ model: String,
        costUSD: Double?,
        totalTokens: Int? = nil,
        currencyCode: String = "USD") -> String?
    {
        let costDetail: String? = if let label = CostUsagePricing.codexDisplayLabel(model: model) {
            label
        } else if let costUSD {
            self.currencyString(costUSD, currencyCode: currencyCode)
        } else {
            nil
        }

        let tokenDetail = totalTokens.map(self.tokenCountString)
        let parts = [costDetail, tokenDetail].compactMap(\.self)
        guard !parts.isEmpty else { return nil }
        return parts.joined(separator: " · ")
    }

    /// Cleans a provider plan string: strip ANSI/bracket noise, drop boilerplate words, collapse whitespace, and
    /// ensure a leading capital if the result starts lowercase.
    public static func cleanPlanName(_ text: String) -> String {
        let stripped = TextParsing.stripANSICodes(text)
        let withoutCodes = stripped.replacingOccurrences(
            of: #"^\s*(?:\[\d{1,3}m\s*)+"#,
            with: "",
            options: [.regularExpression])
        let withoutBoilerplate = withoutCodes.replacingOccurrences(
            of: #"(?i)\b(claude|codex|account|plan)\b"#,
            with: "",
            options: [.regularExpression])
        var cleaned = withoutBoilerplate
            .replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
            .trimmingCharacters(in: .whitespacesAndNewlines)
        if cleaned.isEmpty {
            cleaned = stripped.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        if cleaned.lowercased() == "oauth" {
            return "Ollama"
        }
        // Capitalize first letter only if lowercase, preserving acronyms like "AI"
        if let first = cleaned.first, first.isLowercase {
            return cleaned.prefix(1).uppercased() + cleaned.dropFirst()
        }
        return cleaned
    }
}
