// 薄薄一层 fetch 封装。错误统一抛 Error(message)。

async function jsonOrThrow(res) {
  if (!res.ok) {
    const text = await res.text().catch(() => '')
    throw new Error(text || `${res.status} ${res.statusText}`)
  }
  return res.json()
}

export function listPieces({ tag, playlist } = {}) {
  const qs = new URLSearchParams()
  if (tag) qs.set('tag', tag)
  if (playlist) qs.set('playlist', String(playlist))
  const q = qs.toString()
  return fetch('/api/pieces' + (q ? '?' + q : '')).then(jsonOrThrow)
}

export function getPiece(id) {
  return fetch(`/api/pieces/${id}`).then(jsonOrThrow)
}

export function createPiece(body) {
  return fetch('/api/pieces', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  }).then(jsonOrThrow)
}

export function patchPiece(id, body) {
  return fetch(`/api/pieces/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  }).then(jsonOrThrow)
}

export function deletePiece(id) {
  return fetch(`/api/pieces/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
}

export function recordPlay(id) {
  return fetch(`/api/pieces/${id}/play`, { method: 'POST' }).then(jsonOrThrow)
}

// `role`: null | 'chord' | 'numbered' | 'staff'
export function uploadAttachments(pieceId, files, role) {
  const fd = new FormData()
  for (const f of files) fd.append('files', f, f.name)
  const url = role
    ? `/api/pieces/${pieceId}/attachments?role=${encodeURIComponent(role)}`
    : `/api/pieces/${pieceId}/attachments`
  return fetch(url, { method: 'POST', body: fd }).then(jsonOrThrow)
}

export function deleteAttachment(id) {
  return fetch(`/api/attachments/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
}

export function attachmentUrl(id) {
  return `/api/attachments/${id}`
}

export function chordFetch(pieceId, mode = 'functional') {
  return fetch(`/api/pieces/${pieceId}/chord/fetch?mode=${encodeURIComponent(mode)}`, { method: 'POST' }).then(jsonOrThrow)
}

export function chordStatus(pieceId, mode = 'functional') {
  return fetch(`/api/pieces/${pieceId}/chord/status?mode=${encodeURIComponent(mode)}`).then(jsonOrThrow)
}

// ---- chat ----

export function listChat(pieceId) {
  return fetch(`/api/pieces/${pieceId}/chat`).then(jsonOrThrow)
}

export function clearChat(pieceId) {
  return fetch(`/api/pieces/${pieceId}/chat`, { method: 'DELETE' }).then(jsonOrThrow)
}

/**
 * stream a chat reply.
 * @param {number} pieceId
 * @param {string} message
 * @param {(delta: string) => void} onDelta
 * @param {AbortSignal} signal
 * @returns {Promise<{ok: boolean, error?: string}>}
 */
export async function streamChat(pieceId, message, onDelta, signal) {
  const resp = await fetch(`/api/pieces/${pieceId}/chat`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message }),
    signal,
  })
  if (!resp.ok) {
    const text = await resp.text().catch(() => '')
    return { ok: false, error: text || `${resp.status}` }
  }
  const reader = resp.body.getReader()
  const dec = new TextDecoder()
  let buf = ''
  let lastEvent = 'message'
  let errorMsg = ''
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    buf += dec.decode(value, { stream: true })
    let idx
    while ((idx = buf.indexOf('\n')) >= 0) {
      const line = buf.slice(0, idx)
      buf = buf.slice(idx + 1)
      // SSE 协议：空行 = event 边界（已在内容中处理）；event:/data: 各自一行
      if (line.startsWith('event:')) {
        lastEvent = line.slice(6).trim()
      } else if (line.startsWith('data:')) {
        const data = line.slice(5).replace(/^ /, '')
        if (lastEvent === 'error') {
          errorMsg = data
        } else if (lastEvent === 'done') {
          // 不带 data 也算结束
        } else {
          onDelta(data)
        }
      } else if (line === '') {
        lastEvent = 'message'
      }
    }
  }
  if (errorMsg) return { ok: false, error: errorMsg }
  return { ok: true }
}

// ---- inspire ----

export async function streamInspire(hint, onDelta, signal) {
  const resp = await fetch('/api/inspire', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ hint: hint || null }),
    signal,
  })
  if (!resp.ok) {
    const text = await resp.text().catch(() => '')
    return { ok: false, error: text || `${resp.status}` }
  }
  const reader = resp.body.getReader()
  const dec = new TextDecoder()
  let buf = ''
  let lastEvent = 'message'
  let errorMsg = ''
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    buf += dec.decode(value, { stream: true })
    let idx
    while ((idx = buf.indexOf('\n')) >= 0) {
      const line = buf.slice(0, idx)
      buf = buf.slice(idx + 1)
      if (line.startsWith('event:')) {
        lastEvent = line.slice(6).trim()
      } else if (line.startsWith('data:')) {
        const data = line.slice(5).replace(/^ /, '')
        if (lastEvent === 'error') errorMsg = data
        else if (lastEvent !== 'done') onDelta(data)
      } else if (line === '') {
        lastEvent = 'message'
      }
    }
  }
  if (errorMsg) return { ok: false, error: errorMsg }
  return { ok: true }
}

// ---- tags ----

export function listTags() {
  return fetch('/api/tags').then(jsonOrThrow)
}

export function createTag(name) {
  return fetch('/api/tags', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name }),
  }).then(jsonOrThrow)
}

export function deleteTag(id) {
  return fetch(`/api/tags/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
}

// ---- playlists ----

export function listPlaylists() {
  return fetch('/api/playlists').then(jsonOrThrow)
}

export function createPlaylist(name, description) {
  return fetch('/api/playlists', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, description: description || null }),
  }).then(jsonOrThrow)
}

export function getPlaylist(id) {
  return fetch(`/api/playlists/${id}`).then(jsonOrThrow)
}

export function patchPlaylist(id, body) {
  return fetch(`/api/playlists/${id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  }).then(jsonOrThrow)
}

export function deletePlaylist(id) {
  return fetch(`/api/playlists/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
}

export function playlistAddPiece(playlistId, pieceId) {
  return fetch(`/api/playlists/${playlistId}/pieces`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ piece_id: pieceId }),
  }).then(jsonOrThrow)
}

export function playlistRemovePiece(playlistId, pieceId) {
  return fetch(`/api/playlists/${playlistId}/pieces/${pieceId}`, { method: 'DELETE' }).then(jsonOrThrow)
}
