import { putObject, type R2PutBucket } from './r2-binding.js'
import { presignR2Put, type PresignR2Creds } from './presign-s3compat.js'
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 R2HttpMetadataLike {
  contentType?: string
  cacheControl?: string
}
interface R2ObjectLike {
  key: string
  size: number
  etag: string
  httpMetadata?: R2HttpMetadataLike
  customMetadata?: Record<string, string>
  uploaded?: Date
}
interface R2ObjectBodyLike extends R2ObjectLike {
  body: ReadableStream<Uint8Array>
}
interface R2ListLike {
  objects: R2ObjectLike[]
  truncated: boolean
  cursor?: string
}

/** Structural R2Bucket surface the adapter consumes. The native Workers `R2Bucket` satisfies it. */
export interface R2StorageBucket extends R2PutBucket {
  get(key: string): Promise<R2ObjectBodyLike | null>
  head(key: string): Promise<R2ObjectLike | null>
  delete(key: string): Promise<void>
  list(opts?: { prefix?: string; cursor?: string; limit?: number }): Promise<R2ListLike>
}

export interface CreateR2StorageOptions {
  bucket: R2StorageBucket
  /** S3-API creds for presign — R2 native binding cannot presign. Omit on public-bucket hosts. */
  presign?: PresignR2Creds
}

function headOf(o: R2ObjectLike): StorageObjectHead {
  return {
    key: o.key,
    size: o.size,
    etag: o.etag,
    contentType: o.httpMetadata?.contentType,
    customMetadata: o.customMetadata,
    uploadedAt: o.uploaded?.getTime(),
  }
}

/** CF R2 StorageAdapter — the mod-cms production backend. Wraps the existing putObject/presignR2Put leaves. */
export function createR2Storage(options: CreateR2StorageOptions): StorageAdapter {
  const { bucket, presign } = options

  return {
    async put(key, body: StorageBody, opts?: PutOptions): Promise<PutResult> {
      const result = await putObject(bucket, key, body, {
        httpMetadata: { contentType: opts?.contentType, cacheControl: opts?.cacheControl },
        customMetadata: opts?.customMetadata,
      })
      return { key: result.key, size: result.size, etag: result.etag }
    },

    async get(key): Promise<StorageObject | null> {
      const o = await bucket.get(key)
      if (!o) return null
      return { ...headOf(o), body: o.body }
    },

    async head(key): Promise<StorageObjectHead | null> {
      const o = await bucket.head(key)
      return o ? headOf(o) : null
    },

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

    async list(opts?: ListOptions): Promise<ListResult> {
      const res = await bucket.list({ prefix: opts?.prefix, cursor: opts?.cursor, limit: opts?.limit })
      return { objects: res.objects.map(headOf), cursor: res.truncated ? res.cursor : undefined }
    },

    async presignPut(key, opts: PresignOptions): Promise<PresignedUrl> {
      if (!presign) {
        throw new StorageError('backend', 'presignPut requires S3-API creds (createR2Storage was given no `presign`)')
      }
      assertExactStorageKey(key)
      assertPresignExpiry(opts.expiresIn)
      try {
        const signed = await presignR2Put(presign, key, opts.contentType ?? 'application/octet-stream', opts.expiresIn)
        return { url: signed.url, method: 'PUT', headers: signed.headers, expiresIn: opts.expiresIn }
      } catch (e) {
        if ((e as { name?: string }).name === 'InvalidPresignKeyError') {
          throw new StorageError('invalid_key', (e as Error).message)
        }
        throw e
      }
    },
    // presignGet intentionally omitted — public-bucket default (CLAUDE.md spec §4.2).
  }
}

export { StorageError }
