import { env } from '$env/dynamic/public';
import { browser } from '$app/environment';
import {
  mockClientAccess,
  mockClientAccessExport,
  mockCosts,
  mockMixCalculatorOptions,
  mockMixCalculatorSessions,
  mockMixes,
  mockProducts,
  mockRawMaterials,
  mockScenarios
} from '$lib/mock';
import type {
  ClientAccessAccount,
  ClientAccessPowerBiExport,
  DashboardSummary,
  ClientUserCreateInput,
  ClientUserModulePermission,
  ClientUserUpdateInput,
  LoginResponse,
  MixCalculatorCreateInput,
  MixCalculatorOptions,
  MixCalculatorPreview,
  MixCalculatorSession,
  MixCalculatorUpdateInput,
  Mix,
  MixCreateInput,
  MixIngredientUpdateInput,
  MixUpdateInput,
  Product,
  ProductCostBreakdown,
  RawMaterial,
  RawMaterialCreateInput,
  RawMaterialPriceCreateInput,
  Scenario
} from '$lib/types';
import { getStoredAdminSession, getStoredClientSession } from '$lib/session';

const DEFAULT_API_PORT = env.PUBLIC_API_PORT || '8000';
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(/\/+$/, '');
    }
  }

  const configuredBaseUrl = env.PUBLIC_API_BASE_URL?.trim();
  if (configuredBaseUrl) {
    return configuredBaseUrl.replace(/\/+$/, '');
  }

  if (browser) {
    return `${window.location.protocol}//${window.location.hostname}:${DEFAULT_API_PORT}`;
  }

  return `http://127.0.0.1:${DEFAULT_API_PORT}`;
}

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

function getToken(auth: AuthMode) {
  if (!browser) {
    return null;
  }

  if (auth === 'client') {
    return getStoredClientSession()?.token ?? null;
  }

  if (auth === 'admin') {
    return getStoredAdminSession()?.token ?? null;
  }

  if (auth === 'manager') {
    return getStoredAdminSession()?.token ?? getStoredClientSession()?.token ?? null;
  }

  return null;
}

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, fallback: T, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise<T> {
  try {
    const token = getToken(auth);
    const response = await fetcher(buildApiUrl(path), {
      headers: token ? { Authorization: `Bearer ${token}` } : undefined
    });
    if (!response.ok) {
      if (auth !== 'none') {
        throw new Error(response.statusText || 'Unauthorized');
      }
      return fallback;
    }
    return (await response.json()) as T;
  } catch (error) {
    if (auth !== 'none') {
      throw normalizeRequestError(error);
    }
    return fallback;
  }
}

// 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 token = browser ? getToken(auth) ?? '' : '';
  return `${auth}:${token.slice(-8)}:${path}`;
}

async function cachedFetchJson<T>(
  path: string,
  fallback: T,
  auth: AuthMode = 'none',
  fetcher: ApiFetch = fetch
): Promise<T> {
  // Bypass the cache during SSR (no localStorage, no shared session).
  if (!browser) {
    return fetchJson<T>(path, fallback, 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, fallback, 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 token = getToken(auth);
    const response = await fetcher(buildApiUrl(path), {
      headers: {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
        ...(options.headers ?? {})
      },
      ...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();
    }
    return (await response.json()) as T;
  } catch (error) {
    throw normalizeRequestError(error);
  }
}

async function requestBlob(
  path: string,
  auth: AuthMode = 'none',
  fetcher: ApiFetch = fetch
): Promise<Blob> {
  try {
    const token = getToken(auth);
    const response = await fetcher(buildApiUrl(path), {
      headers: token ? { Authorization: `Bearer ${token}` } : undefined
    });

    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', mockRawMaterials, 'client', fetcher),
  mixes: (fetcher?: ApiFetch) => cachedFetchJson('/api/mixes', mockMixes, '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', mockMixCalculatorOptions, 'client', fetcher),
  mixCalculatorSessions: (fetcher?: ApiFetch) =>
    cachedFetchJson<MixCalculatorSession[]>('/api/mix-calculator', mockMixCalculatorSessions, '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'),
  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', mockProducts, 'client', fetcher),
  productCosts: (fetcher?: ApiFetch) =>
    cachedFetchJson<ProductCostBreakdown[]>('/api/powerbi/product-costs', mockCosts, 'client', fetcher),
  scenarios: (fetcher?: ApiFetch) => cachedFetchJson<Scenario[]>('/api/scenarios', mockScenarios, 'client', fetcher),
  clientAccess: (fetcher?: ApiFetch) => cachedFetchJson<ClientAccessAccount[]>('/api/client-access', mockClientAccess, 'manager', fetcher),
  clientAccessExport: (fetcher?: ApiFetch) =>
    cachedFetchJson<ClientAccessPowerBiExport>('/api/powerbi/client-access', mockClientAccessExport, 'manager', fetcher),
  dataQuality: (fetcher?: ApiFetch) => cachedFetchJson('/api/powerbi/data-quality-issues', [], 'client', fetcher),
  dashboardSummary: (fetcher?: ApiFetch) =>
    cachedFetchJson<DashboardSummary>(
      '/api/dashboard/summary',
      {
        raw_materials: null,
        mixes: null,
        products: null,
        trend_seeds: { raw_material_cost_per_kg: [], mix_cost_per_kg: [], product_finished_delivered: [] }
      },
      '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'),
  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),
  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')
};
