// versioned json snapshots of store data: the one primitive behind every
// persisted read cache. a snapshot is a disposable replica of server state —
// read() returns undefined (and drops the entry) on corruption, a version
// bump or a parse failure, so stale bytes can never crash the app or leak an
// old shape into new code

export type SnapshotStorage = {
  getItem: (key: string) => string | null
  setItem: (key: string, value: string) => void
  removeItem: (key: string) => void
}

export type Snapshot<T> = {
  read: () => T | undefined
  write: (value: T) => void
  clear: () => void
}

// a parser is total: undefined means reject the snapshot
export type Parse<T> = (raw: unknown) => T | undefined

type SnapshotConfig<T> = {
  key: string
  // bump to invalidate persisted data on shape change
  version: number
  parse: Parse<T>
}

export const createSnapshot = <T>(
  storage: SnapshotStorage,
  {key, version, parse}: SnapshotConfig<T>,
): Snapshot<T> => {
  // cross-store sync fans out no-op commits (a mapped list is a new array
  // even when nothing matched) and every write is a synchronous disk hit,
  // so an identical envelope is never rewritten
  let lastWritten: string | undefined
  const clear = () => {
    lastWritten = undefined
    storage.removeItem(key)
  }
  return {
    read: () => {
      const raw = storage.getItem(key)
      if (raw == null) return undefined
      const envelope = parseJson(raw)
      const value =
        isEnvelope(envelope) && envelope.v === version
          ? parse(envelope.data)
          : undefined
      if (value === undefined) clear()
      return value
    },
    write: (value) => {
      const envelope = JSON.stringify({v: version, data: value})
      if (envelope === lastWritten) return
      lastWritten = envelope
      storage.setItem(key, envelope)
    },
    clear,
  }
}

const parseJson = (raw: string): unknown => {
  try {
    return JSON.parse(raw)
  } catch {
    return undefined
  }
}

const isEnvelope = (
  value: unknown,
): value is {v: unknown; data: unknown} =>
  typeof value === 'object' && value !== null && 'v' in value

// adapt a zod-style schema into a parser without importing zod
export const fromSchema =
  <T>(schema: {
    safeParse: (
      raw: unknown,
    ) => {success: true; data: T} | {success: false}
  }): Parse<T> =>
  (raw) => {
    const result = schema.safeParse(raw)
    return result.success ? result.data : undefined
  }

export const arrayOf =
  <T>(parseItem: Parse<T>): Parse<T[]> =>
  (raw) => {
    if (!Array.isArray(raw)) return undefined
    const items = raw.map(parseItem)
    return items.every((item) => item !== undefined)
      ? (items as T[])
      : undefined
  }

// parse an [id, value][] list (the on-disk form of a keyed snapshot),
// rejecting the whole list if any entry is malformed or its value is rejected
export const parseEntries =
  <P>(parseValue: Parse<P>): Parse<[string, P][]> =>
  (raw) => {
    if (!Array.isArray(raw)) return undefined
    const entries: [string, P][] = []
    for (const entry of raw) {
      if (!Array.isArray(entry) || entry.length !== 2)
        return undefined
      const [id, rawValue] = entry
      if (typeof id !== 'string') return undefined
      const value = parseValue(rawValue)
      if (value === undefined) return undefined
      entries.push([id, value])
    }
    return entries
  }
