import CodexBarCore
import Commander
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
import Foundation

extension CodexBarCLI {
    static func decodeProvider(from values: ParsedValues, config: CodexBarConfig) -> ProviderSelection {
        let rawOverride = values.options["provider"]?.last
        return Self.providerSelection(
            rawOverride: rawOverride,
            enabled: config.enabledProviders().compactMap(\.firstPartyProvider))
    }

    static func providerSelection(rawOverride: String?, enabled: [UsageProvider]) -> ProviderSelection {
        if let rawOverride, let parsed = ProviderSelection(argument: rawOverride) {
            return parsed
        }
        if enabled.count == 2 {
            let enabledSet = Set(enabled)
            let primary = Set(ProviderDescriptorRegistry.all.filter(\ .metadata.isPrimaryProvider).map(\ .id))
            if !primary.isEmpty, enabledSet == primary {
                return .both
            }
            return .custom(enabled)
        }
        if enabled.count >= 3 {
            return .custom(enabled)
        }
        if let first = enabled.first {
            return ProviderSelection(provider: first)
        }
        return .custom([])
    }

    static func decodeFormat(from values: ParsedValues) -> OutputFormat {
        if let raw = values.options["format"]?.last, let parsed = OutputFormat(argument: raw) {
            return parsed
        }
        if values.flags.contains("jsonShortcut") || values.flags.contains("json") || values.flags.contains("jsonOnly") {
            return .json
        }
        return .text
    }

    static func decodeTokenAccountSelection(from values: ParsedValues) throws -> TokenAccountCLISelection {
        let label = values.options["account"]?.last
        let rawIndex = values.options["accountIndex"]?.last
        var index: Int?
        if let rawIndex {
            guard let parsed = Int(rawIndex), parsed > 0 else {
                throw CLIArgumentError("--account-index must be a positive integer.")
            }
            index = parsed - 1
        }
        let allAccounts = values.flags.contains("allAccounts")
        return TokenAccountCLISelection(label: label, index: index, allAccounts: allAccounts)
    }

    static func shouldUseColor(noColor: Bool, format: OutputFormat) -> Bool {
        guard format == .text else { return false }
        if noColor {
            return false
        }
        let env = ProcessInfo.processInfo.environment
        if env["TERM"]?.lowercased() == "dumb" {
            return false
        }
        return isatty(STDOUT_FILENO) == 1
    }

    static func detectVersion(for provider: UsageProvider, browserDetection: BrowserDetection) -> String? {
        ProviderDescriptorRegistry.descriptor(for: provider).cli.versionDetector?(browserDetection)
    }

    static func normalizeVersion(raw: String?) -> String? {
        guard let raw, !raw.isEmpty else { return nil }
        if let match = raw.range(of: #"(\d+(?:\.\d+)+)"#, options: .regularExpression) {
            return String(raw[match]).trimmingCharacters(in: .whitespacesAndNewlines)
        }
        return raw.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    static func makeHeader(provider: UsageProvider, version: String?, source: String) -> String {
        let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
        if let version, !version.isEmpty {
            return "\(name) \(version) (\(source))"
        }
        return "\(name) (\(source))"
    }

    static func printFetchAttempts(provider: UsageProvider, attempts: [ProviderFetchAttempt]) {
        guard !attempts.isEmpty else { return }
        self.writeStderr("[\(provider.rawValue)] fetch strategies:\n")
        for attempt in attempts {
            let kindLabel = Self.fetchKindLabel(attempt.kind)
            var line = "  - \(attempt.strategyID) (\(kindLabel))"
            line += attempt.wasAvailable ? " available" : " unavailable"
            if let error = attempt.errorDescription, !error.isEmpty {
                line += " error=\(error)"
            }
            self.writeStderr("\(line)\n")
        }
    }

    static func usageTextNotes(
        provider: UsageProvider,
        sourceMode: ProviderSourceMode,
        resolvedSourceLabel: String,
        dataConfidence: UsageDataConfidence = .unknown) -> [String]
    {
        // Provider-specific by design: OpenCode Go local quota windows need an explicit authority warning.
        if provider == .opencodego, dataConfidence == .estimated {
            return ["Quota estimated from local usage history"]
        }

        // Provider-specific by design: Kilo automatic mode reports when its CLI fallback won strategy selection.
        guard provider == .kilo,
              sourceMode == .auto,
              resolvedSourceLabel.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "cli"
        else {
            return []
        }
        return ["Using CLI fallback"]
    }

    static func kiloAutoFallbackSummary(
        provider: UsageProvider,
        sourceMode: ProviderSourceMode,
        attempts: [ProviderFetchAttempt]) -> String?
    {
        // Provider-specific by design: Kilo exposes its ordered API-to-CLI fallback attempts in verbose output.
        guard provider == .kilo, sourceMode == .auto, !attempts.isEmpty else { return nil }
        let parts = attempts.map { attempt in
            let label = Self.fetchKindLabel(attempt.kind)
            let message = attempt.errorDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
            if !message.isEmpty {
                return "\(label): \(message)"
            }
            return "\(label): \(attempt.wasAvailable ? "success" : "unavailable")"
        }
        guard !parts.isEmpty else { return nil }
        return "Kilo auto fallback attempts: " + parts.joined(separator: " -> ")
    }

    private static func fetchKindLabel(_ kind: ProviderFetchKind) -> String {
        switch kind {
        case .cli: "cli"
        case .web: "web"
        case .oauth: "oauth"
        case .apiToken: "api"
        case .localProbe: "local"
        case .webDashboard: "web"
        }
    }

    static func fetchStatus(for provider: UsageProvider) async -> ProviderStatusPayload? {
        let urlString = ProviderDescriptorRegistry.descriptor(for: provider).metadata.statusPageURL
        guard let urlString,
              let baseURL = URL(string: urlString) else { return nil }
        do {
            return try await StatusFetcher.fetch(from: baseURL)
        } catch {
            return ProviderStatusPayload(
                indicator: .unknown,
                description: error.localizedDescription,
                updatedAt: nil,
                url: urlString)
        }
    }

    static func resetTimeDisplayStyleFromDefaults() -> ResetTimeDisplayStyle {
        let domains = [
            "com.steipete.codexbar",
            "com.steipete.codexbar.debug",
        ]
        for domain in domains {
            if let value = UserDefaults(suiteName: domain)?.object(forKey: "resetTimesShowAbsolute") as? Bool {
                return value ? .absolute : .countdown
            }
        }
        let fallback = UserDefaults.standard.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false
        return fallback ? .absolute : .countdown
    }

    static func weeklyProgressWorkDaysFromDefaults() -> Int? {
        let domains = [
            "com.steipete.codexbar",
            "com.steipete.codexbar.debug",
        ]
        for domain in domains {
            if let value = UserDefaults(suiteName: domain)?.object(forKey: "weeklyProgressWorkDays") as? Int {
                return value
            }
        }
        return UserDefaults.standard.object(forKey: "weeklyProgressWorkDays") as? Int
    }

    /// The app's "Hide personal information" privacy toggle. Read per request so the
    /// serve dashboard follows the setting without a restart, the same way reset style
    /// and weekly work days already do.
    static func hidePersonalInfoFromDefaults() -> Bool {
        let domains = [
            "com.steipete.codexbar",
            "com.steipete.codexbar.debug",
        ]
        for domain in domains {
            if let value = UserDefaults(suiteName: domain)?.object(forKey: "hidePersonalInfo") as? Bool {
                return value
            }
        }
        return UserDefaults.standard.object(forKey: "hidePersonalInfo") as? Bool ?? false
    }

    static func fetchProviderUsage(
        provider: UsageProvider,
        context: ProviderFetchContext) async -> ProviderFetchOutcome
    {
        let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
        if !descriptor.fetchPlan.sourceModes.contains(context.sourceMode) {
            let error = SourceSelectionError.unsupported(
                provider: descriptor.cli.name,
                source: context.sourceMode)
            return ProviderFetchOutcome(result: .failure(error), attempts: [])
        }
        return await descriptor.fetchOutcome(context: context)
    }

    private enum SourceSelectionError: LocalizedError {
        case unsupported(provider: String, source: ProviderSourceMode)

        var errorDescription: String? {
            switch self {
            case let .unsupported(provider, source):
                "Source '\(source.rawValue)' is not supported for \(provider)."
            }
        }
    }

    static func loadOpenAIDashboardIfAvailable(
        usage: UsageSnapshot,
        sourceLabel: String,
        context: ProviderFetchContext) -> OpenAIDashboardSnapshot?
    {
        guard let cache = OpenAIDashboardCacheStore.load() else { return nil }
        let snapshot: OpenAIDashboardSnapshot = if cache.snapshot.dailyBreakdown.isEmpty,
                                                   !cache.snapshot.creditEvents.isEmpty
        {
            OpenAIDashboardSnapshot(
                signedInEmail: cache.snapshot.signedInEmail,
                codeReviewRemainingPercent: cache.snapshot.codeReviewRemainingPercent,
                codeReviewLimit: cache.snapshot.codeReviewLimit,
                creditEvents: cache.snapshot.creditEvents,
                dailyBreakdown: OpenAIDashboardSnapshot.makeDailyBreakdown(
                    from: cache.snapshot.creditEvents,
                    maxDays: 30),
                usageBreakdown: cache.snapshot.usageBreakdown,
                creditsPurchaseURL: cache.snapshot.creditsPurchaseURL,
                updatedAt: cache.snapshot.updatedAt)
        } else {
            cache.snapshot
        }

        let input = CodexCLIDashboardAuthorityContext.makeCachedDashboardInput(
            dashboard: snapshot,
            cachedAccountEmail: cache.accountEmail,
            usage: usage,
            sourceLabel: sourceLabel,
            context: context)
        let decision = CodexDashboardAuthority.evaluate(input)
        if decision.allowedEffects.contains(.cachedDashboardReuse) {
            return snapshot
        }
        if decision.cleanup.contains(.dashboardCache) {
            OpenAIDashboardCacheStore.clear()
        }
        return nil
    }

    static func decodeWebTimeout(from values: ParsedValues) throws -> TimeInterval? {
        guard let raw = values.options["webTimeout"]?.last else { return nil }
        guard let seconds = Double(raw),
              seconds.isFinite,
              seconds >= 0,
              seconds <= TimeInterval(Int64.max)
        else {
            throw CLIArgumentError("--web-timeout must be a finite, nonnegative number within the supported range.")
        }
        return seconds
    }

    static func decodeSourceMode(from values: ParsedValues) -> ProviderSourceMode? {
        if values.flags.contains("web") {
            return .web
        }
        guard let raw = values.options["source"]?.last?.lowercased() else { return nil }
        return ProviderSourceMode(rawValue: raw)
    }

    static func renderOpenAIWebDashboardText(_ dash: OpenAIDashboardSnapshot) -> String {
        var lines: [String] = []
        if let email = dash.signedInEmail, !email.isEmpty {
            lines.append("Web session: \(email)")
        }
        if let remaining = dash.codeReviewRemainingPercent {
            let percent = Int(remaining.rounded())
            if let limit = dash.codeReviewLimit,
               let reset = UsageFormatter.resetLine(for: limit, style: .countdown)
            {
                lines.append("Code review: \(percent)% remaining (\(reset))")
            } else {
                lines.append("Code review: \(percent)% remaining")
            }
        }
        if let first = dash.creditEvents.first {
            let day = first.date.formatted(date: .abbreviated, time: .omitted)
            lines.append("Web history: \(dash.creditEvents.count) events (latest \(day))")
        } else {
            lines.append("Web history: none")
        }
        return lines.joined(separator: "\n")
    }

    static func mapError(_ error: Error) -> ExitCode {
        switch error {
        case TTYCommandRunner.Error.binaryNotFound,
             CodexStatusProbeError.codexNotInstalled,
             ClaudeUsageError.claudeNotInstalled,
             GeminiStatusProbeError.geminiNotInstalled:
            ExitCode(2)
        case CodexStatusProbeError.timedOut,
             TTYCommandRunner.Error.timedOut,
             GeminiStatusProbeError.timedOut,
             ClaudeWebFetchStrategyError.timedOut,
             CostUsageError.timedOut:
            ExitCode(4)
        case ClaudeUsageError.parseFailed,
             ClaudeUsageError.oauthFailed,
             CostUsageError.unsupportedProvider,
             UsageError.decodeFailed,
             UsageError.noRateLimitsFound,
             GeminiStatusProbeError.parseFailed,
             GeminiStatusProbeError.consumerTierDeprecated:
            ExitCode(3)
        default:
            .failure
        }
    }

    static func printAntigravityPlanInfo(_ info: AntigravityPlanInfoSummary) {
        let fields: [(String, String?)] = [
            ("planName", info.planName),
            ("planDisplayName", info.planDisplayName),
            ("displayName", info.displayName),
            ("productName", info.productName),
            ("planShortName", info.planShortName),
        ]
        self.writeStderr("Antigravity plan info:\n")
        for (label, value) in fields {
            guard let value, !value.isEmpty else { continue }
            self.writeStderr("  \(label): \(value)\n")
        }
    }

    static func loadConfig(output: CLIOutputPreferences) -> CodexBarConfig {
        let store = CodexBarConfigStore()
        do {
            if let existing = try store.load() {
                return existing
            }
            return CodexBarConfig.makeDefault()
        } catch {
            if output.usesJSONOutput {
                let payload = ProviderPayload(
                    providerID: "cli",
                    account: nil,
                    version: nil,
                    source: "cli",
                    status: nil,
                    usage: nil,
                    credits: nil,
                    antigravityPlanInfo: nil,
                    openaiDashboard: nil,
                    error: self.makeErrorPayload(code: .failure, message: error.localizedDescription, kind: .config))
                self.printJSON([payload], pretty: output.pretty)
            } else {
                self.writeStderr("Error: \(error.localizedDescription)\n")
            }
            Self.platformExit(ExitCode.failure.rawValue)
        }
    }
}

struct CLIArgumentError: LocalizedError {
    let message: String

    init(_ message: String) {
        self.message = message
    }

    var errorDescription: String? {
        self.message
    }
}

#if DEBUG
extension CodexBarCLI {
    static func _usageSignatureForTesting() -> CommandSignature {
        CommandSignature.describe(UsageOptions())
    }

    static func _costSignatureForTesting() -> CommandSignature {
        CommandSignature.describe(CostOptions())
    }

    static func _cacheSignatureForTesting() -> CommandSignature {
        CommandSignature.describe(CacheOptions())
    }

    static func _diagnoseSignatureForTesting() -> CommandSignature {
        CommandSignature.describe(DiagnoseOptions())
    }

    static func _configSetAPIKeySignatureForTesting() -> CommandSignature {
        CommandSignature.describe(ConfigSetAPIKeyOptions())
    }

    static func _configDumpSignatureForTesting() -> CommandSignature {
        CommandSignature.describe(ConfigDumpOptions())
    }

    static func _configProviderToggleSignatureForTesting() -> CommandSignature {
        CommandSignature.describe(ConfigProviderToggleOptions())
    }

    static func _decodeFormatForTesting(from values: ParsedValues) -> OutputFormat {
        self.decodeFormat(from: values)
    }

    static func _decodeCostGroupByForTesting(from values: ParsedValues) -> CostGroupBy {
        self.decodeCostGroupBy(from: values)
    }

    static func _decodeWebTimeoutForTesting(from values: ParsedValues) throws -> TimeInterval? {
        try self.decodeWebTimeout(from: values)
    }

    static func _decodeSourceModeForTesting(from values: ParsedValues) -> ProviderSourceMode? {
        self.decodeSourceMode(from: values)
    }
}
#endif
