import Foundation

public struct GrokUsageSnapshot: Sendable {
    public let billing: GrokBillingResponse?
    public let webBilling: GrokWebBillingSnapshot?
    public let credentials: GrokCredentials?
    public let localSummary: GrokLocalSessionSummary?
    public let cliVersion: String?
    public let diagnostic: String?
    public let updatedAt: Date

    public init(
        billing: GrokBillingResponse?,
        webBilling: GrokWebBillingSnapshot? = nil,
        credentials: GrokCredentials?,
        localSummary: GrokLocalSessionSummary?,
        cliVersion: String?,
        updatedAt: Date,
        diagnostic: String? = nil)
    {
        self.billing = billing
        self.webBilling = webBilling
        self.credentials = credentials
        self.localSummary = localSummary
        self.cliVersion = cliVersion
        self.diagnostic = diagnostic
        self.updatedAt = updatedAt
    }

    public func toUsageSnapshot() -> UsageSnapshot {
        // Primary window: credit usage (against included limit) from the CLI RPC,
        // falling back to the web billing RPC used by grok.com when the agent surface lacks billing.
        var primary: RateWindow?
        if let billing,
           let percent = billing.monthlyUsedPercent
        {
            primary = RateWindow(
                usedPercent: percent,
                windowMinutes: billing.billingPeriodMinutes,
                resetsAt: billing.billingPeriodEndDate,
                resetDescription: nil)
        } else if let webBilling,
                  let percent = webBilling.usedPercent
        {
            // Do not infer the full quota cadence from the remaining time until reset; a
            // monthly window near its reset would otherwise be misclassified as weekly.
            primary = RateWindow(
                usedPercent: percent,
                windowMinutes: nil,
                resetsAt: webBilling.resetsAt,
                resetDescription: nil)
        }

        let identity = ProviderIdentitySnapshot(
            providerID: .grok,
            accountEmail: self.credentials?.email,
            accountOrganization: self.credentials?.teamId,
            loginMethod: self.credentials?.loginMethod)

        return UsageSnapshot(
            primary: primary,
            secondary: nil,
            tertiary: nil,
            updatedAt: self.updatedAt,
            identity: identity)
    }
}

public struct GrokStatusProbe: Sendable {
    public static let teamUsageUnavailableMessage =
        "Grok team usage is unavailable from the current billing surface; identity is still available."

    public init() {}

    public static func detectVersion(env: [String: String] = ProcessInfo.processInfo.environment) -> String? {
        guard let binary = BinaryLocator.resolveGrokBinary(env: env) else { return nil }
        guard let output = ProviderVersionDetector.run(
            path: binary,
            args: ["--version"],
            environment: env,
            mergeStandardError: true)
        else { return nil }
        // Output is like "grok 0.1.210 (8b63e9068c)" — strip the leading "grok " so
        // callers can prefix the CLI name themselves without duplicating it.
        let withoutPrefix = output.replacingOccurrences(
            of: #"^grok\s+"#,
            with: "",
            options: [.regularExpression])
            .trimmingCharacters(in: .whitespacesAndNewlines)
        return withoutPrefix.isEmpty ? nil : withoutPrefix
    }

    public func fetch(env: [String: String] = ProcessInfo.processInfo.environment) async throws -> GrokUsageSnapshot {
        // Credentials are optional: we still show identity-less state if the user
        // hasn't logged in, with a clear hint via the RPC error.
        let credentials = try? GrokCredentialsStore.load(env: env)

        var billing: GrokBillingResponse?
        var rpcError: Error?
        var billingAttempted = false
        do {
            let client = try GrokRPCClient(environment: env)
            defer { client.shutdown() }
            try await client.initialize()
            billingAttempted = true
            billing = try await client.fetchBilling()
        } catch {
            rpcError = error
        }

        // Local fallback summary always succeeds (empty if no sessions yet).
        let localSummary = GrokLocalSessionScanner.summarize(env: env)
        let cliVersion = Self.detectVersion(env: env)

        // `localSummary` is *not* currently projected into a visible RateWindow or
        // identity field, so a stale `~/.grok/sessions/` directory must not
        // suppress the auth-required hint. CLI-only fetches need a billing
        // response; the provider pipeline owns the separate web fallback.
        if billing == nil,
           let credentials,
           Self.shouldUseIdentityOnlyFallback(
               credentials: credentials,
               billingAttempted: billingAttempted,
               error: rpcError)
        {
            return Self.identityOnlySnapshot(
                credentials: credentials,
                localSummary: localSummary,
                cliVersion: cliVersion)
        }

        if billing == nil {
            throw rpcError ?? GrokRPCError.notAuthenticated
        }

        return GrokUsageSnapshot(
            billing: billing,
            webBilling: nil,
            credentials: Self.credentialsForSnapshot(
                credentials: credentials,
                billing: billing,
                webBilling: nil),
            localSummary: localSummary,
            cliVersion: cliVersion,
            updatedAt: Date())
    }

    static func identityOnlySnapshot(
        credentials: GrokCredentials,
        localSummary: GrokLocalSessionSummary?,
        cliVersion: String?,
        updatedAt: Date = .init()) -> GrokUsageSnapshot
    {
        GrokUsageSnapshot(
            billing: nil,
            webBilling: nil,
            credentials: credentials,
            localSummary: localSummary,
            cliVersion: cliVersion,
            updatedAt: updatedAt,
            diagnostic: GrokStatusProbe.teamUsageUnavailableMessage)
    }

    static func isBillingMethodUnavailable(_ error: Error?) -> Bool {
        guard let error,
              case let GrokRPCError.requestFailed(message) = error
        else {
            return false
        }
        let normalized = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        return normalized == "method not found" || normalized.hasPrefix("method not found:")
    }

    static func shouldUseIdentityOnlyFallback(
        credentials: GrokCredentials?,
        billingAttempted: Bool,
        error: Error?) -> Bool
    {
        guard billingAttempted,
              let credentials,
              !credentials.isExpired,
              credentials.isTeamPrincipal
        else {
            return false
        }
        return Self.isBillingMethodUnavailable(error)
    }

    static func credentialsForSnapshot(
        credentials: GrokCredentials?,
        billing: GrokBillingResponse?,
        webBilling: GrokWebBillingSnapshot? = nil) -> GrokCredentials?
    {
        // If remote usage succeeded, xAI accepted auth and the local
        // identity is still useful even when the persisted expires_at is stale.
        if billing != nil || webBilling != nil { return credentials }
        return credentials.flatMap { $0.isExpired ? nil : $0 }
    }

    static func shouldSurfaceRemoteAuthError(_ error: Error?) -> Bool {
        guard let error = error as? GrokWebBillingError else { return false }
        switch error {
        case let .requestFailed(status, _):
            return status == 401 || status == 403
        case let .rpcFailed(status, _):
            return status == 16
        case .missingCredentials, .emptyResponse, .invalidResponse, .teamUsageUnsupported, .parseFailed:
            return false
        }
    }
}
