// Thin fetch wrapper around the FastAPI backend. All endpoints are same-origin
// (the SPA is served by FastAPI in production; proxied in dev via vite.config).

export class ApiError extends Error {
  status: number;
  constructor(message: string, status: number) {
    super(message);
    this.status = status;
  }
}

async function parseError(res: Response): Promise<string> {
  try {
    const data = await res.json();
    if (typeof data?.detail === "string") return data.detail;
    if (Array.isArray(data?.detail)) return data.detail.map((d: any) => d.msg).join(", ");
    return res.statusText;
  } catch {
    return res.statusText || `Request failed (${res.status})`;
  }
}

export async function apiGet<T = any>(path: string): Promise<T> {
  const res = await fetch(path);
  if (!res.ok) throw new ApiError(await parseError(res), res.status);
  return res.json();
}

export async function apiPost<T = any>(path: string, body?: unknown): Promise<T> {
  const res = await fetch(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) throw new ApiError(await parseError(res), res.status);
  return res.json();
}

// Returns the rendered image as an object URL plus the X-Cache-Key header.
export async function apiPostImage(
  path: string,
  body: unknown
): Promise<{ url: string; cacheKey: string | null }> {
  const res = await fetch(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new ApiError(await parseError(res), res.status);
  const blob = await res.blob();
  return { url: URL.createObjectURL(blob), cacheKey: res.headers.get("X-Cache-Key") };
}

// Reads a newline-delimited JSON stream, invoking onMessage per parsed object.
// Used for the disk-efficient music scan and the live maintenance run.
export async function streamNDJSON(
  path: string,
  opts: { method?: string; body?: unknown; signal?: AbortSignal; onMessage: (obj: any) => void }
): Promise<void> {
  const res = await fetch(path, {
    method: opts.method || "GET",
    headers: opts.body !== undefined ? { "Content-Type": "application/json" } : undefined,
    body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
    signal: opts.signal,
  });
  if (!res.ok || !res.body) throw new ApiError(await parseError(res), res.status);
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buf = "";
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buf += decoder.decode(value, { stream: true });
    let nl: number;
    while ((nl = buf.indexOf("\n")) >= 0) {
      const line = buf.slice(0, nl).trim();
      buf = buf.slice(nl + 1);
      if (line) {
        try {
          opts.onMessage(JSON.parse(line));
        } catch {
          /* ignore partial/invalid line */
        }
      }
    }
  }
  const tail = buf.trim();
  if (tail) {
    try {
      opts.onMessage(JSON.parse(tail));
    } catch {
      /* ignore */
    }
  }
}

export async function uploadBackground(file: File): Promise<{ upload_id: string; width: number; height: number }> {
  const form = new FormData();
  form.append("file", file);
  const res = await fetch("/api/upload-background", { method: "POST", body: form });
  if (!res.ok) throw new ApiError(await parseError(res), res.status);
  return res.json();
}

export async function uploadHomescreenDb(
  file: File
): Promise<{ upload: { upload_id: string; filename: string; size_bytes: number; uploaded_at: string; sha256: string; path: string } }> {
  const form = new FormData();
  form.append("file", file);
  const res = await fetch("/api/homescreen/db-upload", { method: "POST", body: form });
  if (!res.ok) throw new ApiError(await parseError(res), res.status);
  return res.json();
}

// ── Shared types ────────────────────────────────────────────────────────────
export interface AppConfig {
  app_name: string;
  emby: { url: string; connected: boolean };
  navidrome: { url: string; configured: boolean };
  audiobookshelf: { url: string; configured: boolean };
  music: { root: string; available: boolean };
}

export interface SearchItem {
  id: string;
  name: string;
  year: number | string;
  type: string;
  has_logo: boolean;
  auto_studio: string | null;
  backdrop_count: number;
  poster_url: string;
}
