import { env } from '$env/dynamic/public';
import { browser } from '$app/environment';
import type {
  ClientAccessAccount,
  ClientAccessPowerBiExport,
  DashboardSummary,
  ClientUserCreateInput,
  ClientUserModulePermission,
  ClientUserUpdateInput,
  InternalUser,
  InternalRoleOption,
  InternalRole,
  InternalRoleCreateInput,
  InternalRoleModuleDefinition,
  InternalRoleUpdateInput,
  InternalUserCreateInput,
  InternalUserUpdateInput,
  LoginResponse,
  EditorMixCreateInput,
  EditorMixUpdateInput,
  EditorMixRow,
  EditorMixFormula,
  EditorResolvedMixFormula,
  EditorMixFormulaRowInput,
  EditorIngredientRow,
  EditorIngredientCreateInput,
  EditorIngredientUpdateInput,
  EditorChangeEvent,
  EditorProductFormula,
  EditorProductRow,
  EditorProductUpdateInput,
  MixCalculatorCreateInput,
  MixCalculatorOptions,
  MixCalculatorPreview,
  MixCalculatorSession,
  MixCalculatorUpdateInput,
  Mix,
  MixCreateInput,
  MixIngredientUpdateInput,
  MixUpdateInput,
  Product,
  ProductCostBreakdown,
  ProductCostingInputs,
  ProductCostingItem,
  ProductCostingItemUpdateInput,
  RawMaterial,
  RawMaterialCreateInput,
  RawMaterialPriceCreateInput,
  CatalogueProduct,
  CustomerPricing,
  CustomerVisibilityRow,
  DraftOrderInput,
  Order,
  OrderingCustomer,
  OrderingCustomerUser,
  OrderingNotificationSettings,
  XeroStatus,
  XeroContactList,
  XeroContactLinkRow,
  Scenario,
  ThroughputDeleteAllResult,
  ThroughputEntry,
  ThroughputEntryCreateInput,
  ThroughputEntryUpdateInput,
  ThroughputEntryListParams,
  ThroughputImportResult,
  ThroughputProduct,
  ThroughputProductCreateInput,
  ThroughputProductUpdateInput
} from '$lib/types';
import { getStoredAdminSession, getStoredClientSession } from '$lib/session';

const BACKEND_UNAVAILABLE_MESSAGE = 'Unable to reach the server. Check that the backend is running and try again.';

type AuthMode = 'none' | 'client' | 'admin' | 'manager';
type ApiFetch = typeof fetch;

function getApiBaseUrl() {
  if (!browser) {
    const internalBaseUrl = typeof process !== 'undefined' ? process.env.INTERNAL_API_BASE_URL?.trim() : '';
    if (internalBaseUrl) {
      return internalBaseUrl.replace(/\/+$/, '');
    }
  }

  if (browser) {
    const configuredBaseUrl = env.PUBLIC_API_BASE_URL?.trim();
    if (configuredBaseUrl) {
      try {
        const configuredUrl = new URL(configuredBaseUrl, window.location.origin);
        // Keep browser API traffic same-origin by default. This avoids CORS,
        // CSP `connect-src`, and cookie policy failures when the backend is
        // reverse-proxied under `/api` on the same host.
        if (configuredUrl.origin === window.location.origin || configuredUrl.hostname === window.location.hostname) {
          return '';
        }
        return configuredUrl.toString().replace(/\/+$/, '');
      } catch {
        return '';
      }
    }

    return '';
  }

  const defaultApiPort = env.PUBLIC_API_PORT || '8000';
  return `http://127.0.0.1:${defaultApiPort}`;
}

function buildApiUrl(path: string) {
  return `${getApiBaseUrl()}${path}`;
}

function getSessionFingerprint(auth: AuthMode) {
  if (auth === 'client') {
    const session = getStoredClientSession();
    return session ? `${session.role}:${session.email}:${session.user_id ?? ''}` : '';
  }

  if (auth === 'admin') {
    const session = getStoredAdminSession();
    return session ? `${session.role}:${session.email}` : '';
  }

  if (auth === 'manager') {
    const admin = getStoredAdminSession();
    if (admin) {
      return `${admin.role}:${admin.email}`;
    }
    const client = getStoredClientSession();
    return client ? `${client.role}:${client.email}:${client.user_id ?? ''}` : '';
  }

  return '';
}

function resolveRequestUrl(path: string, fetcher: ApiFetch) {
  if (fetcher !== fetch) {
    return path;
  }
  return buildApiUrl(path);
}

function normalizeRequestError(error: unknown) {
  if (error instanceof Error) {
    const message = error.message.trim();
    const isNetworkFetchFailure =
      /failed to fetch|fetch failed|networkerror when attempting to fetch resource|load failed|network request failed/i.test(
        message
      ) || error.name === 'NetworkError';

    if (isNetworkFetchFailure) {
      return new Error(BACKEND_UNAVAILABLE_MESSAGE);
    }

    return error;
  }

  return new Error('An unexpected error occurred while contacting the server.');
}

async function fetchJson<T>(path: string, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise<T> {
  try {
    const response = await fetcher(resolveRequestUrl(path, fetcher), {
      credentials: 'include'
    });
    if (!response.ok) {
      throw new Error(response.statusText || 'Request failed');
    }
    return (await response.json()) as T;
  } catch (error) {
    throw normalizeRequestError(error);
  }
}

// In-memory GET cache with TTL + in-flight de-duplication. The cache key
// includes the auth-mode and last 8 chars of the bearer token so different
// sessions can't read each other's entries. Any mutation calls clearApiCache()
// to invalidate. Memory footprint is bounded by entries naturally aging out.
type CacheEntry = { value: unknown; expiresAt: number };
const responseCache = new Map<string, CacheEntry>();
const inflightRequests = new Map<string, Promise<unknown>>();
const READ_CACHE_TTL_MS = 30_000;

function makeCacheKey(path: string, auth: AuthMode) {
  const sessionFingerprint = browser ? getSessionFingerprint(auth) : '';
  return `${auth}:${sessionFingerprint}:${path}`;
}

async function cachedFetchJson<T>(
  path: string,
  auth: AuthMode = 'none',
  fetcher: ApiFetch = fetch
): Promise<T> {
  // Bypass the cache during SSR (no localStorage, no shared session).
  if (!browser) {
    return fetchJson<T>(path, auth, fetcher);
  }

  const key = makeCacheKey(path, auth);
  const now = Date.now();
  const cached = responseCache.get(key);
  if (cached && cached.expiresAt > now) {
    return cached.value as T;
  }

  // De-duplicate concurrent callers (e.g. two effects firing the same load).
  const existing = inflightRequests.get(key);
  if (existing) {
    return existing as Promise<T>;
  }

  const promise = fetchJson<T>(path, auth, fetcher)
    .then((value) => {
      responseCache.set(key, { value, expiresAt: Date.now() + READ_CACHE_TTL_MS });
      return value;
    })
    .finally(() => {
      inflightRequests.delete(key);
    });

  inflightRequests.set(key, promise);
  return promise;
}

export function clearApiCache() {
  responseCache.clear();
  inflightRequests.clear();
}

async function request<T>(
  path: string,
  options: RequestInit,
  auth: AuthMode = 'none',
  fetcher: ApiFetch = fetch
): Promise<T> {
  try {
    const response = await fetcher(resolveRequestUrl(path, fetcher), {
      headers: {
        'Content-Type': 'application/json',
        ...(options.headers ?? {})
      },
      credentials: 'include',
      ...options
    });

    if (!response.ok) {
      let message = 'Request failed';

      try {
        const body = (await response.json()) as { detail?: string };
        message = body.detail ?? message;
      } catch {
        message = response.statusText || message;
      }

      throw new Error(message);
    }

    const isMutation = !!options.method && options.method.toUpperCase() !== 'GET';
    if (isMutation && browser) {
      // Mutations invalidate cached reads — keeps Dashboard / lists fresh
      // after the user creates or updates anything.
      clearApiCache();
    }
    if (response.status === 204) {
      return undefined as T;
    }
    return (await response.json()) as T;
  } catch (error) {
    throw normalizeRequestError(error);
  }
}

// Multipart upload. Unlike `request`, we must NOT set Content-Type ourselves —
// the browser sets `multipart/form-data` with the correct boundary when given a
// FormData body. Mirrors `request`'s auth/cache/error handling otherwise.
async function uploadFile<T>(
  path: string,
  formData: FormData,
  auth: AuthMode = 'none',
  fetcher: ApiFetch = fetch
): Promise<T> {
  try {
    const response = await fetcher(resolveRequestUrl(path, fetcher), {
      method: 'POST',
      body: formData,
      credentials: 'include'
    });

    if (!response.ok) {
      let message = 'Request failed';
      try {
        const body = (await response.json()) as { detail?: string };
        message = body.detail ?? message;
      } catch {
        message = response.statusText || message;
      }
      throw new Error(message);
    }

    if (browser) {
      clearApiCache();
    }
    if (response.status === 204) {
      return undefined as T;
    }
    return (await response.json()) as T;
  } catch (error) {
    throw normalizeRequestError(error);
  }
}

async function requestBlob(
  path: string,
  options: RequestInit = {},
  auth: AuthMode = 'none',
  fetcher: ApiFetch = fetch
): Promise<Blob> {
  try {
    const response = await fetcher(resolveRequestUrl(path, fetcher), {
      headers: {
        'Content-Type': 'application/json',
        ...(options.headers ?? {})
      },
      credentials: 'include',
      ...options
    });

    if (!response.ok) {
      let message = 'Request failed';
      try {
        const body = (await response.json()) as { detail?: string };
        message = body.detail ?? message;
      } catch {
        message = response.statusText || message;
      }
      throw new Error(message);
    }

    return await response.blob();
  } catch (error) {
    throw normalizeRequestError(error);
  }
}

export const api = {
  rawMaterials: (fetcher?: ApiFetch) => cachedFetchJson<RawMaterial[]>('/api/raw-materials', 'client', fetcher),
  mixes: (fetcher?: ApiFetch) => cachedFetchJson<Mix[]>('/api/mixes', 'client', fetcher),
  mix: (mixId: number, fetcher?: ApiFetch) => request<Mix>(`/api/mixes/${mixId}`, { method: 'GET' }, 'client', fetcher),
  mixCalculatorOptions: (fetcher?: ApiFetch) =>
    cachedFetchJson<MixCalculatorOptions>('/api/mix-calculator/options', 'client', fetcher),
  mixCalculatorSessions: (fetcher?: ApiFetch) =>
    cachedFetchJson<MixCalculatorSession[]>('/api/mix-calculator', 'client', fetcher),
  mixCalculatorSession: (sessionId: number, fetcher?: ApiFetch) =>
    request<MixCalculatorSession>(`/api/mix-calculator/${sessionId}`, { method: 'GET' }, 'client', fetcher),
  mixCalculatorSessionPdf: (sessionId: number, fetcher?: ApiFetch) =>
    requestBlob(`/api/mix-calculator/${sessionId}/pdf`, {}, 'client', fetcher),
  previewMixCalculatorSession: (payload: MixCalculatorCreateInput) =>
    request<MixCalculatorPreview>('/api/mix-calculator/preview', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  previewMixCalculatorPdf: (payload: MixCalculatorCreateInput) =>
    requestBlob('/api/mix-calculator/preview/pdf', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  createMixCalculatorSession: (payload: MixCalculatorCreateInput) =>
    request<MixCalculatorSession>('/api/mix-calculator', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateMixCalculatorSession: (sessionId: number, payload: MixCalculatorUpdateInput) =>
    request<MixCalculatorSession>(`/api/mix-calculator/${sessionId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  products: (fetcher?: ApiFetch) => cachedFetchJson<Product[]>('/api/products', 'client', fetcher),
  editorProducts: (params?: { q?: string; client_name?: string; limit?: number }, fetcher?: ApiFetch) => {
    const search = new URLSearchParams();
    if (params?.q) search.set('q', params.q);
    if (params?.client_name) search.set('client_name', params.client_name);
    if (params?.limit) search.set('limit', String(params.limit));
    const qs = search.toString();
    const path = qs ? `/api/editor/products?${qs}` : '/api/editor/products';
    return cachedFetchJson<EditorProductRow[]>(path, 'client', fetcher);
  },
  updateEditorProduct: (productId: number, payload: EditorProductUpdateInput) =>
    request<EditorProductRow>(`/api/editor/products/${productId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  editorMixes: (params?: { q?: string; client_name?: string; limit?: number }, fetcher?: ApiFetch) => {
    const search = new URLSearchParams();
    if (params?.q) search.set('q', params.q);
    if (params?.client_name) search.set('client_name', params.client_name);
    if (params?.limit) search.set('limit', String(params.limit));
    const qs = search.toString();
    const path = qs ? `/api/editor/mixes?${qs}` : '/api/editor/mixes';
    return cachedFetchJson<EditorMixRow[]>(path, 'client', fetcher);
  },
  createEditorMix: (payload: EditorMixCreateInput) =>
    request<EditorMixRow>('/api/editor/mixes', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateEditorMix: (mixId: number, payload: EditorMixUpdateInput) =>
    request<EditorMixRow>(`/api/editor/mixes/${mixId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  editorMixFormula: (mixId: number) =>
    request<EditorMixFormula>(`/api/editor/mixes/${mixId}/ingredients`, {}, 'client'),
  // The resolved formula matching the Mix Calculator (product-first), used by
  // the Mix Editor ingredient panel.
  editorMixResolvedFormula: (mixId: number) =>
    request<EditorResolvedMixFormula>(`/api/editor/mixes/${mixId}/formula`, {}, 'client'),
  replaceEditorMixFormula: (mixId: number, rows: EditorMixFormulaRowInput[]) =>
    request<EditorResolvedMixFormula>(`/api/editor/mixes/${mixId}/formula`, {
      method: 'PUT',
      body: JSON.stringify({ rows })
    }, 'client'),
  addEditorMixIngredient: (mixId: number, payload: { raw_material_id: number; quantity_kg: number; notes?: string | null }) =>
    request<EditorMixFormula>(`/api/editor/mixes/${mixId}/ingredients`, {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateEditorMixIngredient: (mixId: number, ingredientId: number, payload: MixIngredientUpdateInput) =>
    request<EditorMixFormula>(`/api/editor/mixes/${mixId}/ingredients/${ingredientId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  deleteEditorMixIngredient: (mixId: number, ingredientId: number) =>
    request<EditorMixFormula>(`/api/editor/mixes/${mixId}/ingredients/${ingredientId}`, {
      method: 'DELETE'
    }, 'client'),
  editorProductFormula: (productId: number) =>
    request<EditorProductFormula>(`/api/editor/products/${productId}/ingredients`, {}, 'client'),
  addEditorProductIngredient: (productId: number, payload: { raw_material_id: number; quantity_kg: number; notes?: string | null }) =>
    request<EditorProductFormula>(`/api/editor/products/${productId}/ingredients`, {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateEditorProductIngredient: (productId: number, ingredientId: number, payload: MixIngredientUpdateInput) =>
    request<EditorProductFormula>(`/api/editor/products/${productId}/ingredients/${ingredientId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  deleteEditorProductIngredient: (productId: number, ingredientId: number) =>
    request<EditorProductFormula>(`/api/editor/products/${productId}/ingredients/${ingredientId}`, {
      method: 'DELETE'
    }, 'client'),
  editorIngredients: (fetcher?: ApiFetch) =>
    cachedFetchJson<EditorIngredientRow[]>('/api/editor/ingredients', 'client', fetcher),
  createEditorIngredient: (payload: EditorIngredientCreateInput) =>
    request<EditorIngredientRow>('/api/editor/ingredients', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateEditorIngredient: (ingredientId: number, payload: EditorIngredientUpdateInput) =>
    request<EditorIngredientRow>(`/api/editor/ingredients/${ingredientId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  editorMixHistory: (mixId: number) =>
    request<EditorChangeEvent[]>(`/api/editor/mixes/${mixId}/history`, {}, 'client'),
  editorIngredientHistory: (ingredientId: number) =>
    request<EditorChangeEvent[]>(`/api/editor/ingredients/${ingredientId}/history`, {}, 'client'),
  productCosts: (fetcher?: ApiFetch) =>
    cachedFetchJson<ProductCostBreakdown[]>('/api/powerbi/product-costs', 'client', fetcher),
  productCostingItems: (fetcher?: ApiFetch) =>
    cachedFetchJson<ProductCostingItem[]>('/api/product-costing/items', 'client', fetcher),
  productCostingItemsFresh: () =>
    request<ProductCostingItem[]>(`/api/product-costing/items?_=${Date.now()}`, { method: 'GET' }, 'client'),
  productCostingInputs: (fetcher?: ApiFetch) =>
    cachedFetchJson<ProductCostingInputs>('/api/product-costing/inputs', 'client', fetcher),
  updateProductCostingInputs: (payload: Partial<ProductCostingInputs>) =>
    request<ProductCostingInputs>('/api/product-costing/inputs', {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  updateProductCostingItem: (itemId: number, payload: ProductCostingItemUpdateInput) =>
    request<ProductCostingItem>(`/api/product-costing/items/${itemId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  recalculateProductCosting: () =>
    request<{ recalculated: number }>('/api/product-costing/recalculate-all', { method: 'POST' }, 'client'),
  scenarios: (fetcher?: ApiFetch) => cachedFetchJson<Scenario[]>('/api/scenarios', 'client', fetcher),
  throughputProducts: (fetcher?: ApiFetch) =>
    cachedFetchJson<ThroughputProduct[]>('/api/throughput/products', 'client', fetcher),
  throughputEntries: (params?: ThroughputEntryListParams, fetcher?: ApiFetch) => {
    const search = new URLSearchParams();
    if (params?.date_from) search.set('date_from', params.date_from);
    if (params?.date_to) search.set('date_to', params.date_to);
    if (params?.product_id != null) search.set('product_id', String(params.product_id));
    if (params?.staff_name) search.set('staff_name', params.staff_name);
    if (params?.quantity_type) search.set('quantity_type', params.quantity_type);
    if (params?.limit) search.set('limit', String(params.limit));
    const qs = search.toString();
    const path = qs ? `/api/throughput/entries?${qs}` : '/api/throughput/entries';
    return cachedFetchJson<ThroughputEntry[]>(path, 'client', fetcher);
  },
  createThroughputEntry: (payload: ThroughputEntryCreateInput) =>
    request<ThroughputEntry>('/api/throughput/entries', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateThroughputEntry: (entryId: number, payload: ThroughputEntryUpdateInput) =>
    request<ThroughputEntry>(`/api/throughput/entries/${entryId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  deleteThroughputEntry: (entryId: number) =>
    request<void>(`/api/throughput/entries/${entryId}`, { method: 'DELETE' }, 'client'),
  importThroughputEntries: (file: File) => {
    const formData = new FormData();
    formData.append('file', file);
    return uploadFile<ThroughputImportResult>('/api/throughput/import', formData, 'client');
  },
  deleteAllThroughputEntries: () =>
    request<ThroughputDeleteAllResult>('/api/throughput/entries', { method: 'DELETE' }, 'client'),
  createThroughputProduct: (payload: ThroughputProductCreateInput) =>
    request<ThroughputProduct>('/api/throughput/products', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateThroughputProduct: (productId: number, payload: ThroughputProductUpdateInput) =>
    request<ThroughputProduct>(`/api/throughput/products/${productId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  clientAccess: (fetcher?: ApiFetch) => cachedFetchJson<ClientAccessAccount[]>('/api/client-access', 'manager', fetcher),
  clientAccessExport: (fetcher?: ApiFetch) =>
    cachedFetchJson<ClientAccessPowerBiExport>('/api/powerbi/client-access', 'manager', fetcher),
  dataQuality: (fetcher?: ApiFetch) => cachedFetchJson('/api/powerbi/data-quality-issues', 'client', fetcher),
  dashboardSummary: (fetcher?: ApiFetch) =>
    cachedFetchJson<DashboardSummary>('/api/dashboard/summary', 'client', fetcher),
  clientLogin: (email: string, password: string) =>
    request<LoginResponse>('/api/auth/client/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    }),
  // Internal Hunter Stock Feeds login. Returns the same LoginResponse shape
  // (with `permissions` populated) so the existing client-session store can
  // consume it directly.
  internalLogin: (email: string, password: string) =>
    request<LoginResponse>('/api/access/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    }),
  internalSession: (fetcher?: ApiFetch) =>
    request<LoginResponse>('/api/access/me', { method: 'GET' }, 'client', fetcher),
  updateMe: (payload: { name?: string; email?: string; current_password?: string; new_password?: string }) =>
    request<LoginResponse>('/api/access/me', {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  // --- Internal user management (lean/admin: manage_users) ------------------
  accessUsers: (fetcher?: ApiFetch) =>
    request<InternalUser[]>('/api/access/users', { method: 'GET' }, 'client', fetcher),
  accessAssignableRoles: (fetcher?: ApiFetch) =>
    request<InternalRoleOption[]>('/api/access/assignable-roles', { method: 'GET' }, 'client', fetcher),
  accessRoles: (fetcher?: ApiFetch) =>
    request<InternalRole[]>('/api/access/roles', { method: 'GET' }, 'client', fetcher),
  accessRoleModules: (fetcher?: ApiFetch) =>
    request<InternalRoleModuleDefinition[]>('/api/access/role-modules', { method: 'GET' }, 'client', fetcher),
  createAccessRole: (payload: InternalRoleCreateInput) =>
    request<InternalRole>('/api/access/roles', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateAccessRole: (roleId: number, payload: InternalRoleUpdateInput) =>
    request<InternalRole>(`/api/access/roles/${roleId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  deleteAccessRole: (roleId: number) =>
    request<void>(`/api/access/roles/${roleId}`, { method: 'DELETE' }, 'client'),
  createAccessUser: (payload: InternalUserCreateInput) =>
    request<InternalUser>('/api/access/users', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateAccessUser: (userId: number, payload: InternalUserUpdateInput) =>
    request<InternalUser>(`/api/access/users/${userId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  setAccessUserPassword: (userId: number, newPassword: string) =>
    request<InternalUser>(`/api/access/users/${userId}/password`, {
      method: 'POST',
      body: JSON.stringify({ new_password: newPassword })
    }, 'client'),
  deleteAccessUser: (userId: number) =>
    request<void>(`/api/access/users/${userId}`, { method: 'DELETE' }, 'client'),
  adminLogin: (email: string, password: string) =>
    request<LoginResponse>('/api/auth/admin/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    }),
  clientSession: (fetcher?: ApiFetch) => request<LoginResponse>('/api/auth/client/session', { method: 'GET' }, 'client', fetcher),
  adminSession: (fetcher?: ApiFetch) => request<LoginResponse>('/api/auth/admin/session', { method: 'GET' }, 'admin', fetcher),
  clientLogout: () => request<void>('/api/auth/client/logout', { method: 'POST' }, 'client'),
  adminLogout: () => request<void>('/api/auth/admin/logout', { method: 'POST' }, 'admin'),
  internalLogout: () => request<void>('/api/access/logout', { method: 'POST' }, 'client'),
  login: (email: string, password: string) =>
    request<LoginResponse>('/api/auth/client/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    }),
  createMix: (payload: MixCreateInput) =>
    request<Mix>('/api/mixes', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateMix: (mixId: number, payload: MixUpdateInput) =>
    request<Mix>(`/api/mixes/${mixId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  addMixIngredient: (mixId: number, payload: { raw_material_id: number; quantity_kg: number; notes?: string | null }) =>
    request<Mix>(`/api/mixes/${mixId}/ingredients`, {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  updateMixIngredient: (mixId: number, ingredientId: number, payload: MixIngredientUpdateInput) =>
    request<Mix>(`/api/mixes/${mixId}/ingredients/${ingredientId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'client'),
  deleteMixIngredient: (mixId: number, ingredientId: number) =>
    request<Mix>(`/api/mixes/${mixId}/ingredients/${ingredientId}`, {
      method: 'DELETE'
    }, 'client'),
  createRawMaterial: (payload: RawMaterialCreateInput) =>
    request<RawMaterial>('/api/raw-materials', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  addRawMaterialPrice: (rawMaterialId: number, payload: RawMaterialPriceCreateInput) =>
    request(`/api/raw-materials/${rawMaterialId}/prices`, {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'client'),
  createClientUser: (payload: ClientUserCreateInput) =>
    request<ClientAccessAccount>('/api/client-access/users', {
      method: 'POST',
      body: JSON.stringify(payload)
    }, 'manager'),
  updateClientUser: (userId: number, payload: ClientUserUpdateInput) =>
    request<ClientAccessAccount>(`/api/client-access/users/${userId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'manager'),
  updateClientUserModulePermission: (userId: number, permission: Pick<ClientUserModulePermission, 'module_key'>, payload: { access_level: string }) =>
    request<ClientAccessAccount>(`/api/client-access/users/${userId}/module-permissions/${permission.module_key}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'manager'),
  updateClientFeature: (featureId: number, payload: { enabled: boolean }) =>
    request<ClientAccessAccount>(`/api/client-access/features/${featureId}`, {
      method: 'PATCH',
      body: JSON.stringify(payload)
    }, 'manager'),

  // --- B2B ordering portal (customer) ---------------------------------------
  ordering: {
    catalogue: (params?: { category?: string; q?: string }, fetcher?: ApiFetch) => {
      const search = new URLSearchParams();
      if (params?.category) search.set('category', params.category);
      if (params?.q) search.set('q', params.q);
      const qs = search.toString();
      return cachedFetchJson<CatalogueProduct[]>(`/api/ordering/catalogue${qs ? `?${qs}` : ''}`, 'client', fetcher);
    },
    product: (productId: number, quantity = 1, fetcher?: ApiFetch) =>
      request<CatalogueProduct>(`/api/ordering/catalogue/${productId}?quantity=${quantity}`, { method: 'GET' }, 'client', fetcher),
    orders: (statusFilter?: string, fetcher?: ApiFetch) =>
      cachedFetchJson<Order[]>(`/api/ordering/orders${statusFilter ? `?status=${statusFilter}` : ''}`, 'client', fetcher),
    order: (orderId: number, fetcher?: ApiFetch) =>
      request<Order>(`/api/ordering/orders/${orderId}`, { method: 'GET' }, 'client', fetcher),
    createDraft: (payload: DraftOrderInput) =>
      request<Order>('/api/ordering/orders', { method: 'POST', body: JSON.stringify(payload) }, 'client'),
    updateDraft: (orderId: number, payload: Partial<DraftOrderInput>) =>
      request<Order>(`/api/ordering/orders/${orderId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'),
    deleteDraft: (orderId: number) =>
      request<void>(`/api/ordering/orders/${orderId}`, { method: 'DELETE' }, 'client'),
    submit: (orderId: number, payload: Partial<DraftOrderInput> = {}) =>
      request<Order>(`/api/ordering/orders/${orderId}/submit`, { method: 'POST', body: JSON.stringify(payload) }, 'client'),
    reorder: (orderId: number) =>
      request<Order>(`/api/ordering/orders/${orderId}/reorder`, { method: 'POST' }, 'client'),
    confirmationPdf: (orderId: number) =>
      requestBlob(`/api/ordering/orders/${orderId}/confirmation.pdf`, {}, 'client')
  },

  // --- B2B ordering portal (admin) ------------------------------------------
  orderingAdmin: {
    customers: (fetcher?: ApiFetch) => cachedFetchJson<OrderingCustomer[]>('/api/ordering-admin/customers', 'client', fetcher),
    createCustomer: (payload: { name: string; client_code: string; tenant_id?: string; notes?: string }) =>
      request<OrderingCustomer>('/api/ordering-admin/customers', { method: 'POST', body: JSON.stringify(payload) }, 'client'),
    updateCustomer: (customerId: number, payload: { name?: string; status?: string; notes?: string }) =>
      request<OrderingCustomer>(`/api/ordering-admin/customers/${customerId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'),
    customerUsers: (customerId: number, fetcher?: ApiFetch) =>
      cachedFetchJson<OrderingCustomerUser[]>(`/api/ordering-admin/customers/${customerId}/users`, 'client', fetcher),
    createCustomerUser: (customerId: number, payload: { full_name: string; email: string; role: string }) =>
      request<OrderingCustomerUser>(`/api/ordering-admin/customers/${customerId}/users`, { method: 'POST', body: JSON.stringify(payload) }, 'client'),
    updateCustomerUser: (customerId: number, userId: number, payload: { full_name?: string; role?: string; status?: string }) =>
      request<OrderingCustomerUser>(`/api/ordering-admin/customers/${customerId}/users/${userId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'),
    products: (fetcher?: ApiFetch) => cachedFetchJson<CatalogueProduct[]>('/api/ordering-admin/products', 'client', fetcher),
    createProduct: (payload: Partial<CatalogueProduct>) =>
      request<CatalogueProduct>('/api/ordering-admin/products', { method: 'POST', body: JSON.stringify(payload) }, 'client'),
    updateProduct: (productId: number, payload: Partial<CatalogueProduct>) =>
      request<CatalogueProduct>(`/api/ordering-admin/products/${productId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'),
    visibility: (customerId: number, fetcher?: ApiFetch) =>
      cachedFetchJson<CustomerVisibilityRow[]>(`/api/ordering-admin/customers/${customerId}/visibility`, 'client', fetcher),
    setVisibility: (customerId: number, payload: { product_id: number; visible: boolean }) =>
      request(`/api/ordering-admin/customers/${customerId}/visibility`, { method: 'PUT', body: JSON.stringify(payload) }, 'client'),
    pricing: (customerId: number, fetcher?: ApiFetch) =>
      cachedFetchJson<CustomerPricing>(`/api/ordering-admin/customers/${customerId}/pricing`, 'client', fetcher),
    setAssignment: (customerId: number, payload: { price_list_id: number | null; discount_percent: number }) =>
      request<CustomerPricing>(`/api/ordering-admin/customers/${customerId}/assignment`, { method: 'PUT', body: JSON.stringify(payload) }, 'client'),
    setProductPrice: (
      customerId: number,
      payload: { product_id: number; unit_price: number | null; rule_type: string; contract_reference?: string | null; notes?: string | null; active?: boolean }
    ) => request<CustomerPricing>(`/api/ordering-admin/customers/${customerId}/product-prices`, { method: 'PUT', body: JSON.stringify(payload) }, 'client'),
    deleteProductPrice: (customerId: number, productId: number) =>
      request<void>(`/api/ordering-admin/customers/${customerId}/product-prices/${productId}`, { method: 'DELETE' }, 'client'),
    orders: (params?: { status?: string; customer_id?: number }, fetcher?: ApiFetch) => {
      const search = new URLSearchParams();
      if (params?.status) search.set('status', params.status);
      if (params?.customer_id != null) search.set('customer_id', String(params.customer_id));
      const qs = search.toString();
      return cachedFetchJson<Order[]>(`/api/ordering-admin/orders${qs ? `?${qs}` : ''}`, 'client', fetcher);
    },
    order: (orderId: number, fetcher?: ApiFetch) =>
      request<Order>(`/api/ordering-admin/orders/${orderId}`, { method: 'GET' }, 'client', fetcher),
    updateStatus: (orderId: number, payload: { to_status: string; note?: string }) =>
      request<Order>(`/api/ordering-admin/orders/${orderId}/status`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'),
    overrideLine: (orderId: number, lineId: number, payload: { quantity?: number; unit_price?: number; reason?: string }) =>
      request<Order>(`/api/ordering-admin/orders/${orderId}/lines/${lineId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'),
    reopen: (orderId: number, note?: string) =>
      request<Order>(`/api/ordering-admin/orders/${orderId}/reopen`, { method: 'POST', body: JSON.stringify({ note }) }, 'client'),
    sendToXero: (orderId: number) =>
      request<Order>(`/api/ordering-admin/orders/${orderId}/send-to-xero`, { method: 'POST' }, 'client'),
    notificationSettings: (fetcher?: ApiFetch) =>
      cachedFetchJson<OrderingNotificationSettings>('/api/ordering-admin/notification-settings', 'client', fetcher),
    updateNotificationSettings: (payload: Partial<OrderingNotificationSettings>) =>
      request<OrderingNotificationSettings>('/api/ordering-admin/notification-settings', { method: 'PATCH', body: JSON.stringify(payload) }, 'client'),
    xeroStatus: (fetcher?: ApiFetch) => cachedFetchJson<XeroStatus>('/api/ordering-admin/xero/status', 'client', fetcher),
    xeroContacts: (fetcher?: ApiFetch) =>
      cachedFetchJson<XeroContactList>('/api/ordering-admin/xero/contacts', 'client', fetcher),
    xeroContactLinks: (fetcher?: ApiFetch) =>
      cachedFetchJson<XeroContactLinkRow[]>('/api/ordering-admin/xero/contact-links', 'client', fetcher),
    linkCustomerToXero: (
      customerId: number,
      payload: { xero_contact_id: string; xero_contact_name?: string | null; xero_contact_email?: string | null }
    ) => request(`/api/ordering-admin/customers/${customerId}/xero-link`, { method: 'PUT', body: JSON.stringify(payload) }, 'client'),
    unlinkCustomerFromXero: (customerId: number) =>
      request<void>(`/api/ordering-admin/customers/${customerId}/xero-link`, { method: 'DELETE' }, 'client')
  }
};
