import {
  readCodexCredentialsAsync,
  refreshCodexAccessTokenIfNeeded,
} from '../../utils/codexCredentials.js'
import { createCombinedAbortSignal } from '../../utils/combinedAbortSignal.js'
import { logForDebugging } from '../../utils/debug.js'
import { isBareMode } from '../../utils/envUtils.js'
import {
  DEFAULT_CODEX_BASE_URL,
  isCodexBaseUrl,
  resolveRuntimeCodexCredentials,
  resolveProviderRequest,
} from './providerConfig.js'

export type CodexUsageWindow = {
  usedPercent: number
  windowMinutes?: number
  resetsAt?: string
}

export type CodexUsageCredits = {
  hasCredits: boolean
  unlimited: boolean
  balance?: string
}

export type CodexUsageSnapshot = {
  limitName: string
  primary?: CodexUsageWindow
  secondary?: CodexUsageWindow
  credits?: CodexUsageCredits
}

export type CodexUsageData = {
  planType?: string
  snapshots: CodexUsageSnapshot[]
}

export type CodexUsageRow =
  | {
      kind: 'window'
      label: string
      usedPercent: number
      resetsAt?: string
    }
  | {
      kind: 'text'
      label: string
      value: string
    }

type RecordLike = Record<string, unknown>

function isRecord(value: unknown): value is RecordLike {
  return typeof value === 'object' && value !== null
}

function asString(value: unknown): string | undefined {
  return typeof value === 'string' && value.trim() ? value.trim() : undefined
}

function asNumber(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined
}

function asBoolean(value: unknown): boolean | undefined {
  return typeof value === 'boolean' ? value : undefined
}

function toIsoFromUnixSeconds(value: unknown): string | undefined {
  const seconds = asNumber(value)
  if (seconds === undefined) return undefined
  return new Date(seconds * 1000).toISOString()
}

function normalizeWindow(value: unknown): CodexUsageWindow | undefined {
  if (!isRecord(value)) return undefined

  const usedPercent =
    asNumber(value.used_percent) ?? asNumber(value.usedPercent)
  if (usedPercent === undefined) return undefined

  const windowMinutes =
    asNumber(value.window_minutes) ??
    asNumber(value.windowDurationMins) ??
    (() => {
      const seconds = asNumber(value.limit_window_seconds)
      return seconds === undefined ? undefined : Math.round(seconds / 60)
    })()

  const resetsAt =
    toIsoFromUnixSeconds(value.resets_at) ??
    toIsoFromUnixSeconds(value.resetsAt) ??
    toIsoFromUnixSeconds(value.reset_at)

  return {
    usedPercent,
    windowMinutes,
    resetsAt,
  }
}

function normalizeCredits(value: unknown): CodexUsageCredits | undefined {
  if (!isRecord(value)) return undefined

  const hasCredits =
    asBoolean(value.has_credits) ?? asBoolean(value.hasCredits) ?? false
  const unlimited = asBoolean(value.unlimited) ?? false
  const balance = asString(value.balance)

  if (!hasCredits && !unlimited && !balance) {
    return undefined
  }

  return {
    hasCredits,
    unlimited,
    balance,
  }
}

function normalizeSnapshot(
  value: unknown,
  fallbackLimitName: string,
): CodexUsageSnapshot | undefined {
  if (!isRecord(value)) return undefined

  const limitName =
    asString(value.limit_name) ??
    asString(value.limitName) ??
    asString(value.limit_id) ??
    asString(value.limitId) ??
    fallbackLimitName

  const primary =
    normalizeWindow(value.primary) ?? normalizeWindow(value.primary_window)
  const secondary =
    normalizeWindow(value.secondary) ?? normalizeWindow(value.secondary_window)
  const credits = normalizeCredits(value.credits)

  if (!primary && !secondary && !credits) {
    return undefined
  }

  return {
    limitName,
    primary,
    secondary,
    credits,
  }
}

function normalizeSnapshotsFromCollection(
  value: unknown,
  defaultLimitName = 'codex',
): CodexUsageSnapshot[] {
  if (Array.isArray(value)) {
    return value
      .map((item, index) =>
        normalizeSnapshot(
          item,
          index === 0 ? defaultLimitName : `${defaultLimitName}-${index + 1}`,
        ),
      )
      .filter((item): item is CodexUsageSnapshot => item !== undefined)
  }

  if (!isRecord(value)) return []

  return Object.entries(value)
    .map(([key, entry]) => normalizeSnapshot(entry, key))
    .filter((item): item is CodexUsageSnapshot => item !== undefined)
}

function normalizeLiveUsagePayload(payload: RecordLike): CodexUsageData {
  const planType = asString(payload.plan_type) ?? asString(payload.planType)
  const snapshots: CodexUsageSnapshot[] = []
  const codexCredits = normalizeCredits(payload.credits)

  const codexSnapshot = normalizeSnapshot(payload.rate_limit, 'codex')
  if (codexSnapshot) {
    codexSnapshot.credits ??= codexCredits
    snapshots.push(codexSnapshot)
  } else if (codexCredits) {
    snapshots.push({
      limitName: 'codex',
      credits: codexCredits,
    })
  }

  const codeReviewSnapshot = normalizeSnapshot(
    payload.code_review_rate_limit,
    'code review',
  )
  if (codeReviewSnapshot) {
    snapshots.push(codeReviewSnapshot)
  }

  snapshots.push(
    ...normalizeSnapshotsFromCollection(
      payload.additional_rate_limits ?? payload.additionalRateLimits,
      'additional',
    ),
  )

  return {
    planType,
    snapshots,
  }
}

export function normalizeCodexUsagePayload(payload: unknown): CodexUsageData {
  if (Array.isArray(payload)) {
    return {
      snapshots: normalizeSnapshotsFromCollection(payload),
    }
  }

  if (!isRecord(payload)) {
    return { snapshots: [] }
  }

  if (
    'rate_limit' in payload ||
    'code_review_rate_limit' in payload ||
    'additional_rate_limits' in payload ||
    'credits' in payload
  ) {
    return normalizeLiveUsagePayload(payload)
  }

  const collection =
    payload.rate_limits ??
    payload.rateLimits ??
    payload.rate_limits_by_limit_id ??
    payload.rateLimitsByLimitId

  if (collection !== undefined) {
    return {
      planType: asString(payload.plan_type) ?? asString(payload.planType),
      snapshots: normalizeSnapshotsFromCollection(collection),
    }
  }

  const snapshot = normalizeSnapshot(payload, 'codex')
  return {
    planType: asString(payload.plan_type) ?? asString(payload.planType),
    snapshots: snapshot ? [snapshot] : [],
  }
}

function capitalizeFirst(value: string): string {
  if (!value) return value
  return value[0]!.toUpperCase() + value.slice(1)
}

function formatWindowDuration(
  windowMinutes: number | undefined,
  fallback: string,
): string {
  if (windowMinutes === undefined || windowMinutes <= 0) {
    return fallback
  }

  if (windowMinutes === 60 * 24 * 7) {
    return 'weekly'
  }

  if (windowMinutes % (60 * 24) === 0) {
    return `${windowMinutes / (60 * 24)}d`
  }

  if (windowMinutes % 60 === 0) {
    return `${windowMinutes / 60}h`
  }

  return `${windowMinutes}m`
}

function formatCreditBalance(rawBalance: string | undefined): string | undefined {
  const balance = rawBalance?.trim()
  if (!balance) return undefined

  const intValue = Number.parseInt(balance, 10)
  if (Number.isFinite(intValue) && `${intValue}` === balance && intValue > 0) {
    return `${intValue}`
  }

  const floatValue = Number.parseFloat(balance)
  if (Number.isFinite(floatValue) && floatValue > 0) {
    return `${Math.round(floatValue)}`
  }

  return undefined
}

function buildCreditsRow(
  credits: CodexUsageCredits | undefined,
): CodexUsageRow | undefined {
  if (!credits?.hasCredits) return undefined
  if (credits.unlimited) {
    return {
      kind: 'text',
      label: 'Credits',
      value: 'Unlimited',
    }
  }

  const displayBalance = formatCreditBalance(credits.balance)
  if (!displayBalance) return undefined

  return {
    kind: 'text',
    label: 'Credits',
    value: `${displayBalance} credits`,
  }
}

export function buildCodexUsageRows(
  snapshots: CodexUsageSnapshot[],
): CodexUsageRow[] {
  const rows: CodexUsageRow[] = []

  for (const snapshot of snapshots) {
    const limitBucketLabel = snapshot.limitName.trim() || 'codex'
    const creditsRow = buildCreditsRow(snapshot.credits)
    const hasRenderableContent =
      snapshot.primary !== undefined ||
      snapshot.secondary !== undefined ||
      creditsRow !== undefined
    if (!hasRenderableContent) {
      continue
    }

    const showLimitPrefix = limitBucketLabel.toLowerCase() !== 'codex'
    const windowCount =
      Number(snapshot.primary !== undefined) +
      Number(snapshot.secondary !== undefined)
    const combineNonCodexSingleLimit = showLimitPrefix && windowCount === 1

    if (showLimitPrefix && !combineNonCodexSingleLimit) {
      rows.push({
        kind: 'text',
        label: `${capitalizeFirst(limitBucketLabel)} limit`,
        value: '',
      })
    }

    if (snapshot.primary) {
      const durationLabel = capitalizeFirst(
        formatWindowDuration(snapshot.primary.windowMinutes, '5h'),
      )
      rows.push({
        kind: 'window',
        label: combineNonCodexSingleLimit
          ? `${capitalizeFirst(limitBucketLabel)} ${durationLabel} limit`
          : `${durationLabel} limit`,
        usedPercent: snapshot.primary.usedPercent,
        resetsAt: snapshot.primary.resetsAt,
      })
    }

    if (snapshot.secondary) {
      const durationLabel = capitalizeFirst(
        formatWindowDuration(snapshot.secondary.windowMinutes, 'weekly'),
      )
      rows.push({
        kind: 'window',
        label: combineNonCodexSingleLimit
          ? `${capitalizeFirst(limitBucketLabel)} ${durationLabel} limit`
          : `${durationLabel} limit`,
        usedPercent: snapshot.secondary.usedPercent,
        resetsAt: snapshot.secondary.resetsAt,
      })
    }

    if (creditsRow) {
      rows.push(creditsRow)
    }
  }

  return rows
}

export function formatCodexPlanType(
  planType: string | undefined,
): string | undefined {
  if (!planType) return undefined
  return planType
    .split(/[_\s-]+/)
    .filter(Boolean)
    .map(part => capitalizeFirst(part.toLowerCase()))
    .join(' ')
}

export function getCodexUsageUrl(baseUrl = DEFAULT_CODEX_BASE_URL): string {
  return new URL('/backend-api/wham/usage', baseUrl).toString()
}

export async function fetchCodexUsage(): Promise<CodexUsageData> {
  const refreshResult = await refreshCodexAccessTokenIfNeeded().catch(
    async error => {
      logForDebugging(
        `[codex] access token refresh failed before usage fetch: ${error instanceof Error ? error.message : String(error)}`,
        { level: 'warn' },
      )
      return {
        refreshed: false,
        credentials: await readCodexCredentialsAsync(),
      }
    },
  )
  const request = resolveProviderRequest({
    model: process.env.OPENAI_MODEL,
    baseUrl: process.env.OPENAI_BASE_URL,
  })
  if (!isCodexBaseUrl(request.baseUrl)) {
    throw new Error(
      'Codex usage is only available with the official ChatGPT Codex backend.',
    )
  }

  const credentials = resolveRuntimeCodexCredentials({
    storedCredentials: refreshResult.credentials,
  })
  if (!credentials.apiKey) {
    const oauthHint = isBareMode() ? '' : ', choose Codex OAuth in /provider'
    const authHint = credentials.authPath
      ? `${oauthHint} or place a Codex auth.json at ${credentials.authPath}`
      : oauthHint
    throw new Error(`Codex auth is required. Set CODEX_API_KEY${authHint}.`)
  }
  if (!credentials.accountId) {
    throw new Error(
      'Codex auth is missing chatgpt_account_id. Re-login with Codex OAuth, the Codex CLI, or set CHATGPT_ACCOUNT_ID/CODEX_ACCOUNT_ID.',
    )
  }

  const { signal, cleanup } = createCombinedAbortSignal(undefined, {
    timeoutMs: 5000,
  })
  try {
    const response = await fetch(getCodexUsageUrl(request.baseUrl), {
      method: 'GET',
      headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${credentials.apiKey}`,
        'chatgpt-account-id': credentials.accountId,
        originator: 'openclaude',
      },
      signal,
    })

    if (!response.ok) {
      const errorBody = await response.text().catch(() => 'unknown error')
      throw new Error(`Codex usage error ${response.status}: ${errorBody}`)
    }

    return normalizeCodexUsagePayload(await response.json())
  } finally {
    cleanup()
  }
}
