import {Directory, File, Paths} from 'expo-file-system'

import {tolerateStatus} from '@/lib/errors'

// durable staging for deferred image uploads. a picker/downscaled jpeg lives in
// the cache dir the os may purge, so an offline-queued upload copies it here —
// safe from eviction — and reads this copy when it finally replays. files are
// named by the outbox entry id, so each queued upload owns exactly one. the
// native counterpart to snapshot-cache's storage adapter: kept in its own
// module so pure code and tests never import native file-system code

const dir = new Directory(Paths.document, 'outbox-images')

const stagedFile = (id: string): File => new File(dir, `${id}.jpg`)

// the durable file:// uri for a staged image, derived from the entry id so it
// resolves the same across restarts (expo-image renders file:// natively, so
// this doubles as the optimistic preview uri until the server copy lands)
export const stagedImageUri = (id: string): string =>
  stagedFile(id).uri

// copy a downscaled jpeg into the durable dir under the entry id, returning its
// file:// uri. idempotent: re-staging a redriven upload overwrites in place
export const stageImage = async (
  id: string,
  sourceUri: string,
): Promise<string> => {
  dir.create({intermediates: true, idempotent: true})
  const dest = stagedFile(id)
  await new File(sourceUri).copy(dest, {overwrite: true})
  return dest.uri
}

// whether a staged copy still exists — a replayed upload checks before reading
// so a purged/missing file resolves as moot instead of throwing an fs error
export const hasStagedImage = (id: string): boolean =>
  stagedFile(id).exists

// free a staged file once its upload lands, or its queued upload is discarded
export const deleteStagedImage = (id: string): void => {
  const file = stagedFile(id)
  if (file.exists) file.delete()
}

// a queued image write is either a set carrying its staged file's id, or a
// remove — payloads embed this next to their owner's id
export type ImageAction =
  | {action: 'set'; imageId: string}
  | {action: 'remove'}

// the shared outbox handler for a queued image set/remove on one entity (a
// crumb's image, a trail's cover). run: skip a purged staged copy (it would
// throw the same fs error on every replay — moot), replay the write,
// reconcile the owner on the chain's last ack, swallow a 404 (the entity is
// gone, so its image is too), then free the staged file. rollback: a
// discarded 'set' clears the optimistic staged uri first — so no holder keeps
// rendering the deleted file — then drops it; a discarded 'remove' refetches
// the server copy the optimistic clear hid
export const imageOutboxHandler = <T, P extends ImageAction>({
  entityId,
  upload,
  remove,
  shouldReconcile,
  reconcile,
  clearOptimistic,
  restore,
}: {
  entityId: (payload: P) => string
  upload: (id: string, uri: string) => Promise<T>
  remove: (id: string) => Promise<T>
  shouldReconcile: (id: string) => boolean
  reconcile: (entity: T) => void
  clearOptimistic: (id: string, stagedUri: string) => void
  restore: (id: string) => void
}) => ({
  run: async (payload: P): Promise<void> => {
    const id = entityId(payload)
    if (payload.action === 'set' && !hasStagedImage(payload.imageId))
      return
    await tolerateStatus(404, async () => {
      const entity =
        payload.action === 'remove'
          ? await remove(id)
          : await upload(id, stagedImageUri(payload.imageId))
      if (shouldReconcile(id)) reconcile(entity)
    })
    if (payload.action === 'set') deleteStagedImage(payload.imageId)
  },
  rollback: (payload: P): void => {
    if (payload.action === 'remove') {
      restore(entityId(payload))
      return
    }
    clearOptimistic(
      entityId(payload),
      stagedImageUri(payload.imageId),
    )
    deleteStagedImage(payload.imageId)
  },
})

// wipe every staged file — runs on the outbox reset (sign-out / account switch
// / 401), where the queue is cleared, so no entry references these anymore.
// keeps the privacy seam whole: one account's pending images never survive to
// the next session on disk
export const clearStagedImages = (): void => {
  if (dir.exists) dir.delete()
}
