const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000';

export async function apiFetch<T = unknown>(path: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(`${API_BASE}${path}`, {
    credentials: 'include',
    cache: 'no-store',
    ...init,
    headers: {
      'Content-Type': 'application/json',
      ...(init.headers ?? {}),
    },
  });
  if (!res.ok) {
    let detail: unknown = undefined;
    try {
      detail = await res.json();
    } catch {}
    const err = new Error(`api_error_${res.status}`);
    (err as unknown as { detail?: unknown }).detail = detail;
    (err as unknown as { status?: number }).status = res.status;
    throw err;
  }
  return (await res.json()) as T;
}

/** Absolute API URL for a path — use for full-page navigations (OAuth redirects). */
export function apiUrl(path: string): string {
  return `${API_BASE}${path}`;
}

export function apiWebSocketURL(path: string): string {
  const httpBase = API_BASE;
  const wsBase = httpBase.replace(/^http/, 'ws');
  return `${wsBase}${path}`;
}

export interface SseHandlers {
  onEvent: (event: string, data: unknown) => void;
  onError?: (err: Error) => void;
}

/**
 * POST with SSE response. Used by the streaming preview where Cloudflare's
 * sync edge timeout (~100s) is fatal — every server-side chunk resets it,
 * so as long as the model emits text we never hit the cap.
 *
 * Native EventSource doesn't support POST or auth-cookie credentials, hence
 * the manual fetch + ReadableStream consumer. Caller controls aborting via
 * the supplied AbortSignal (we attach it to the underlying fetch).
 */
export async function apiSseStream(
  path: string,
  body: unknown,
  handlers: SseHandlers,
  signal?: AbortSignal,
): Promise<void> {
  let res: Response;
  try {
    res = await fetch(`${API_BASE}${path}`, {
      method: 'POST',
      credentials: 'include',
      cache: 'no-store',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'text/event-stream',
      },
      body: JSON.stringify(body),
      signal,
    });
  } catch (err) {
    handlers.onError?.(err as Error);
    return;
  }

  if (!res.ok) {
    let detail: unknown = undefined;
    try {
      detail = await res.json();
    } catch {}
    const err = new Error(`api_error_${res.status}`);
    (err as unknown as { detail?: unknown }).detail = detail;
    (err as unknown as { status?: number }).status = res.status;
    handlers.onError?.(err);
    return;
  }

  const reader = res.body?.getReader();
  if (!reader) {
    handlers.onError?.(new Error('no_response_body'));
    return;
  }

  const decoder = new TextDecoder();
  let buffer = '';
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      // SSE events are separated by a blank line. Parse one event at a time.
      let idx = buffer.indexOf('\n\n');
      while (idx >= 0) {
        const raw = buffer.slice(0, idx);
        buffer = buffer.slice(idx + 2);
        let event = 'message';
        const dataLines: string[] = [];
        for (const line of raw.split('\n')) {
          if (line.startsWith(':')) continue; // SSE comment
          if (line.startsWith('event:')) event = line.slice(6).trim();
          else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
        }
        if (dataLines.length > 0) {
          const data = dataLines.join('\n');
          let parsed: unknown = data;
          try {
            parsed = JSON.parse(data);
          } catch {
            // Treat as a raw string event.
          }
          handlers.onEvent(event, parsed);
        }
        idx = buffer.indexOf('\n\n');
      }
    }
  } catch (err) {
    handlers.onError?.(err as Error);
  }
}
