import { AwsClient } from 'aws4fetch'

export interface PresignR2Creds {
  endpoint: string
  accessKeyId: string
  secretAccessKey: string
  region?: string
}

export interface PresignedR2Put {
  url: string
  method: 'PUT'
  headers: Record<string, string>
}

export class InvalidPresignKeyError extends Error {
  readonly name = 'InvalidPresignKeyError'

  constructor(key: string) {
    super(`Presign key must be one exact object key, not a prefix or wildcard: ${key}`)
  }
}

function assertExactKey(key: string): void {
  if (!key || key.includes('*') || key.includes('?') || key.endsWith('/')) {
    throw new InvalidPresignKeyError(key)
  }
}

function joinEndpoint(endpoint: string, key: string): string {
  const base = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint
  const encodedKey = key
    .split('/')
    .map((segment) => encodeURIComponent(segment))
    .join('/')
  return `${base}/${encodedKey}`
}

/**
 * Mint a browser-direct PUT URL for one exact R2 object key.
 *
 * Authz floor: single exact key (never prefix/wildcard), PUT-only, required bounded
 * `expiresIn`, Content-Type bound in `X-Amz-SignedHeaders`.
 *
 * **Direct-PUT bypass contract (two control points):**
 * - **Pre-issue:** host MUST call `checkStorageQuota` before minting — bytes hit R2 on PUT.
 * - **Post-upload:** host MUST HEAD+range-GET first bytes → `detectMimeFromMagicBytes` + size,
 *   or use proxy/direct-POST where the worker sees bytes. Content-Type binding pins the declared
 *   type only — NOT spoof-protection.
 */
export async function presignR2Put(
  creds: PresignR2Creds,
  key: string,
  contentType: string,
  expiresIn: number,
): Promise<PresignedR2Put> {
  assertExactKey(key)
  if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
    throw new RangeError('expiresIn must be a positive finite number of seconds')
  }

  const client = new AwsClient({
    accessKeyId: creds.accessKeyId,
    secretAccessKey: creds.secretAccessKey,
    region: creds.region ?? 'auto',
    service: 's3',
  })

  const objectUrl = `${joinEndpoint(creds.endpoint, key)}?X-Amz-Expires=${expiresIn}`
  const signed = await client.sign(
    new Request(objectUrl, {
      method: 'PUT',
      headers: {
        'Content-Type': contentType,
      },
    }),
    { aws: { signQuery: true, allHeaders: true } },
  )

  const headers: Record<string, string> = {}
  signed.headers.forEach((value, name) => {
    headers[name.toLowerCase()] = value
  })

  return {
    url: signed.url,
    method: 'PUT',
    headers,
  }
}
