import Foundation

public struct ProviderDiagnosticBatchExport: Codable, Sendable {
    public let schemaVersion: String
    public let timestamp: Date
    public let diagnostics: [ProviderDiagnosticExport]

    public init(
        schemaVersion: String = "1.0",
        timestamp: Date,
        diagnostics: [ProviderDiagnosticExport])
    {
        self.schemaVersion = schemaVersion
        self.timestamp = timestamp
        self.diagnostics = diagnostics
    }
}

public struct ProviderDiagnosticExport: Codable, Sendable {
    public let schemaVersion: String
    public let timestamp: Date
    public let platform: String
    public let appVersion: String?
    public let provider: String
    public let displayName: String
    public let source: String
    public let sourceMode: String
    public let auth: ProviderDiagnosticAuthSummary
    public let usage: ProviderDiagnosticUsageSummary?
    public let fetchAttempts: [ProviderDiagnosticFetchAttempt]
    public let error: ProviderDiagnosticError?
    public let settings: ProviderDiagnosticSettingsSummary
    public let details: ProviderDiagnosticDetails?

    private enum CodingKeys: String, CodingKey {
        case schemaVersion
        case timestamp
        case platform
        case appVersion
        case provider
        case displayName
        case source
        case sourceMode
        case auth
        case usage
        case fetchAttempts
        case error
        case settings
        case details
    }

    public init(
        schemaVersion: String = "1.0",
        timestamp: Date,
        platform: String = ProviderDiagnosticPlatform.current,
        appVersion: String? = nil,
        provider: String,
        displayName: String,
        source: String,
        sourceMode: String,
        auth: ProviderDiagnosticAuthSummary,
        usage: ProviderDiagnosticUsageSummary?,
        fetchAttempts: [ProviderDiagnosticFetchAttempt],
        error: ProviderDiagnosticError?,
        settings: ProviderDiagnosticSettingsSummary,
        details: ProviderDiagnosticDetails?)
    {
        self.schemaVersion = schemaVersion
        self.timestamp = timestamp
        self.platform = platform
        self.appVersion = appVersion
        self.provider = provider
        self.displayName = displayName
        self.source = source
        self.sourceMode = sourceMode
        self.auth = auth
        self.usage = usage
        self.fetchAttempts = fetchAttempts
        self.error = error
        self.settings = settings
        self.details = details
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        try self.init(
            schemaVersion: container.decode(String.self, forKey: .schemaVersion),
            timestamp: container.decode(Date.self, forKey: .timestamp),
            platform: container.decodeIfPresent(String.self, forKey: .platform)
                ?? ProviderDiagnosticPlatform.current,
            appVersion: container.decodeIfPresent(String.self, forKey: .appVersion),
            provider: container.decode(String.self, forKey: .provider),
            displayName: container.decode(String.self, forKey: .displayName),
            source: container.decode(String.self, forKey: .source),
            sourceMode: container.decode(String.self, forKey: .sourceMode),
            auth: container.decode(ProviderDiagnosticAuthSummary.self, forKey: .auth),
            usage: container.decodeIfPresent(ProviderDiagnosticUsageSummary.self, forKey: .usage),
            fetchAttempts: container.decode([ProviderDiagnosticFetchAttempt].self, forKey: .fetchAttempts),
            error: container.decodeIfPresent(ProviderDiagnosticError.self, forKey: .error),
            settings: container.decode(ProviderDiagnosticSettingsSummary.self, forKey: .settings),
            details: container.decodeIfPresent(ProviderDiagnosticDetails.self, forKey: .details))
    }
}

public enum ProviderDiagnosticPlatform {
    public static var current: String {
        #if os(macOS)
        "macOS"
        #elseif os(Linux)
        "Linux"
        #else
        "unknown"
        #endif
    }
}

public struct ProviderDiagnosticAuthSummary: Codable, Sendable {
    public let configured: Bool
    public let modes: [String]

    public init(configured: Bool, modes: [String]) {
        self.configured = configured
        self.modes = modes
    }

    public func resolved(with outcome: ProviderFetchOutcome) -> ProviderDiagnosticAuthSummary {
        var resolvedModes = self.modes
        if outcome.isSuccess {
            for attempt in outcome.attempts where attempt.wasAvailable {
                let mode = ProviderDiagnosticFetchAttempt.kindLabel(attempt.kind)
                if !resolvedModes.contains(mode) {
                    resolvedModes.append(mode)
                }
            }
        }
        let configured = self.configured || outcome.isSuccess
        return ProviderDiagnosticAuthSummary(configured: configured, modes: resolvedModes)
    }
}

public struct ProviderDiagnosticUsageSummary: Codable, Sendable {
    public let updatedAt: Date
    public let dataConfidence: String
    public let windows: [ProviderDiagnosticRateWindow]
    public let extraWindowCount: Int
    public let providerCostPresent: Bool
    public let providerSpecificData: [String]
    public let detailSections: [ProviderDetailSection]

    private enum CodingKeys: String, CodingKey {
        case updatedAt
        case dataConfidence
        case windows
        case extraWindowCount
        case providerCostPresent
        case providerSpecificData
        case detailSections
    }

    public init(from snapshot: UsageSnapshot) {
        var windows: [ProviderDiagnosticRateWindow] = []
        if let primary = snapshot.primary {
            windows.append(ProviderDiagnosticRateWindow(label: "primary", window: primary))
        }
        if let secondary = snapshot.secondary {
            windows.append(ProviderDiagnosticRateWindow(label: "secondary", window: secondary))
        }
        if let tertiary = snapshot.tertiary {
            windows.append(ProviderDiagnosticRateWindow(label: "tertiary", window: tertiary))
        }
        for extra in snapshot.extraRateWindows ?? [] {
            windows.append(ProviderDiagnosticRateWindow(
                label: extra.title,
                window: extra.window,
                usageKnown: extra.usageKnown))
        }

        var providerSpecificData: [String] = []
        if snapshot.openAIAPIUsage != nil {
            providerSpecificData.append("openAIAPIUsage")
        }
        if snapshot.mistralUsage != nil {
            providerSpecificData.append("mistralUsage")
        }

        self.updatedAt = snapshot.updatedAt
        self.dataConfidence = snapshot.dataConfidence.rawValue
        self.windows = windows
        self.extraWindowCount = snapshot.extraRateWindows?.count ?? 0
        self.providerCostPresent = snapshot.providerCost != nil
        self.providerSpecificData = providerSpecificData.sorted()
        self.detailSections = snapshot.details
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.updatedAt = try container.decode(Date.self, forKey: .updatedAt)
        self.dataConfidence = try container.decodeIfPresent(String.self, forKey: .dataConfidence)
            ?? UsageDataConfidence.unknown.rawValue
        self.windows = try container.decode([ProviderDiagnosticRateWindow].self, forKey: .windows)
        self.extraWindowCount = try container.decode(Int.self, forKey: .extraWindowCount)
        self.providerCostPresent = try container.decode(Bool.self, forKey: .providerCostPresent)
        self.providerSpecificData = try container.decode([String].self, forKey: .providerSpecificData)
        self.detailSections = try container.decodeIfPresent(
            [ProviderDetailSection].self,
            forKey: .detailSections) ?? []
    }
}

public struct ProviderDiagnosticRateWindow: Codable, Sendable {
    public let label: String
    public let usedPercent: Double
    public let windowMinutes: Int?
    public let resetsAt: Date?
    public let hasResetDescription: Bool
    public let nextRegenPercent: Double?
    public let usageKnown: Bool

    private enum CodingKeys: String, CodingKey {
        case label
        case usedPercent
        case windowMinutes
        case resetsAt
        case hasResetDescription
        case nextRegenPercent
        case usageKnown
    }

    public init(label: String, window: RateWindow, usageKnown: Bool = true) {
        self.label = label
        self.usedPercent = window.usedPercent
        self.windowMinutes = window.windowMinutes
        self.resetsAt = window.resetsAt
        self.hasResetDescription = window.resetDescription?.isEmpty == false
        self.nextRegenPercent = window.nextRegenPercent
        self.usageKnown = usageKnown
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.label = try container.decode(String.self, forKey: .label)
        self.usedPercent = try container.decode(Double.self, forKey: .usedPercent)
        self.windowMinutes = try container.decodeIfPresent(Int.self, forKey: .windowMinutes)
        self.resetsAt = try container.decodeIfPresent(Date.self, forKey: .resetsAt)
        self.hasResetDescription = try container.decode(Bool.self, forKey: .hasResetDescription)
        self.nextRegenPercent = try container.decodeIfPresent(Double.self, forKey: .nextRegenPercent)
        self.usageKnown = try container.decodeIfPresent(Bool.self, forKey: .usageKnown) ?? true
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.label, forKey: .label)
        try container.encode(self.usedPercent, forKey: .usedPercent)
        try container.encodeIfPresent(self.windowMinutes, forKey: .windowMinutes)
        try container.encodeIfPresent(self.resetsAt, forKey: .resetsAt)
        try container.encode(self.hasResetDescription, forKey: .hasResetDescription)
        try container.encodeIfPresent(self.nextRegenPercent, forKey: .nextRegenPercent)
        if !self.usageKnown {
            try container.encode(false, forKey: .usageKnown)
        }
    }
}

public struct ProviderDiagnosticFetchAttempt: Codable, Sendable {
    public let kind: String
    public let wasAvailable: Bool
    public let errorCategory: String?

    public init(
        kind: String,
        wasAvailable: Bool,
        errorCategory: String?)
    {
        self.kind = kind
        self.wasAvailable = wasAvailable
        self.errorCategory = errorCategory
    }

    public init(from attempt: ProviderFetchAttempt) {
        self.kind = Self.kindLabel(attempt.kind)
        self.wasAvailable = attempt.wasAvailable
        self.errorCategory = attempt.errorDescription.map(Self.errorCategoryLabel)
    }

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

    public static func errorCategoryLabel(_ description: String?) -> String {
        guard let desc = description?.lowercased() else { return "unknown" }
        if desc.contains("endpoint override") {
            return "configuration"
        }
        if desc.contains("network") || desc.contains("timeout") || desc.contains("connection") {
            return "network"
        }
        if desc.contains("auth") || desc.contains("credential") || desc.contains("token") || desc.contains("cookie") ||
            desc.contains("api key") || desc.contains("key not configured") || desc.contains("missing key")
        {
            return "auth"
        }
        if desc.contains("source") || desc.contains("not supported") || desc.contains("unavailable") {
            return "configuration"
        }
        if desc.contains("api") || desc.contains("http") || desc.contains("404") || desc.contains("403") {
            return "api"
        }
        if desc.contains("parse") || desc.contains("format") || desc.contains("decode") {
            return "parse"
        }
        return "unknown"
    }
}

public struct ProviderDiagnosticError: Codable, Sendable {
    public let category: String
    public let safeDescription: String

    public init(category: String, safeDescription: String) {
        self.category = category
        self.safeDescription = safeDescription
    }

    public init(from error: Error, authConfigured: Bool) {
        self.category = Self.errorCategory(error, authConfigured: authConfigured)
        self.safeDescription = Self.safeDescription(category: self.category)
    }

    private static func errorCategory(_ error: Error, authConfigured: Bool) -> String {
        if case ProviderFetchError.noAvailableStrategy = error {
            return authConfigured ? "configuration" : "auth"
        }
        if error is ProviderEndpointOverrideError {
            return "configuration"
        }
        if let pluginError = error as? ProviderFetchClassifiedError {
            return switch pluginError.kind {
            case .authenticationExpired, .missingCredential, .permissionDenied: "auth"
            case .rateLimited, .providerUnavailable, .apiFailure: "api"
            case .parseFailure: "parse"
            case .networkFailure: "network"
            }
        }
        if let minimaxError = error as? MiniMaxUsageError {
            switch minimaxError {
            case .networkError: return "network"
            case .invalidCredentials: return "auth"
            case .apiError: return "api"
            case .parseFailed: return "parse"
            }
        }
        if let alibabaError = error as? AlibabaCodingPlanUsageError {
            switch alibabaError {
            case .networkError: return "network"
            case .loginRequired, .invalidCredentials: return "auth"
            case .apiError, .apiKeyUnavailableInRegion: return "api"
            case .parseFailed: return "parse"
            }
        }
        if error is MiniMaxSettingsError || error is MiniMaxAPISettingsError {
            return "auth"
        }
        return ProviderDiagnosticFetchAttempt.errorCategoryLabel(error.localizedDescription)
    }

    private static func safeDescription(category: String) -> String {
        switch category {
        case "network":
            "Network error - check your connection"
        case "auth":
            "Authentication or setup issue - check provider credentials"
        case "api":
            "API error - service returned an unexpected response"
        case "parse":
            "Parse error - unexpected response format"
        case "configuration":
            "Configuration issue - check provider source and settings"
        default:
            "An unexpected error occurred"
        }
    }
}

public struct ProviderDiagnosticSettingsSummary: Codable, Sendable {
    public let sourceMode: String
    public let apiRegion: String?

    public init(sourceMode: ProviderSourceMode, apiRegion: String? = nil) {
        self.sourceMode = sourceMode.rawValue
        self.apiRegion = apiRegion
    }
}

public enum ProviderDiagnosticDetails: Codable, Sendable {
    case minimax(MiniMaxDiagnosticDetails)

    private enum CodingKeys: String, CodingKey {
        case type
        case minimax
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)
        switch type {
        case "minimax":
            self = try .minimax(container.decode(MiniMaxDiagnosticDetails.self, forKey: .minimax))
        default:
            throw DecodingError.dataCorruptedError(
                forKey: .type,
                in: container,
                debugDescription: "Unknown provider diagnostic detail type")
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .minimax(details):
            try container.encode("minimax", forKey: .type)
            try container.encode(details, forKey: .minimax)
        }
    }
}

public struct MiniMaxDiagnosticDetails: Codable, Sendable {
    public let planName: String?
    public let availablePrompts: Int?
    public let currentPrompts: Int?
    public let remainingPrompts: Int?
    public let windowMinutes: Int?
    public let usedPercent: Double?
    public let resetsAt: Date?
    public let services: [MiniMaxDiagnosticServiceUsage]?
    public let billingSummaryPresent: Bool

    public init(from snapshot: MiniMaxUsageSnapshot) {
        self.planName = snapshot.planName
        self.availablePrompts = snapshot.availablePrompts
        self.currentPrompts = snapshot.currentPrompts
        self.remainingPrompts = snapshot.remainingPrompts
        self.windowMinutes = snapshot.windowMinutes
        self.usedPercent = snapshot.usedPercent
        self.resetsAt = snapshot.resetsAt
        self.services = snapshot.services?.map { MiniMaxDiagnosticServiceUsage(from: $0) }
        self.billingSummaryPresent = snapshot.billingSummary != nil
    }
}

public struct MiniMaxDiagnosticServiceUsage: Codable, Sendable {
    public let displayName: String
    public let percent: Double
    public let usage: Int
    public let limit: Int
    public let remaining: Int?
    public let isUnlimited: Bool
    public let windowType: String
    public let resetsAt: Date?
    public let hasResetDescription: Bool

    private enum CodingKeys: String, CodingKey {
        case displayName
        case percent
        case usage
        case limit
        case remaining
        case isUnlimited
        case windowType
        case resetsAt
        case hasResetDescription
    }

    public init(from service: MiniMaxServiceUsage) {
        self.displayName = service.displayName
        self.percent = service.percent
        self.usage = service.usage
        self.limit = service.limit
        self.remaining = service.isUnlimited ? nil : max(0, service.limit - service.usage)
        self.isUnlimited = service.isUnlimited
        self.windowType = service.windowType
        self.resetsAt = service.resetsAt
        self.hasResetDescription = !service.resetDescription.isEmpty
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.displayName = try container.decode(String.self, forKey: .displayName)
        self.percent = try container.decode(Double.self, forKey: .percent)
        self.usage = try container.decodeIfPresent(Int.self, forKey: .usage) ?? 0
        self.limit = try container.decodeIfPresent(Int.self, forKey: .limit) ?? 0
        self.remaining = try container.decodeIfPresent(Int.self, forKey: .remaining)
        self.isUnlimited = try container.decodeIfPresent(Bool.self, forKey: .isUnlimited) ?? false
        self.windowType = try container.decode(String.self, forKey: .windowType)
        self.resetsAt = try container.decodeIfPresent(Date.self, forKey: .resetsAt)
        self.hasResetDescription = try container.decode(Bool.self, forKey: .hasResetDescription)
    }
}

public enum ProviderDiagnosticExportBuilder {
    public struct Input: Sendable {
        public let provider: UsageProvider
        public let descriptor: ProviderDescriptor
        public let outcome: ProviderFetchOutcome
        public let sourceMode: ProviderSourceMode
        public let settings: ProviderSettingsSnapshot?
        public let auth: ProviderDiagnosticAuthSummary
        public let appVersion: String?

        public init(
            provider: UsageProvider,
            descriptor: ProviderDescriptor,
            outcome: ProviderFetchOutcome,
            sourceMode: ProviderSourceMode,
            settings: ProviderSettingsSnapshot?,
            auth: ProviderDiagnosticAuthSummary,
            appVersion: String? = nil)
        {
            self.provider = provider
            self.descriptor = descriptor
            self.outcome = outcome
            self.sourceMode = sourceMode
            self.settings = settings
            self.auth = auth
            self.appVersion = appVersion
        }
    }

    public static func build(_ input: Input) -> ProviderDiagnosticExport {
        let resolvedAuth = input.auth.resolved(with: input.outcome)
        let usage = input.outcome.usageSnapshot.map { ProviderDiagnosticUsageSummary(from: $0) }
        let error = input.outcome.failureError
            .map { ProviderDiagnosticError(from: $0, authConfigured: resolvedAuth.configured) }
        let settingsSummary = ProviderDiagnosticSettingsSummary(
            sourceMode: input.sourceMode,
            apiRegion: Self.safeAPIRegion(provider: input.provider, settings: input.settings))

        return ProviderDiagnosticExport(
            timestamp: Date(),
            appVersion: input.appVersion,
            provider: input.provider.rawValue,
            displayName: input.descriptor.metadata.displayName,
            source: input.outcome.sourceLabel,
            sourceMode: input.sourceMode.rawValue,
            auth: resolvedAuth,
            usage: usage,
            fetchAttempts: input.outcome.attempts.map { ProviderDiagnosticFetchAttempt(from: $0) },
            error: error,
            settings: settingsSummary,
            details: nil)
    }

    private static func safeAPIRegion(provider: UsageProvider, settings: ProviderSettingsSnapshot?) -> String? {
        guard provider == .minimax else { return nil }
        return settings?.minimax?.apiRegion.rawValue ?? "global"
    }
}

extension ProviderFetchOutcome {
    fileprivate var isSuccess: Bool {
        guard case .success = self.result else { return false }
        return true
    }

    fileprivate var sourceLabel: String {
        guard case let .success(result) = result else { return "failed" }
        return result.sourceLabel
    }

    fileprivate var usageSnapshot: UsageSnapshot? {
        guard case let .success(result) = result else { return nil }
        return result.usage
    }

    fileprivate var failureError: Error? {
        guard case let .failure(error) = result else { return nil }
        return error
    }
}
