/**
 * The API client.
 *
 * One place that knows how to attach credentials and interpret failures, so no
 * component has to. The distinction that matters is between 401 (not signed
 * in) and 403 `needs_profile` (signed in, but no PIG account) — those need
 * completely different screens, and collapsing them into "auth error" produces
 * the classic bug where an invited user is bounced to a login page they have
 * already completed.
 */
import { createClient, type SupabaseClient } from '@supabase/supabase-js';

export interface PublicConfig {
  supabaseUrl: string | null;
  supabaseAnonKey: string | null;
  authDisabled: boolean;
  inviteRequired: boolean;
  canSelfRegister: boolean;
  accents: { key: string; label: string }[];
  teams: string[];
}

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

  get needsProfile(): boolean {
    return this.status === 403 && this.code === 'needs_profile';
  }

  get needsSignIn(): boolean {
    return this.status === 401;
  }
}

let supabase: SupabaseClient | null = null;
let publicConfig: PublicConfig | null = null;

export async function loadPublicConfig(): Promise<PublicConfig> {
  if (publicConfig) return publicConfig;
  const response = await fetch('/api/config');
  if (!response.ok) throw new Error('Could not load configuration from the server.');
  publicConfig = (await response.json()) as PublicConfig;

  if (publicConfig.supabaseUrl && publicConfig.supabaseAnonKey) {
    supabase = createClient(publicConfig.supabaseUrl, publicConfig.supabaseAnonKey, {
      auth: {
        persistSession: true,
        autoRefreshToken: true,
        // The session lands in a URL fragment after an email link; picking it
        // up automatically is what makes magic-link sign-in work.
        detectSessionInUrl: true,
      },
    });
  }
  return publicConfig;
}

export function getSupabase(): SupabaseClient | null {
  return supabase;
}

export function getPublicConfig(): PublicConfig | null {
  return publicConfig;
}

async function authHeader(): Promise<Record<string, string>> {
  if (!supabase) return {};
  // getSession refreshes an expired token transparently, which is why this is
  // read per request rather than cached at sign-in.
  const { data } = await supabase.auth.getSession();
  const token = data.session?.access_token;
  return token ? { authorization: `Bearer ${token}` } : {};
}

export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
  const response = await fetch(path, {
    ...init,
    headers: {
      'content-type': 'application/json',
      ...(await authHeader()),
      ...(init.headers ?? {}),
    },
  });

  if (!response.ok) {
    let message = response.statusText;
    let code: string | undefined;
    try {
      const body = (await response.json()) as { error?: string; code?: string };
      message = body.error ?? message;
      code = body.code;
    } catch {
      /* Not every error response is JSON — a proxy may return HTML. */
    }
    throw new ApiError(message, response.status, code);
  }

  // 204 and empty bodies are legitimate; parsing them would throw.
  if (response.status === 204) return undefined as T;
  const text = await response.text();
  return (text ? JSON.parse(text) : undefined) as T;
}

export const get = <T,>(path: string) => api<T>(path);
export const post = <T,>(path: string, body: unknown) =>
  api<T>(path, { method: 'POST', body: JSON.stringify(body) });
export const patch = <T,>(path: string, body: unknown) =>
  api<T>(path, { method: 'PATCH', body: JSON.stringify(body) });

// ---------------------------------------------------------------- formatting

/**
 * Money, from integer cents.
 *
 * Compact above a million because a pipeline view showing "$12,400,000.00" in
 * a phone-width column is unreadable, and the exact cent is never the point at
 * that magnitude.
 */
export function money(cents: number | null | undefined, currency = 'USD'): string {
  if (cents == null) return '—';
  const value = cents / 100;
  const compact = Math.abs(value) >= 1_000_000;
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
    notation: compact ? 'compact' : 'standard',
    maximumFractionDigits: compact ? 1 : value % 1 === 0 ? 0 : 2,
  }).format(value);
}

/** Money at full precision — for detail views where the cent does matter. */
export function moneyExact(cents: number | null | undefined, currency = 'USD'): string {
  if (cents == null) return '—';
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  }).format(cents / 100);
}

export function percent(value: number | null | undefined, digits = 0): string {
  if (value == null || !Number.isFinite(value)) return '—';
  return `${(value * 100).toFixed(digits)}%`;
}

export function compactNumber(value: number | null | undefined): string {
  if (value == null) return '—';
  return new Intl.NumberFormat('en-US', {
    notation: Math.abs(value) >= 10_000 ? 'compact' : 'standard',
    maximumFractionDigits: 1,
  }).format(value);
}

export function shortDate(value: string | Date | null | undefined): string {
  if (!value) return '—';
  const date = typeof value === 'string' ? new Date(value) : value;
  if (Number.isNaN(date.getTime())) return '—';
  return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(date);
}

export function relativeTime(value: string | Date | null | undefined): string {
  if (!value) return '—';
  const date = typeof value === 'string' ? new Date(value) : value;
  if (Number.isNaN(date.getTime())) return '—';

  const seconds = Math.round((date.getTime() - Date.now()) / 1000);
  const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
  const divisions: [number, Intl.RelativeTimeFormatUnit][] = [
    [60, 'second'],
    [60, 'minute'],
    [24, 'hour'],
    [7, 'day'],
    [4.34524, 'week'],
    [12, 'month'],
    [Number.POSITIVE_INFINITY, 'year'],
  ];

  let duration = seconds;
  for (const [amount, unit] of divisions) {
    if (Math.abs(duration) < amount) return formatter.format(Math.round(duration), unit);
    duration /= amount;
  }
  return formatter.format(Math.round(duration), 'year');
}
