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

// Friendly messages for known backend error codes. Anything not listed falls
// back to a status-based sentence — the user must never see a raw
// "api_error_409" or a bare code like "slug_taken".
const CODE_MESSAGES: Record<string, string> = {
  slug_taken: 'That slug is already taken by one of your servers — pick a different one.',
  unauthorized: 'Your session expired — please sign in again.',
  forbidden: 'You don’t have access to that.',
  not_found: 'We couldn’t find that.',
  org_not_found: 'We couldn’t find your workspace.',
  user_not_found: 'Account not found.',
  invalid_input: 'Some details weren’t valid — please check and try again.',
  invalid_id: 'That link looks invalid.',
  invalid_query: 'That request wasn’t valid.',
  rate_limited: 'You’ve hit a rate limit — wait a moment and try again.',
  plan_limit_reached: 'You’ve reached your plan’s limit — upgrade to add more.',
  subscription_suspended: 'Your subscription is paused — see Settings → Billing.',
  secret_in_prompt:
    'Your prompt looks like it contains an API key — remove it and add credentials in the next step.',
  stripe_not_configured: 'Payments aren’t configured yet.',
  no_active_subscription: 'No active subscription found.',
  checkout_failed: 'Checkout couldn’t start — please try again in a moment.',
  google_oauth_not_configured: 'Google sign-in isn’t available right now.',
  taken_down: 'This template was taken down and can’t be used.',
};

function statusFallback(status: number): string {
  if (status === 400) return 'That request wasn’t valid.';
  if (status === 401) return 'Your session expired — please sign in again.';
  if (status === 402) return 'A billing action is required to continue.';
  if (status === 403) return 'You don’t have access to that.';
  if (status === 404) return 'We couldn’t find that.';
  if (status === 409) return 'That conflicts with something that already exists.';
  if (status === 429) return 'Too many requests — please wait a moment.';
  if (status >= 500) return 'Something went wrong on our side — please try again.';
  return `Request failed (HTTP ${status}).`;
}

/**
 * Turn any thrown error (from apiFetch or a network failure) into a clear,
 * human-readable sentence. Prefers an explicit backend `detail`, then a mapped
 * error code, then a status-based fallback. Never returns a raw code.
 */
export function humanizeError(e: unknown): string {
  const err = e as { detail?: { detail?: string; error?: string }; status?: number; message?: string };
  const d = err?.detail;
  if (d && typeof d.detail === 'string' && d.detail.trim()) return d.detail;
  if (d?.error) {
    const mapped = CODE_MESSAGES[d.error];
    if (mapped) return mapped;
  }
  if (typeof err?.status === 'number') return statusFallback(err.status);
  if (err?.message && !/^api_error_/.test(err.message)) return err.message; // network / TypeError
  return 'Something went wrong — please try again.';
}

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('') as Error & { detail?: unknown; status?: number };
    err.detail = detail;
    err.status = res.status;
    // Human-readable message so any `(e as Error).message` fallback in the UI
    // shows a real sentence instead of "api_error_409".
    err.message = humanizeError(err);
    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);
  }
}
