/**
 * desktop/src/live-poll.ts — B2: validate + map GET /live-sessions.
 *
 * The response body is untrusted external JSON (`unknown`), so every field is
 * hand-checked before it becomes a SessionStatusSnapshot — matching the repo's
 * no-validation-library convention (src/config.ts hand-parses everything).
 * Boundary contract: never throw. Garbage in → [] or filtered-out elements out.
 *
 * The /live-sessions endpoint carries id/status/cwd but not approval state, so
 * pendingApproval is fixed false and gate null here; approval snapshots come
 * from the hook status bus, not this poll.
 */

import type { DesktopClaudeStatus, SessionStatusSnapshot } from './types.js'

/** Valid ClaudeStatus literals (mirrors backend ClaudeStatus, src/types.ts). */
const VALID_STATUSES: ReadonlySet<string> = new Set([
  'working',
  'waiting',
  'idle',
  'unknown',
  'stuck',
])

/**
 * Map the untrusted /live-sessions JSON body to snapshots. A non-array body
 * yields []; elements missing a string id or a valid status literal are dropped.
 */
export function mapLiveSessions(raw: unknown): SessionStatusSnapshot[] {
  if (!Array.isArray(raw)) return []

  const snapshots: SessionStatusSnapshot[] = []
  for (const element of raw) {
    const snapshot = toSnapshot(element)
    if (snapshot !== null) snapshots.push(snapshot)
  }
  return snapshots
}

/** Narrow one untrusted element to a snapshot, or null if it is invalid. */
function toSnapshot(element: unknown): SessionStatusSnapshot | null {
  if (!isRecord(element)) return null

  const { id, status, cwd, exited } = element
  if (typeof id !== 'string' || id === '') return null
  if (typeof status !== 'string' || !VALID_STATUSES.has(status)) return null
  // Drop already-exited sessions: the backend keeps them in list() until they
  // are reclaimed, but a post-exit status flip must not fire a stale "needs
  // attention" notification.
  if (exited === true) return null

  return {
    sessionId: id,
    claudeStatus: status as DesktopClaudeStatus,
    pendingApproval: false,
    gate: null,
    title: titleFromCwd(cwd),
  }
}

/** Last path segment of a cwd string, or undefined when cwd is absent/blank.
 *  Splits on both separators so a Windows backslash path (an explicitly
 *  supported target — shell.ts defaults to powershell.exe) yields the folder,
 *  not the whole path. */
function titleFromCwd(cwd: unknown): string | undefined {
  if (typeof cwd !== 'string' || cwd === '') return undefined
  const segments = cwd.split(/[\\/]/).filter((segment) => segment !== '')
  const last = segments[segments.length - 1]
  return last === undefined ? undefined : last
}

/** Type guard: value is a non-null plain object (indexable by string). */
function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}
