import { normalizeText } from './utils'

export const SESSION_QUEUE_LIMIT = 5

export type SessionQueueStatus = 'idle' | 'running' | 'paused_confirm' | 'error'
export type SessionQueueItemKind = 'user' | 'steer'

export type SessionQueueErrorCode
  = | 'network_error'
    | 'timeout'
    | 'canceled_by_user'
    | 'rate_limited'
    | 'config_unavailable'
    | 'unknown'

export type SessionQueueConfigSnapshot = Readonly<{
  model?: string
  temperature?: number
  interleavedThinking?: boolean
  approvalsMode?: 'auto' | 'normal' | 'strict'
  providerId?: string
}>

export type SessionQueueItem = Readonly<{
  id: string
  text: string
  createdAt: number
  kind: SessionQueueItemKind
  steeredAt?: number
  config?: SessionQueueConfigSnapshot
}>

export type ReasoningDiagnosticState = 'visible' | 'tokens_only' | 'none'
export type ReasoningDiagnosticsByTurn = Readonly<Record<string, ReasoningDiagnosticState>>

export type SessionUiMetadata = Readonly<{
  queue: SessionQueueItem[]
  queueLength: number
  status: SessionQueueStatus
  lastUpdatedAt: number
  lastErrorCode?: SessionQueueErrorCode
  pinnedMessageIds: string[]
  titleSuggestedAt?: number
  reasoningDiagnosticsByTurn: ReasoningDiagnosticsByTurn
}>

const VALID_QUEUE_STATUS = new Set<SessionQueueStatus>([
  'idle',
  'running',
  'paused_confirm',
  'error',
])
const VALID_ERROR_CODES = new Set<SessionQueueErrorCode>([
  'network_error',
  'timeout',
  'canceled_by_user',
  'rate_limited',
  'config_unavailable',
  'unknown',
])
const VALID_REASONING_DIAGNOSTIC_STATES = new Set<ReasoningDiagnosticState>([
  'visible',
  'tokens_only',
  'none',
])

function isRecord(value: unknown): value is Record<string, unknown> {
  return value != null && typeof value === 'object' && !Array.isArray(value)
}

function normalizeApprovalsMode(
  value: unknown,
): SessionQueueConfigSnapshot['approvalsMode'] {
  return value === 'auto' || value === 'normal' || value === 'strict'
    ? value
    : undefined
}

function readNestedValue(root: unknown, path: readonly string[]): unknown {
  let current = root
  for (const key of path) {
    if (!isRecord(current))
      return undefined
    current = current[key]
  }
  return current
}

function normalizeReasoningDiagnosticsByTurn(
  raw: unknown,
): ReasoningDiagnosticsByTurn {
  if (!isRecord(raw))
    return {}
  const out: Record<string, ReasoningDiagnosticState> = {}
  for (const [turnId, state] of Object.entries(raw)) {
    const normalizedTurnId = String(turnId ?? '').trim()
    if (!normalizedTurnId)
      continue
    const normalizedState = String(state ?? '').trim() as ReasoningDiagnosticState
    if (!VALID_REASONING_DIAGNOSTIC_STATES.has(normalizedState))
      continue
    out[normalizedTurnId] = normalizedState
  }
  return out
}

function sanitizeQueueText(text: unknown): string {
  const raw = normalizeText(String(text ?? ''))
  if (!raw)
    return ''
  // Strip control chars except newline/tab.
  let out = ''
  for (let i = 0; i < raw.length; i++) {
    const code = raw.charCodeAt(i)
    if (code === 0x09 || code === 0x0A) {
      out += raw[i]
      continue
    }
    if (code < 0x20 || code === 0x7F)
      continue
    out += raw[i]
  }
  return out
}

function sortQueueItems(
  items: readonly SessionQueueItem[],
): SessionQueueItem[] {
  return [...items].sort((a, b) => {
    if (a.kind !== b.kind)
      return a.kind === 'steer' ? -1 : 1

    if (a.kind === 'steer' && b.kind === 'steer') {
      const aSteeredAt = Number(a.steeredAt ?? a.createdAt ?? 0)
      const bSteeredAt = Number(b.steeredAt ?? b.createdAt ?? 0)
      if (aSteeredAt !== bSteeredAt)
        return aSteeredAt - bSteeredAt
    }

    const aCreatedAt = Number(a.createdAt ?? 0)
    const bCreatedAt = Number(b.createdAt ?? 0)
    if (aCreatedAt !== bCreatedAt)
      return aCreatedAt - bCreatedAt

    return String(a.id).localeCompare(String(b.id))
  })
}

function normalizeQueueItem(raw: unknown): SessionQueueItem | null {
  if (!isRecord(raw))
    return null
  const id = String(raw.id ?? '').trim()
  const text = sanitizeQueueText(raw.text)
  if (!id || !text)
    return null
  const createdAt = Number(raw.createdAt ?? 0)
  const rawKind = String(raw.kind ?? '').trim()
  const kind: SessionQueueItemKind = rawKind === 'steer' ? 'steer' : 'user'
  const steeredAt = Number(raw.steeredAt ?? 0)
  const cfg = raw.config
  let config: SessionQueueConfigSnapshot | undefined
  if (isRecord(cfg)) {
    config = {
      model: String(cfg.model ?? '').trim() || undefined,
      temperature: Number.isFinite(Number(cfg.temperature ?? Number.NaN))
        ? Number(cfg.temperature)
        : undefined,
      interleavedThinking:
        typeof cfg.interleavedThinking === 'boolean'
          ? cfg.interleavedThinking
          : undefined,
      approvalsMode: normalizeApprovalsMode(cfg.approvalsMode),
      providerId: String(cfg.providerId ?? '').trim() || undefined,
    }
  }
  return {
    id,
    text,
    createdAt: Number.isFinite(createdAt) ? createdAt : 0,
    kind,
    steeredAt:
      kind === 'steer' && Number.isFinite(steeredAt) && steeredAt > 0
        ? steeredAt
        : undefined,
    ...(config ? { config } : {}),
  } satisfies SessionQueueItem
}

export function defaultSessionUiMetadata(): SessionUiMetadata {
  return {
    queue: [],
    queueLength: 0,
    status: 'idle',
    lastUpdatedAt: 0,
    lastErrorCode: undefined,
    pinnedMessageIds: [],
    titleSuggestedAt: undefined,
    reasoningDiagnosticsByTurn: {},
  }
}

export function normalizeSessionUiMetadata(raw: unknown): SessionUiMetadata {
  const base = defaultSessionUiMetadata()
  if (!isRecord(raw))
    return base
  const queueRaw = Array.isArray(raw.queue) ? raw.queue : []
  const queue = sortQueueItems(
    queueRaw
      .map(normalizeQueueItem)
      .filter((item): item is SessionQueueItem => Boolean(item))
      .slice(0, SESSION_QUEUE_LIMIT),
  )
  const statusRaw = String(raw.status ?? '').trim() as SessionQueueStatus
  const status = VALID_QUEUE_STATUS.has(statusRaw) ? statusRaw : base.status
  const lastUpdatedAt = Number(raw.lastUpdatedAt ?? 0)
  const lastErrorCodeRaw = String(raw.lastErrorCode ?? '').trim() as SessionQueueErrorCode
  const lastErrorCode = VALID_ERROR_CODES.has(lastErrorCodeRaw)
    ? lastErrorCodeRaw
    : undefined
  const pinnedMessageIds = Array.isArray(raw.pinnedMessageIds)
    ? raw.pinnedMessageIds
        .map((id: unknown) => String(id ?? '').trim())
        .filter(Boolean)
    : []
  const titleSuggestedAt = Number(raw.titleSuggestedAt ?? 0)
  const reasoningDiagnosticsByTurn = normalizeReasoningDiagnosticsByTurn(
    raw.reasoningDiagnosticsByTurn,
  )
  return {
    queue,
    queueLength: queue.length,
    status,
    lastUpdatedAt: Number.isFinite(lastUpdatedAt) ? lastUpdatedAt : 0,
    lastErrorCode,
    pinnedMessageIds,
    titleSuggestedAt:
      Number.isFinite(titleSuggestedAt) && titleSuggestedAt > 0
        ? titleSuggestedAt
        : undefined,
    reasoningDiagnosticsByTurn,
  }
}

export function readSessionUiMetadataFromSnapshot(
  snapshot: unknown,
): SessionUiMetadata {
  const meta
    = readNestedValue(snapshot, ['metadata', 'ui'])
      ?? readNestedValue(snapshot, ['metadata', 'dimAgentRuntime', 'metadata', 'ui'])
      ?? readNestedValue(snapshot, ['metadata', 'goatchainUi'])
      ?? readNestedValue(snapshot, ['metadata', 'goatchain'])
      ?? null
  return normalizeSessionUiMetadata(meta)
}

export function mergeSessionUiMetadata(
  prev: SessionUiMetadata,
  patch: Partial<SessionUiMetadata>,
): SessionUiMetadata {
  const nextQueue = Array.isArray(patch.queue) ? patch.queue : prev.queue
  const queue = sortQueueItems(nextQueue).slice(0, SESSION_QUEUE_LIMIT)
  const queueLength = queue.length
  const status
    = patch.status && VALID_QUEUE_STATUS.has(patch.status)
      ? patch.status
      : prev.status
  const lastUpdatedAt = Number.isFinite(Number(patch.lastUpdatedAt))
    ? Number(patch.lastUpdatedAt)
    : prev.lastUpdatedAt
  const lastErrorCode = Object.prototype.hasOwnProperty.call(
    patch,
    'lastErrorCode',
  )
    ? patch.lastErrorCode === undefined
      ? undefined
      : patch.lastErrorCode && VALID_ERROR_CODES.has(patch.lastErrorCode)
        ? patch.lastErrorCode
        : prev.lastErrorCode
    : prev.lastErrorCode
  const pinnedMessageIds = Array.isArray(patch.pinnedMessageIds)
    ? patch.pinnedMessageIds
    : prev.pinnedMessageIds
  const titleSuggestedAt = Number.isFinite(Number(patch.titleSuggestedAt))
    ? Number(patch.titleSuggestedAt)
    : prev.titleSuggestedAt
  const reasoningDiagnosticsByTurn = Object.prototype.hasOwnProperty.call(
    patch,
    'reasoningDiagnosticsByTurn',
  )
    ? normalizeReasoningDiagnosticsByTurn(patch.reasoningDiagnosticsByTurn)
    : prev.reasoningDiagnosticsByTurn
  return {
    queue,
    queueLength,
    status,
    lastUpdatedAt,
    lastErrorCode,
    pinnedMessageIds,
    titleSuggestedAt,
    reasoningDiagnosticsByTurn,
  }
}
