import {
  StorageError,
  assertExactStorageKey,
  assertPresignExpiry,
  type ListOptions,
  type ListResult,
  type PresignOptions,
  type PresignedUrl,
  type PutOptions,
  type PutResult,
  type StorageAdapter,
  type StorageBody,
  type StorageObject,
  type StorageObjectHead,
} from './storage.js'

interface MemoryEntry {
  bytes: Uint8Array
  head: StorageObjectHead
}

async function bodyToBytes(body: StorageBody): Promise<Uint8Array> {
  if (body instanceof Uint8Array) return body
  if (body instanceof ArrayBuffer) return new Uint8Array(body)
  if (ArrayBuffer.isView(body)) return new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
  if (typeof Blob !== 'undefined' && body instanceof Blob) return new Uint8Array(await body.arrayBuffer())
  // ReadableStream
  const reader = (body as ReadableStream<Uint8Array>).getReader()
  const chunks: Uint8Array[] = []
  for (;;) {
    const { done, value } = await reader.read()
    if (done) break
    if (value) chunks.push(value)
  }
  const total = chunks.reduce((n, c) => n + c.length, 0)
  const out = new Uint8Array(total)
  let off = 0
  for (const c of chunks) {
    out.set(c, off)
    off += c.length
  }
  return out
}

/** Cheap deterministic content etag (djb2 hex) — test double only, not a cryptographic digest. */
function etagOf(bytes: Uint8Array): string {
  let h = 5381
  for (let i = 0; i < bytes.length; i++) h = ((h << 5) + h + bytes[i]!) >>> 0
  return `"${h.toString(16)}"`
}

function streamOf(bytes: Uint8Array): ReadableStream<Uint8Array> {
  return new ReadableStream<Uint8Array>({
    start(controller) {
      controller.enqueue(bytes)
      controller.close()
    },
  })
}

/** In-memory StorageAdapter — the second real backend (a Map, not a mock of R2). */
export function createMemoryStorage(): StorageAdapter {
  const store = new Map<string, MemoryEntry>()

  return {
    async put(key, body, opts?: PutOptions): Promise<PutResult> {
      const bytes = await bodyToBytes(body)
      const head: StorageObjectHead = {
        key,
        size: bytes.length,
        etag: etagOf(bytes),
        contentType: opts?.contentType,
        customMetadata: opts?.customMetadata,
        uploadedAt: undefined,
      }
      store.set(key, { bytes, head })
      return { key, size: bytes.length, etag: head.etag }
    },

    async get(key): Promise<StorageObject | null> {
      const entry = store.get(key)
      if (!entry) return null
      return { ...entry.head, body: streamOf(entry.bytes) }
    },

    async head(key): Promise<StorageObjectHead | null> {
      return store.get(key)?.head ?? null
    },

    async delete(key): Promise<void> {
      store.delete(key)
    },

    async list(opts?: ListOptions): Promise<ListResult> {
      const prefix = opts?.prefix ?? ''
      const limit = Math.min(Math.max(opts?.limit ?? 1000, 1), 1000)
      const offset = opts?.cursor ? Number.parseInt(opts.cursor, 10) || 0 : 0
      const all = [...store.values()].map((e) => e.head).filter((h) => h.key.startsWith(prefix)).sort((a, b) => a.key.localeCompare(b.key))
      const page = all.slice(offset, offset + limit)
      const next = offset + limit
      return { objects: page, cursor: next < all.length ? String(next) : undefined }
    },

    async presignPut(key, opts: PresignOptions): Promise<PresignedUrl> {
      assertExactStorageKey(key)
      assertPresignExpiry(opts.expiresIn)
      return { url: `memory://${key}?expires=${opts.expiresIn}`, method: 'PUT', headers: {}, expiresIn: opts.expiresIn }
    },

    async presignGet(key, opts: PresignOptions): Promise<PresignedUrl> {
      assertExactStorageKey(key)
      assertPresignExpiry(opts.expiresIn)
      return { url: `memory://${key}?expires=${opts.expiresIn}&op=get`, method: 'GET', headers: {}, expiresIn: opts.expiresIn }
    },
  }
}

// Re-export so a consumer can branch on storage failures from this entry too.
export { StorageError }
