import type { ClientPuzzle, GenerateResponse } from './types'

const BASE = '/api'

export interface ThemeInfo {
  id: string
  name: string
  word_count: number
}

export interface MeInfo {
  display_name: string | null
  is_new: boolean
  has_passkey: boolean
  preferred_preset: string | null
  streak: { current: number; best: number; last_date: string | null }
  streak_freezes: number
}

export interface ValidateResult {
  correct: boolean
  cells: string[][]
  rank: number | null
  percentile: number | null
  total_solvers: number | null
  already_solved: boolean
  solve_time: number
  solve_hints: number
  cell_results: string | null
  new_achievements: string[]
  flagged: boolean
  receipt: ReceiptPayload | null
}

export interface HintLetter {
  row: number
  col: number
  letter: string
}

export interface HintResult {
  is_correct: boolean
  row: number
  col: number
  letter: string
  letters: HintLetter[]
}

export interface LeaderboardEntry {
  name: string | null
  time: number
  hints: number
  date: string | null
  count: number
  current: number
  best: number
  instance: string
}

export interface LeaderboardResult {
  entries: LeaderboardEntry[]
}

export type ServerSettings = Record<string, string | number | boolean | null | string[]>

export interface HistoryEntry {
  date: string
  preset: string | null
  time: number
  hints: number
  short_id: string | null
}

export interface RatingResult {
  average: number
  count: number
  my_rating: number | null
}

export type DailyRatings = Record<string, { average: number; count: number }>

export interface CommunityClueItem {
  id: number
  clue: string
  upvotes: number
  downvotes: number
  status: string
  my_vote: number
  origin_domain: string | null
  created_at: string
}

export interface ClueSubmissionItem {
  id: number
  word: string
  clue: string
  upvotes: number
  downvotes: number
  status: string
}

export interface EditorLockedCell {
  row: number
  col: number
  letter: string
}

export interface EditorGeneratedClue {
  number: number
  direction: string
  text: string
  answer: string
}

export interface EditorAutofillResult {
  ok: boolean
  error: string
  letters: (string | null)[][]
  clues: EditorGeneratedClue[]
}

export interface EditorLoadedClue {
  number: number
  direction: string
  text: string
  answer: string
  startRow?: number
  startCol?: number
}

export interface EditorLoadedPuzzle {
  id: string
  title: string
  width: number
  height: number
  grid: string[][]
  clues: EditorLoadedClue[]
  puzzle: {
    grid?: { cells?: { Letter?: string }[][] | null }
  } | null
  status: string
}

export interface SavedPuzzleItem {
  id: string
  title: string
  width: number
  height: number
  status: string
  updated_at: string
}

export interface CommunityPuzzleItem {
  id: string
  title: string
  author: string | null
  origin: string | null
  low_reputation: boolean
  origin_reputation: number | null
  federated?: boolean
  width: number
  height: number
  updated_at: string
}

export interface CommunityPlayResult {
  id: string
  short_id: string
  title: string
  community: boolean
  puzzle: ClientPuzzle
}

export interface ConstructorAnalytics {
  total_solvers: number
  total_attempts: number
  avg_time: number
  completion_rate: number
  total_hints: number
  width: number
  height: number
  hint_heatmap?: number[][]
  max_hints?: number
}

export interface ArchiveEntry {
  date: string
  preset: string
  short_id: string
  width: number
  height: number
  solved: boolean
}

export interface PasskeyItem {
  id: string
  name: string
  created_at: string
}

export interface PackClue {
  text: string
  difficulty?: number
}

export interface PackWord {
  word: string
  clues: PackClue[]
}

export interface PackItem {
  id: string
  name: string
  description: string | null
  author: string | null
  tags: string[]
  word_count: number
  downloads: number
  rating: number | null
  rating_count: number
  created_at: string
  words?: PackWord[]
}

export interface PackActionResult {
  id: string
  word_count: number
}

export interface ImportResult {
  imported: number
  skipped: string[]
}

export interface MigrateResult {
  ok: boolean
  target: string
  export: unknown
}

export interface ReportResult {
  id: number
}

export interface RoomPlayer {
  id: string
  name: string
  color: string
  cursor: [number, number] | null
  direction: string
  order: number
}

export interface RoomInfo {
  code: string
  mode: string
  puzzle_id: string | null
  players: RoomPlayer[]
  player_count: number
  max_players: number
  started: boolean
  current_turn: string | null
  party: {
    phase: string
    round: number
    total_rounds: number
    sub_mode: string
  } | null
}

export interface ReceiptBody {
  receipt_id: string
  puzzle_hash: string
  solve_time_secs: number
  hints_used: number
  solve_date: string
  instance_url: string
  player_name: string
  room_code: string | null
  mode: string | null
  players: string[]
  created_at: string
  expires_at: string
}

export interface ReceiptPayload {
  receipt: ReceiptBody
  signature: string
  key_id: string
  receipt_url?: string
  receipt_id?: string
}

export interface ClaimReceiptResult {
  claimed: boolean
  solve_time: number
  hints: number
}

export interface TrainerClueResult {
  clue: string
  length: number
  pattern: string
  difficulty: string
}

export interface AnalyticsSummary {
  total_solves: number
  clean_solves: number
  total_time_secs: number
  avg_time_secs: number
  avg_hints: number
  current_streak: number
  best_streak: number
  first_solve: string | null
  puzzles_rated: number
  clues_submitted: number
}

export interface SolveEntry {
  date: string
  preset: string
  time: number
  hints: number
}

export interface TrendEntry {
  date: string
  avg_time?: number
  avg_hints?: number
}

export interface PresetStats {
  count: number
  best: number
  avg: number
  clean: number
}

export interface CleanRateEntry {
  date: string
  rate: number
}

export interface MonthlyEntry {
  month: string
  count: number
}

export interface DiffEntry {
  date: string
  avg_difficulty: number
}

export interface FastestRecord {
  time: number
  date: string
}

export interface Analytics {
  summary: AnalyticsSummary
  by_preset: Record<string, PresetStats>
  solve_history: SolveEntry[]
  activity_calendar: Record<string, number>
  by_hour: number[]
  by_day_of_week: number[]
  time_trend: TrendEntry[]
  hints_trend: TrendEntry[]
  improvement_rate: number | null
  clean_rate_trend: CleanRateEntry[]
  fastest_by_preset: Record<string, FastestRecord>
  solve_time_distribution: number[]
  monthly_totals: MonthlyEntry[]
  completion_rate: number
  words_encountered: number
  avg_time_by_hour: Record<string, number>
  difficulty_progression: DiffEntry[]
}

export interface WordOfTheDayResult {
  word: string
  definition: string
  clue: string
  length: number
}

export type ChallengeCompletionMap = Record<string, boolean>

export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
export type ImportPayload = Record<string, JsonValue>

async function getJson<T>(res: Response): Promise<T> {
  // SAFETY: every caller passes its endpoint's documented JSON shape and decodes at the call site
  return res.json() as T
}

interface PasskeyParam {
  type: 'public-key'
  alg: number
}

interface RegistrationStartOptions {
  challenge: string
  rp: { id: string; name: string }
  user: { id: string; name: string; displayName: string }
  pubKeyCredParams: PasskeyParam[]
  timeout: number
  authenticatorSelection: AuthenticatorSelectionCriteria
  attestation: AttestationConveyancePreference
}

interface AuthenticationStartOptions {
  challenge: string
  rp_id: string
  timeout: number
  allowCredentials: { type: 'public-key'; id: string }[]
}

interface ValidateBody {
  cells: (string | null)[][]
  elapsed: number
  hints: number
  local_date: string
  room_code?: string
  cell_results?: string
}

interface CreateRoomBody {
  puzzle_id: string
  mode: string
  party_sub?: string
}

export async function fetchThemes(): Promise<ThemeInfo[]> {
  const res = await fetch(`${BASE}/themes`)
  if (!res.ok) return []
  const data = await getJson<{ themes?: ThemeInfo[] }>(res)
  return data.themes || []
}

export async function generatePuzzle(
  difficulty?: string,
  size?: number,
  density?: number,
  clue_difficulty?: number,
  theme?: string,
  cryptic?: boolean,
  pack?: string,
): Promise<GenerateResponse> {
  const res = await fetch(`${BASE}/puzzles/generate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ difficulty, size, density, clue_difficulty, theme, cryptic, pack }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchDaily(date: string, preset: string): Promise<GenerateResponse> {
  const res = await fetch(`${BASE}/puzzles/daily/${date}/${preset}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchPuzzleByShortId(shortId: string): Promise<GenerateResponse> {
  const res = await fetch(`${BASE}/puzzles/s/${shortId}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchPuzzleById(id: string): Promise<GenerateResponse> {
  const res = await fetch(`${BASE}/puzzles/${id}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function validateSolution(
  id: string,
  cells: (string | null)[][],
  elapsed?: number,
  hints?: number,
  roomCode?: string | null,
  cellResults?: string,
): Promise<ValidateResult> {
  const localDate = new Date().toISOString().split('T')[0]
  const body: ValidateBody = { cells, elapsed: elapsed ?? 0, hints: hints ?? 0, local_date: localDate }
  if (roomCode) body.room_code = roomCode
  if (cellResults) body.cell_results = cellResults
  const res = await fetch(`${BASE}/puzzles/${id}/validate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function requestHint(
  id: string,
  kind: string,
  row: number,
  col: number,
  direction?: string,
  letter?: string,
): Promise<HintResult> {
  const res = await fetch(`${BASE}/puzzles/${id}/hint`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ kind, row, col, direction, letter }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchMe(): Promise<MeInfo> {
  const res = await fetch(`${BASE}/me`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function updateMe(data: { display_name?: string }): Promise<void> {
  await fetch(`${BASE}/me`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
}

export async function saveProgress(id: string, cells: (string | null)[][]): Promise<void> {
  await fetch(`${BASE}/puzzles/${id}/progress`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ cells }),
  })
}

export async function fetchLeaderboard(puzzleId: string): Promise<LeaderboardResult> {
  const res = await fetch(`${BASE}/leaderboards/${puzzleId}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchDailyLeaderboard(date: string, preset: string): Promise<LeaderboardResult> {
  const res = await fetch(`${BASE}/leaderboards/daily/${date}/${preset}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchFederatedLeaderboard(date: string, preset: string): Promise<LeaderboardResult> {
  const res = await fetch(`${BASE}/leaderboards/federated/${date}/${preset}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchAlltimeLeaderboard(preset: string): Promise<LeaderboardResult> {
  const res = await fetch(`${BASE}/leaderboards/alltime/${preset}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchWeeklyLeaderboard(preset: string): Promise<LeaderboardResult> {
  const res = await fetch(`${BASE}/leaderboards/weekly/${preset}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchStreakLeaderboard(): Promise<LeaderboardResult> {
  const res = await fetch(`${BASE}/leaderboards/streaks`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchCompletionistLeaderboard(): Promise<LeaderboardResult> {
  const res = await fetch(`${BASE}/leaderboards/completionist`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchCleanLeaderboard(date: string, preset: string): Promise<LeaderboardResult> {
  const res = await fetch(`${BASE}/leaderboards/clean/${date}/${preset}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchAnalytics(): Promise<Analytics> {
  const res = await fetch(`${BASE}/me/analytics`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchSettings(): Promise<ServerSettings> {
  const res = await fetch(`${BASE}/me/settings`)
  if (!res.ok) return {}
  return res.json()
}

export async function saveSettings(settings: ServerSettings): Promise<void> {
  await fetch(`${BASE}/me/settings`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(settings),
  })
}

// passkey / WebAuthn helpers

function bufToBase64url(buf: ArrayBuffer): string {
  return btoa(String.fromCharCode(...new Uint8Array(buf)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

function base64urlToBuf(s: string): ArrayBuffer {
  const padded = s.replace(/-/g, '+').replace(/_/g, '/') + '=='.slice(0, (4 - s.length % 4) % 4)
  const binary = atob(padded)
  const buf = new Uint8Array(binary.length)
  for (let i = 0; i < binary.length; i++) buf[i] = binary.charCodeAt(i)
  return buf.buffer
}

export async function registerPasskey(): Promise<boolean> {
  const res = await fetch(`${BASE}/passkey/register/start`, { method: 'POST' })
  if (!res.ok) throw new Error(await res.text())
  const { options } = await getJson<{ options: RegistrationStartOptions }>(res)

  const publicKey: PublicKeyCredentialCreationOptions = {
    challenge: base64urlToBuf(options.challenge),
    rp: options.rp,
    user: {
      id: base64urlToBuf(options.user.id),
      name: options.user.name,
      displayName: options.user.displayName,
    },
    pubKeyCredParams: options.pubKeyCredParams,
    timeout: options.timeout,
    authenticatorSelection: options.authenticatorSelection,
    attestation: options.attestation,
  }

  const credential = await navigator.credentials.create({ publicKey })
  if (!(credential instanceof PublicKeyCredential)) return false

  const attestation = credential.response
  if (!(attestation instanceof AuthenticatorAttestationResponse)) return false
  const finish = await fetch(`${BASE}/passkey/register/finish`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: credential.id,
      clientDataJSON: bufToBase64url(attestation.clientDataJSON),
      attestationObject: bufToBase64url(attestation.attestationObject),
    }),
  })
  if (!finish.ok) throw new Error(await finish.text())
  return true
}

export async function authenticatePasskey(): Promise<{ ok: boolean; display_name: string | null } | null> {
  const res = await fetch(`${BASE}/passkey/authenticate/start`, { method: 'POST' })
  if (!res.ok) throw new Error(await res.text())
  const { options } = await getJson<{ options: AuthenticationStartOptions }>(res)

  const publicKey: PublicKeyCredentialRequestOptions = {
    challenge: base64urlToBuf(options.challenge),
    rpId: options.rp_id,
    timeout: options.timeout,
    allowCredentials: options.allowCredentials.map(c => ({
      type: c.type,
      id: base64urlToBuf(c.id),
    })),
  }

  const assertion = await navigator.credentials.get({ publicKey })
  if (!(assertion instanceof PublicKeyCredential)) return null

  const resp = assertion.response
  if (!(resp instanceof AuthenticatorAssertionResponse)) return null
  const finish = await fetch(`${BASE}/passkey/authenticate/finish`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: assertion.id,
      clientDataJSON: bufToBase64url(resp.clientDataJSON),
      authenticatorData: bufToBase64url(resp.authenticatorData),
      signature: bufToBase64url(resp.signature),
    }),
  })
  if (!finish.ok) throw new Error(await finish.text())
  return finish.json()
}

// history

export async function fetchHistory(): Promise<HistoryEntry[]> {
  const res = await fetch(`${BASE}/me/history`)
  if (!res.ok) return []
  const data = await getJson<{ history?: HistoryEntry[] }>(res)
  return data.history || []
}

// puzzle ratings

export async function ratePuzzle(id: string, rating: number): Promise<RatingResult> {
  const res = await fetch(`${BASE}/puzzles/${id}/rate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ rating }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchRating(id: string): Promise<RatingResult> {
  const res = await fetch(`${BASE}/puzzles/${id}/rating`)
  if (!res.ok) return { average: 0, count: 0, my_rating: null }
  return res.json()
}

export async function fetchDailyRatings(date: string): Promise<DailyRatings> {
  const res = await fetch(`${BASE}/puzzles/daily/${date}/ratings`)
  if (!res.ok) return {}
  return res.json()
}

// community clues

export async function submitClue(word: string, clue: string): Promise<{ id: number }> {
  const res = await fetch(`${BASE}/clues/community`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ word, clue }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchCommunityClues(word: string): Promise<{ clues: CommunityClueItem[] }> {
  const res = await fetch(`${BASE}/clues/community/${word.toUpperCase()}`)
  if (!res.ok) return { clues: [] }
  return res.json()
}

export async function voteClue(id: number, vote: number): Promise<void> {
  await fetch(`${BASE}/clues/community/vote/${id}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ vote }),
  })
}

export async function deleteSubmission(id: number): Promise<void> {
  await fetch(`${BASE}/me/submissions/${id}`, { method: 'DELETE' })
}

export async function fetchMySubmissions(): Promise<ClueSubmissionItem[]> {
  const res = await fetch(`${BASE}/me/submissions`)
  if (!res.ok) return []
  const data = await getJson<{ submissions?: ClueSubmissionItem[] }>(res)
  return data.submissions || []
}

// puzzle import (.puz / .ipuz)

export async function importPuzzle(file: File): Promise<GenerateResponse> {
  const buf = await file.arrayBuffer()
  const res = await fetch(`${BASE}/puzzles/import`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/octet-stream' },
    body: buf,
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

// editor

export async function editorAutofill(width: number, height: number, cells: string[][], locked?: EditorLockedCell[]): Promise<EditorAutofillResult> {
  const res = await fetch(`${BASE}/editor/autofill`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ width, height, cells, locked }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function editorClueSuggestions(word: string): Promise<string[]> {
  const res = await fetch(`${BASE}/editor/clue-suggestions/${word.toUpperCase()}`)
  if (!res.ok) return []
  const data = await getJson<{ suggestions?: string[] }>(res)
  return data.suggestions || []
}

export async function editorCheckWords(words: string[]): Promise<{ blocked: string[] }> {
  const res = await fetch(`${BASE}/editor/check-words`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ words }),
  })
  if (!res.ok) return { blocked: [] }
  return res.json()
}

export async function editorSave(data: {
  id?: string
  title: string
  width: number
  height: number
  grid: string[][]
  clues?: EditorLoadedClue[]
  puzzle?: { grid: { cells: ({ Letter: string } | null)[][] } }
  status?: string
}): Promise<{ id: string }> {
  const res = await fetch(`${BASE}/editor/puzzles`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function editorLoad(id: string): Promise<EditorLoadedPuzzle> {
  const res = await fetch(`${BASE}/editor/puzzles/${id}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function editorList(): Promise<SavedPuzzleItem[]> {
  const res = await fetch(`${BASE}/editor/puzzles`)
  if (!res.ok) return []
  const data = await getJson<{ puzzles?: SavedPuzzleItem[] }>(res)
  return data.puzzles || []
}

export async function editorDelete(id: string): Promise<void> {
  await fetch(`${BASE}/editor/puzzles/${id}`, { method: 'DELETE' })
}

export async function fetchCommunityPuzzles(): Promise<CommunityPuzzleItem[]> {
  const res = await fetch(`${BASE}/community-puzzles`)
  if (!res.ok) return []
  const data = await getJson<{ puzzles?: CommunityPuzzleItem[] }>(res)
  return data.puzzles || []
}

export async function fetchFederatedPuzzles(): Promise<{ puzzles: CommunityPuzzleItem[] }> {
  const res = await fetch(`${BASE}/federated-puzzles`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function loadCommunityPuzzle(id: string): Promise<CommunityPlayResult> {
  const res = await fetch(`${BASE}/community-puzzles/${id}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function editorExport(id: string): Promise<void> {
  const res = await fetch(`${BASE}/editor/puzzles/${id}/export`)
  if (!res.ok) throw new Error(await res.text())
  const blob = await res.blob()
  const cd = res.headers.get('content-disposition') || ''
  const match = cd.match(/filename="(.+)"/)
  const filename = match ? match[1] : 'puzzle.ipuz'
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url; a.download = filename; a.click()
  URL.revokeObjectURL(url)
}

export async function editorExportAll(): Promise<void> {
  const res = await fetch(`${BASE}/editor/export-all`)
  if (!res.ok) throw new Error(await res.text())
  const blob = await res.blob()
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url; a.download = 'cruciverb-puzzles-export.json'; a.click()
  URL.revokeObjectURL(url)
}

export async function editorImport(data: ImportPayload): Promise<ImportResult> {
  const res = await fetch(`${BASE}/editor/import`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function editorPublish(id: string): Promise<{ ok: boolean }> {
  const res = await fetch(`${BASE}/editor/puzzles/${id}/publish`, { method: 'POST' })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchPuzzleAnalytics(id: string): Promise<ConstructorAnalytics> {
  const res = await fetch(`${BASE}/editor/puzzles/${id}/analytics`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchArchive(preset?: string, page?: number): Promise<{ entries: ArchiveEntry[] }> {
  const params = new URLSearchParams()
  if (preset && preset !== 'all') params.set('preset', preset)
  if (page) params.set('page', String(page))
  const qs = params.toString()
  const res = await fetch(`${BASE}/puzzles/archive${qs ? '?' + qs : ''}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function fetchPasskeys(): Promise<PasskeyItem[]> {
  const res = await fetch(`${BASE}/me/passkeys`)
  if (!res.ok) return []
  const data = await getJson<{ passkeys?: PasskeyItem[] }>(res)
  return data.passkeys || []
}

// privacy / account

export async function deleteAccount(): Promise<void> {
  await fetch(`${BASE}/me/account`, { method: 'DELETE' })
}

export async function exportData(): Promise<void> {
  const res = await fetch(`${BASE}/me/export`)
  if (!res.ok) throw new Error(await res.text())
  const blob = await res.blob()
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url; a.download = 'cruciverb-export.json'; a.click()
  URL.revokeObjectURL(url)
}

export async function importData(data: ImportPayload): Promise<ImportResult> {
  const res = await fetch(`${BASE}/me/import`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

// user migration

export async function migrateAccount(targetDomain: string): Promise<MigrateResult> {
  const res = await fetch(`${BASE}/me/migrate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ target_domain: targetDomain }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

// user blocklist import/export

export async function exportUserBlocklist(): Promise<void> {
  const res = await fetch(`${BASE}/me/blocklist`)
  if (!res.ok) throw new Error(await res.text())
  const blob = await res.blob()
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url; a.download = 'my-blocklist.json'; a.click()
  URL.revokeObjectURL(url)
}

export async function importUserBlocklist(data: ImportPayload): Promise<ImportResult> {
  const res = await fetch(`${BASE}/me/blocklist/import`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

// word of the day

export async function fetchWordOfTheDay(): Promise<WordOfTheDayResult> {
  const res = await fetch(`${BASE}/word-of-the-day`)
  if (!res.ok) return { word: '', definition: '', clue: '', length: 0 }
  return res.json()
}

// clue trainer

export async function fetchTrainerClue(difficulty: string): Promise<TrainerClueResult> {
  const res = await fetch(`${BASE}/trainer/clue?difficulty=${difficulty}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function checkTrainerAnswer(clue: string, answer: string): Promise<{ correct: boolean; answer: string }> {
  const res = await fetch(`${BASE}/trainer/check`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ clue, answer }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

// content reporting

export async function submitReport(contentType: string, contentId: string, reason?: string): Promise<ReportResult> {
  const res = await fetch(`${BASE}/report`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ content_type: contentType, content_id: contentId, reason }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

// multiplayer rooms

export async function createRoom(puzzleId: string, mode: string, partySub?: string): Promise<{ code: string }> {
  const body: CreateRoomBody = { puzzle_id: puzzleId, mode }
  if (partySub) body.party_sub = partySub
  const res = await fetch(`${BASE}/rooms`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function getRoomInfo(code: string): Promise<RoomInfo> {
  const res = await fetch(`${BASE}/rooms/${code}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export function connectRoom(code: string): WebSocket {
  const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
  return new WebSocket(`${proto}//${window.location.host}/api/rooms/${code}/ws`)
}

// wordlist packs

export async function fetchPacks(search?: string): Promise<{ packs: PackItem[] }> {
  const params = search ? `?search=${encodeURIComponent(search)}` : ''
  const res = await fetch(`${BASE}/packs${params}`)
  return res.json()
}

export async function createPack(pack: {
  name: string
  description?: string
  tags?: string[]
  words: PackWord[]
}): Promise<PackActionResult> {
  const res = await fetch(`${BASE}/packs`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(pack),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

// admin setup

export async function fetchAdminStatus(): Promise<{ configured: boolean }> {
  const res = await fetch(`${BASE}/admin/status`)
  if (!res.ok) return { configured: true }
  return res.json()
}

export async function setupAdmin(username: string, password: string): Promise<{ ok: boolean }> {
  const res = await fetch(`${BASE}/admin/setup`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

// daily challenges

export async function fetchDailyChallenge(date: string, preset: string): Promise<{ challenge: string; completed: boolean }> {
  const res = await fetch(`${BASE}/puzzles/daily/${date}/${preset}/challenge`)
  if (!res.ok) return { challenge: '', completed: false }
  return res.json()
}

export async function completeDailyChallenge(date: string, preset: string, challengeType: string): Promise<void> {
  await fetch(`${BASE}/puzzles/daily/${date}/${preset}/challenge/complete`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ challenge_type: challengeType }),
  })
}

export async function fetchTodayChallenges(): Promise<ChallengeCompletionMap> {
  const res = await fetch(`${BASE}/me/challenges/today`)
  if (!res.ok) return {}
  return res.json()
}

// receipts

export async function getReceipt(id: string): Promise<ReceiptPayload> {
  const res = await fetch(`${BASE}/receipts/${id}`)
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}

export async function claimReceipt(receipt: ReceiptPayload): Promise<ClaimReceiptResult> {
  const res = await fetch(`${BASE}/receipts/claim`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(receipt),
  })
  if (!res.ok) throw new Error(await res.text())
  return res.json()
}