export type LastRequestBudget = Readonly<{
  runId?: string
  requestId?: string
  autoBudgetOutcome: 'budget-unknown' | 'compacted' | 'failed-to-fit' | 'not-needed'
  autoBudgetReasonCode?: string
  budgetSource?: string
  contextWindowTokens?: number
  estimatedInputTokens: number
  inputBudgetTokens?: number
  maxOutputTokens?: number
  protocolReserveTokens?: number
  reservedOutputTokens?: number
  thresholdTokens?: number
  maxInputTokens?: number
  triggerRatio?: number
  usedAutoCompaction: boolean
  updatedAt?: string
  updatedAtMs: number
}>

function normalizePositiveNumber(value: unknown): number | null {
  const n = Number(value)
  if (!Number.isFinite(n) || n <= 0)
    return null
  return Math.floor(n)
}

function normalizeNonNegativeNumber(value: unknown): number | null {
  const n = Number(value)
  if (!Number.isFinite(n) || n < 0)
    return null
  return Math.floor(n)
}

export function normalizeLastRequestBudget(raw: unknown): LastRequestBudget | null {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
    return null
  const record = raw as Record<string, unknown>
  const estimatedInputTokens = normalizeNonNegativeNumber(
    record.estimatedInputTokens,
  )
  const autoBudgetOutcome = String(record.autoBudgetOutcome ?? '').trim()
  const inputBudgetTokens = normalizePositiveNumber(record.inputBudgetTokens)
  const contextWindowTokens = normalizePositiveNumber(record.contextWindowTokens)
  const maxOutputTokens = normalizeNonNegativeNumber(record.maxOutputTokens)
  const protocolReserveTokens
    = normalizeNonNegativeNumber(record.protocolReserveTokens)
  const reservedOutputTokens
    = normalizeNonNegativeNumber(record.reservedOutputTokens)
  const thresholdTokens = normalizePositiveNumber(record.thresholdTokens)
  const maxInputTokens = normalizePositiveNumber(record.maxInputTokens)
  const triggerRatio = Number(record.triggerRatio)
  const updatedAtMs
    = normalizePositiveNumber(record.updatedAtMs) ?? Date.now()
  if (
    estimatedInputTokens == null
    || (
      autoBudgetOutcome !== 'not-needed'
      && autoBudgetOutcome !== 'compacted'
      && autoBudgetOutcome !== 'failed-to-fit'
      && autoBudgetOutcome !== 'budget-unknown'
    )
  ) {
    return null
  }
  return {
    runId: String(record.runId ?? '').trim() || undefined,
    requestId: String(record.requestId ?? '').trim() || undefined,
    autoBudgetOutcome,
    autoBudgetReasonCode: String(record.autoBudgetReasonCode ?? '').trim() || undefined,
    budgetSource: String(record.budgetSource ?? '').trim() || undefined,
    ...(contextWindowTokens != null ? { contextWindowTokens } : {}),
    estimatedInputTokens,
    ...(inputBudgetTokens != null ? { inputBudgetTokens } : {}),
    ...(maxOutputTokens != null ? { maxOutputTokens } : {}),
    ...(protocolReserveTokens != null ? { protocolReserveTokens } : {}),
    ...(reservedOutputTokens != null ? { reservedOutputTokens } : {}),
    ...(thresholdTokens != null ? { thresholdTokens } : {}),
    ...(maxInputTokens != null ? { maxInputTokens } : {}),
    ...(
      Number.isFinite(triggerRatio) && triggerRatio > 0
        ? { triggerRatio }
        : {}
    ),
    usedAutoCompaction: record.usedAutoCompaction === true,
    updatedAt: String(record.updatedAt ?? '').trim() || undefined,
    updatedAtMs,
  }
}

export function cloneLastRequestBudget(
  budget: LastRequestBudget | null | undefined,
): LastRequestBudget | null {
  return budget ? { ...budget } : null
}

export function computeLastRequestBudgetPct(
  budget: LastRequestBudget | null | undefined,
): number | null {
  if (!budget)
    return null
  const inputBudgetTokens = normalizePositiveNumber(
    budget.inputBudgetTokens ?? budget.maxInputTokens,
  )
  if (inputBudgetTokens == null)
    return null
  return Math.min(
    100,
    Math.max(
      0,
      Math.round(
        budget.estimatedInputTokens / Math.max(1, inputBudgetTokens) * 100,
      ),
    ),
  )
}

type RequestBudgetState = {
  chromeVersion: number
  config: {
    context_window: number
  }
  lastRequestBudget: LastRequestBudget | null
  usage: {
    budget?: number
  }
}

type RequestBudgetPersistence = Readonly<{
  applyLastRequestBudget: (raw: unknown) => void
}>

function budgetsEqual(
  left: LastRequestBudget | null | undefined,
  right: LastRequestBudget | null | undefined,
): boolean {
  if (!left && !right)
    return true
  if (!left || !right)
    return false
  return (
    left.runId === right.runId
    && left.requestId === right.requestId
    && left.autoBudgetOutcome === right.autoBudgetOutcome
    && left.autoBudgetReasonCode === right.autoBudgetReasonCode
    && left.budgetSource === right.budgetSource
    && left.contextWindowTokens === right.contextWindowTokens
    && left.estimatedInputTokens === right.estimatedInputTokens
    && left.inputBudgetTokens === right.inputBudgetTokens
    && left.maxOutputTokens === right.maxOutputTokens
    && left.protocolReserveTokens === right.protocolReserveTokens
    && left.reservedOutputTokens === right.reservedOutputTokens
    && left.thresholdTokens === right.thresholdTokens
    && left.maxInputTokens === right.maxInputTokens
    && left.triggerRatio === right.triggerRatio
    && left.usedAutoCompaction === right.usedAutoCompaction
    && left.updatedAt === right.updatedAt
    && left.updatedAtMs === right.updatedAtMs
  )
}

export function createSessionRequestBudgetPersistence(
  opts: Readonly<{
    state: RequestBudgetState
  }>,
): RequestBudgetPersistence {
  const { state } = opts

  function applyLastRequestBudget(raw: unknown): void {
    let nextBudget = normalizeLastRequestBudget(raw)
    const currentBudget = normalizeLastRequestBudget(state.lastRequestBudget)
    if (
      currentBudget
      && nextBudget
      && currentBudget.estimatedInputTokens > 0
      && nextBudget.estimatedInputTokens <= 0
    ) {
      nextBudget = currentBudget
    }
    if (budgetsEqual(currentBudget, nextBudget))
      return
    state.lastRequestBudget = nextBudget
    if (nextBudget?.inputBudgetTokens) {
      state.usage = {
        ...state.usage,
        budget: nextBudget.inputBudgetTokens,
      }
    } else if (nextBudget?.maxInputTokens) {
      state.usage = {
        ...state.usage,
        budget: nextBudget.maxInputTokens,
      }
    } else {
      const configuredContextWindow = normalizePositiveNumber(
        state.config.context_window,
      )
      state.usage = {
        ...state.usage,
        ...(configuredContextWindow != null
          ? { budget: configuredContextWindow }
          : {}),
      }
    }
    state.chromeVersion += 1
  }

  return {
    applyLastRequestBudget,
  }
}

export function computeLastRequestBudgetTriggerPct(
  budget: LastRequestBudget | null | undefined,
): number | null {
  if (!budget)
    return null
  const thresholdTokens = normalizePositiveNumber(
    budget.thresholdTokens ?? budget.inputBudgetTokens,
  )
  const maxInputTokens = normalizePositiveNumber(
    budget.maxInputTokens ?? budget.inputBudgetTokens,
  )
  if (thresholdTokens == null || maxInputTokens == null)
    return null
  return Math.max(
    0,
    Math.round(
      thresholdTokens / Math.max(1, maxInputTokens) * 100,
    ),
  )
}
