export async function api(path, opts = {}) {
  const { body, ...rest } = opts
  const res = await fetch(path, {
    ...rest,
    headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
    body: body !== undefined ? JSON.stringify(body) : undefined,
  })
  if (!res.ok) {
    let message = res.statusText
    try {
      message = (await res.json()).error || message
    } catch {
      /* not json */
    }
    const err = new Error(message)
    err.status = res.status
    throw err
  }
  if (res.status === 204) return null
  return res.json()
}

// Image URL with a cache-buster tied to the last processing run, so
// reprocessed photos bypass the long-lived immutable browser cache.
// Auth rides on cookies (session or share-access), never in the URL.
export function imgUrl(photo, size) {
  const version = photo.processed_at ? `?v=${encodeURIComponent(photo.processed_at)}` : ''
  return `/api/img/${photo.id}/${size}${version}`
}

// Trigger a browser-native download from a POST endpoint (e.g. zip streams)
// via a hidden form — fetch+blob would buffer the whole file in memory.
// Targets a hidden iframe so an error response can't navigate away from the
// app (which would lose selection/rating state); errors surface as an alert.
export function postDownload(url, ids = '') {
  let frame = document.getElementById('download-frame')
  if (!frame) {
    frame = document.createElement('iframe')
    frame.id = 'download-frame'
    frame.name = 'download-frame'
    frame.style.display = 'none'
    document.body.appendChild(frame)
  }
  frame.onload = () => {
    // load only fires when the response rendered (i.e. an error body);
    // successful attachment downloads never trigger it.
    let message = 'download failed'
    try {
      const text = frame.contentDocument?.body?.textContent
      if (!text) return
      try {
        message = JSON.parse(text).error || message
      } catch {
        /* not json */
      }
    } catch {
      return
    }
    alert(`Download failed: ${message}`)
  }
  const form = document.createElement('form')
  form.method = 'POST'
  form.action = url
  form.target = 'download-frame'
  form.style.display = 'none'
  const input = document.createElement('input')
  input.type = 'hidden'
  input.name = 'ids'
  input.value = ids
  form.appendChild(input)
  document.body.appendChild(form)
  form.submit()
  form.remove()
}

// SHA-256 of a File, matching the server's content hash — used to skip
// uploading bytes the album already has.
export async function sha256Hex(file) {
  const digest = await crypto.subtle.digest('SHA-256', await file.arrayBuffer())
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')
}

export function uploadFile(url, file, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open('POST', url)
    xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream')
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable && onProgress) onProgress(e.loaded / e.total)
    }
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(JSON.parse(xhr.responseText))
      } else {
        let message = `upload failed (${xhr.status})`
        try {
          message = JSON.parse(xhr.responseText).error || message
        } catch {
          /* not json */
        }
        reject(new Error(message))
      }
    }
    xhr.onerror = () => reject(new Error('network error during upload'))
    xhr.send(file)
  })
}
