const SHOPWARE_URL = process.env.NEXT_PUBLIC_SHOPWARE_URL || '';
const ACCESS_KEY = process.env.NEXT_PUBLIC_SHOPWARE_ACCESS_KEY || '';
const CONTEXT_TOKEN_COOKIE = 'sw-context-token';

export function getStorefrontUrl(): string {
  return process.env.NEXT_PUBLIC_SHOPWARE_STOREFRONT_URL
    || (typeof window !== 'undefined' ? window.location.origin : '');
}

let contextToken: string | null = null;

function readBrowserCookie(name: string): string | null {
  if (typeof document === 'undefined') return null;
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
  return match ? decodeURIComponent(match[1]) : null;
}

function writeBrowserCookie(name: string, value: string, days = 30) {
  if (typeof document === 'undefined') return;
  const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
  document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
}

/**
 * Reads a cookie / header from the incoming request when running in a Server
 * Component. `next/headers` only exists in RSC and can't be imported from this
 * file (which is also bundled for client components), so it's registered from
 * a server-only init module — see shopware-server-init.ts, imported by the
 * root layout. Returns null on the client and before init has run.
 */
type ServerReader = (name: string) => string | null;
let _serverCookieReader: ServerReader | null = null;
let _serverHeaderReader: ServerReader | null = null;

export function _registerServerReaders(
  cookieReader: ServerReader,
  headerReader: ServerReader
): void {
  _serverCookieReader = cookieReader;
  _serverHeaderReader = headerReader;
}

function readServerCookie(name: string): string | null {
  if (typeof window !== 'undefined') return null;
  return _serverCookieReader ? _serverCookieReader(name) : null;
}

function readServerHeader(name: string): string | null {
  if (typeof window !== 'undefined') return null;
  return _serverHeaderReader ? _serverHeaderReader(name) : null;
}

export function getContextToken(): string | null {
  if (typeof window === 'undefined') {
    // Server-side: cookie set by the browser is the only source of truth
    return readServerCookie(CONTEXT_TOKEN_COOKIE);
  }
  return (
    contextToken ||
    readBrowserCookie(CONTEXT_TOKEN_COOKIE) ||
    localStorage.getItem(CONTEXT_TOKEN_COOKIE)
  );
}

export function setContextToken(token: string) {
  contextToken = token;
  if (typeof window !== 'undefined') {
    localStorage.setItem(CONTEXT_TOKEN_COOKIE, token);
    writeBrowserCookie(CONTEXT_TOKEN_COOKIE, token);
  }
}

interface ApiOptions {
  method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
  body?: Record<string, any>;
  headers?: Record<string, string>;
}

export async function shopwareApi<T = any>(
  endpoint: string,
  options: ApiOptions = {}
): Promise<T> {
  const { method = 'POST', body, headers = {} } = options;

  const requestHeaders: Record<string, string> = {
    'Content-Type': 'application/json',
    Accept: 'application/json',
    'sw-access-key': ACCESS_KEY,
    ...headers,
  };

  const token = getContextToken();
  if (token) {
    requestHeaders['sw-context-token'] = token;
  }

  // Server-side: middleware forwards the active locale's languageId via the
  // `x-sw-language-id` header. Use it as a per-request language override so
  // the very first SSR after navigating to /{locale}/ already returns
  // translated content — no flash of the previous language.
  if (typeof window === 'undefined') {
    const overrideLangId = readServerHeader('x-sw-language-id');
    if (overrideLangId && !requestHeaders['sw-language-id']) {
      requestHeaders['sw-language-id'] = overrideLangId;
    }
  }

  const response = await fetch(`${SHOPWARE_URL}/store-api${endpoint}`, {
    method,
    headers: requestHeaders,
    body: body ? JSON.stringify(body) : undefined,
    cache: 'no-store',
  });

  const newToken = response.headers.get('sw-context-token');
  if (newToken) {
    setContextToken(newToken);
  }

  if (!response.ok) {
    if (response.status === 403 && endpoint.startsWith('/account')) {
      throw new Error('Not logged in');
    }
    const error = await response.json().catch(() => ({}));
    throw new Error(
      error.errors?.[0]?.detail || `API Error: ${response.status}`
    );
  }

  if (response.status === 204 || response.headers.get('content-length') === '0') {
    return null as T;
  }

  return response.json();
}

// ─── Canonical (default-language) URL helpers ────────────────────────────────
// SEO URL slugs must stay the same in every language (no translated product
// or category names in the path). Since the Store API returns data already
// translated to the active locale, we fetch the default-language name/breadcrumb
// separately and slugify *that*. This only runs server-side and only when the
// active locale differs from the sales-channel default.

const DEFAULT_LANG_TTL_MS = 5 * 60 * 1000;
const LANG_CTX_TTL_MS = 30 * 1000;

// salesChannel.languageId is the channel's configured default language. It does
// NOT change with the active context token or the forwarded language header, so
// it can be cached globally.
let defaultLangCache: { id: string | null; at: number } | null = null;

export async function getDefaultLanguageId(): Promise<string | null> {
  const now = Date.now();
  if (defaultLangCache && now - defaultLangCache.at < DEFAULT_LANG_TTL_MS) {
    return defaultLangCache.id;
  }
  try {
    const ctx = await shopwareApi<any>('/context', { method: 'GET' });
    const id = ctx?.salesChannel?.languageId ?? null;
    defaultLangCache = { id, at: now };
    return id;
  } catch {
    return defaultLangCache?.id ?? null;
  }
}

// The active language depends on the request:
//   • URL-prefixed locale (/de-DE/...) → middleware sets x-sw-language-id, which
//     is authoritative for that render (server-side only).
//   • otherwise → the context token's language, read from /context.
// The token-keyed cache is keyed by token only (no header), so it's only used on
// the no-header path and can't go stale across /de-DE vs / requests.
let activeLangCache: { token: string | null; id: string | null; at: number } | null = null;

async function getActiveLanguageId(): Promise<string | null> {
  const header = readServerHeader('x-sw-language-id');
  if (header) return header;
  const token = getContextToken();
  const now = Date.now();
  if (activeLangCache && activeLangCache.token === token && now - activeLangCache.at < LANG_CTX_TTL_MS) {
    return activeLangCache.id;
  }
  try {
    const ctx = await shopwareApi<any>('/context', { method: 'GET' });
    // The active language is the head of context.languageIdChain. Older shapes
    // expose context.languageId; fall back through both.
    const chain = ctx?.context?.languageIdChain;
    const id =
      (Array.isArray(chain) ? chain.find(Boolean) : null) ??
      ctx?.context?.languageId ??
      null;
    activeLangCache = { token, id, at: now };
    return id;
  } catch {
    return activeLangCache?.id ?? null;
  }
}

// Request-level promise cache for canonicalLanguageId to avoid duplicate /context calls
let canonicalLangIdPromise: Promise<string | null> | null = null;

/**
 * Returns the default languageId when canonical enrichment is needed, else null.
 *
 * Runs on both server and client — client nav (CategoryNav) is fetched in the
 * browser, so canonical slugs must be derivable there too.
 *
 * We enrich whenever the active language differs from the default OR the active
 * language can't be determined. Enriching when unknown is safe: on the default
 * language the canonical (default-language) name equals the translated name, so
 * the slug is unchanged — and it guarantees we never fall back to a translated
 * slug just because /context returned an unexpected shape.
 *
 * NOTE: Uses promise-level caching to avoid duplicate /context calls within a
 * single request when multiple functions need the canonical language ID.
 */
async function canonicalLanguageId(): Promise<string | null> {
  // If a promise is already in-flight, return it to deduplicate concurrent calls
  if (canonicalLangIdPromise) {
    return canonicalLangIdPromise;
  }

  // Create and cache the promise
  canonicalLangIdPromise = (async () => {
    try {
      const [def, active] = await Promise.all([
        getDefaultLanguageId(),
        getActiveLanguageId(),
      ]);
      if (!def) return null;
      if (active && active === def) return null;
      return def;
    } finally {
      // Clear the promise after resolution so the next request can make a fresh call
      canonicalLangIdPromise = null;
    }
  })();

  return canonicalLangIdPromise;
}

/**
 * Server-side active locale prefix for building redirect targets / server-
 * rendered hrefs, e.g. "/de-DE" on a German request, "" on the default locale.
 * Reads the locale the middleware forwarded (header) or persisted (cookie).
 * Returns "" on the client — client components use
 * useSalesChannel().localizeHref instead.
 */
export function getActiveLocalePrefix(): string {
  const locale = readServerHeader('x-sw-locale') || readServerCookie('sw-locale');
  return locale ? `/${locale}` : '';
}

interface CanonicalCat {
  name: string;
  breadcrumb?: string[];
}
let canonicalCatMap: Map<string, CanonicalCat> | null = null;
let canonicalCatMapAt = 0;
// Request-level promise cache for getCanonicalCategoryMap to avoid duplicate /navigation calls
let canonicalCatMapPromise: Promise<Map<string, CanonicalCat>> | null = null;

async function getCanonicalCategoryMap(
  defaultLangId: string
): Promise<Map<string, CanonicalCat>> {
  const now = Date.now();
  if (canonicalCatMap && now - canonicalCatMapAt < DEFAULT_LANG_TTL_MS) {
    return canonicalCatMap;
  }

  // If a fetch is already in-flight, return that promise to deduplicate concurrent calls
  if (canonicalCatMapPromise) {
    return canonicalCatMapPromise;
  }

  const map = new Map<string, CanonicalCat>();
  canonicalCatMapPromise = (async () => {
    try {
      const res = await shopwareApi<any>(
        '/navigation/main-navigation/main-navigation',
        {
          headers: { 'sw-language-id': defaultLangId },
          body: { depth: 5 },
        }
      );
      const list: any[] = Array.isArray(res) ? res : res?.elements ?? [];
      const walk = (nodes: any[]) => {
        for (const n of nodes) {
          if (n?.id) map.set(n.id, { name: n.name, breadcrumb: n.breadcrumb });
          if (n?.children?.length) walk(n.children);
        }
      };
      walk(list);
      canonicalCatMap = map;
      canonicalCatMapAt = now;
      return map;
    } catch {
      return canonicalCatMap ?? map;
    } finally {
      // Clear the promise after resolution so the next request can make a fresh call
      canonicalCatMapPromise = null;
    }
  })();

  return canonicalCatMapPromise;
}

function attachCanonicalToCategories(
  nodes: ShopwareCategory[],
  map: Map<string, CanonicalCat>
): void {
  for (const n of nodes) {
    const c = map.get(n.id);
    if (c) {
      n.canonicalName = c.name;
      n.canonicalBreadcrumb = c.breadcrumb;
    }
    if (n.children?.length) attachCanonicalToCategories(n.children, map);
  }
}

async function enrichProductsCanonical(products: ShopwareProduct[]): Promise<void> {
  const def = await canonicalLanguageId();
  if (!def || !products.length) return;
  const ids = products.map((p) => p.id).filter(Boolean);
  if (!ids.length) return;
  try {
    const res = await shopwareApi<any>('/product', {
      headers: { 'sw-language-id': def },
      body: {
        filter: [{ type: 'equalsAny', field: 'id', value: ids }],
        includes: { product: ['id', 'name', 'translated'] },
        limit: ids.length,
      },
    });
    const byId = new Map<string, string>();
    for (const e of res?.elements ?? []) {
      const name = e?.translated?.name || e?.name;
      if (e?.id && name) byId.set(e.id, name);
    }
    for (const p of products) {
      const name = byId.get(p.id);
      if (name) p.canonicalName = name;
    }
  } catch {
    // canonical enrichment is best-effort; fall back to translated slug
  }
}

export interface ShopwareProduct {
  id: string;
  name: string;
  description: string | null;
  productNumber: string;
  price: Array<{
    currencyId: string;
    gross: number;
    net: number;
    linked: boolean;
  }>;
  cover?: {
    media?: {
      url: string;
      alt: string | null;
      title: string | null;
      thumbnails?: Array<{
        url: string;
        width: number;
        height: number;
      }>;
    };
  };
  media?: Array<{
    id: string;
    position: number;
    media: {
      url: string;
      alt: string | null;
      title: string | null;
      thumbnails?: Array<{ url: string; width: number; height: number }>;
    };
  }>;
  calculatedPrice?: {
    unitPrice: number;
    totalPrice: number;
    listPrice?: {
      price: number;
    } | null;
  };
  calculatedPrices?: Array<{
    unitPrice: number;
    quantity: number;
    totalPrice: number;
    listPrice?: {
      price: number;
    } | null;
  }>;
  translated?: {
    name: string;
    description: string;
  };
  // Default-language name, attached server-side when browsing a non-default
  // locale, so SEO URL slugs stay canonical (never translated). See
  // getProductUrl / enrichProductsCanonical.
  canonicalName?: string;
  seoUrls?: Array<{
    seoPathInfo: string;
  }>;
  parentId: string | null;
  childCount: number;
  optionIds?: string[];
  ratingAverage: number | null;
  availableStock: number;
  stock: number;
  isCloseout: boolean;
  shippingFree: boolean;
  deliveryTime?: {
    name: string;
    min: number;
    max: number;
    unit: string;
    translated?: { name: string };
  } | null;
  manufacturer?: {
    name: string;
    id: string;
  };
  categories?: Array<{
    id: string;
    name: string;
    level?: number;
    breadcrumb?: string[];
    seoUrls?: Array<{ seoPathInfo: string }>;
  }>;
  tax?: {
    taxRate: number;
    name: string;
  };
  // Property options attached to this product (each carries its group).
  properties?: Array<{
    id: string;
    name: string;
    position?: number;
    translated?: { name?: string };
    group?: {
      id: string;
      name: string;
      position?: number;
      translated?: { name?: string };
    };
  }>;
  // Pre-grouped properties computed by Shopware's product detail route.
  sortedProperties?: Array<{
    id: string;
    name: string;
    translated?: { name?: string };
    options?: Array<{
      id: string;
      name: string;
      translated?: { name?: string };
    }>;
  }>;
}

/**
 * Builds the grouped property list for a product detail page, mirroring
 * Shopware's `product.sortedProperties`. Prefers the server-computed
 * `sortedProperties`; otherwise groups the raw `properties` association.
 */
export function getProductProperties(
  product: ShopwareProduct
): Array<{ id: string; name: string; options: Array<{ id: string; name: string }> }> {
  const sorted = (product as any).sortedProperties;
  const sortedArr = Array.isArray(sorted) ? sorted : sorted?.elements;
  if (Array.isArray(sortedArr) && sortedArr.length > 0) {
    return sortedArr
      .map((group: any) => ({
        id: group.id,
        name: group.translated?.name || group.name || '',
        options: (group.options ?? []).map((o: any) => ({
          id: o.id,
          name: o.translated?.name || o.name || '',
        })),
      }))
      .filter((g: any) => g.options.length > 0);
  }

  const props = (product as any).properties;
  const propsArr = Array.isArray(props) ? props : props?.elements;
  if (!Array.isArray(propsArr) || propsArr.length === 0) return [];

  const groupMap = new Map<
    string,
    { id: string; name: string; position: number; options: Array<{ id: string; name: string }> }
  >();
  for (const opt of propsArr) {
    const g = opt.group;
    if (!g) continue;
    if (!groupMap.has(g.id)) {
      groupMap.set(g.id, {
        id: g.id,
        name: g.translated?.name || g.name || '',
        position: g.position ?? 0,
        options: [],
      });
    }
    groupMap.get(g.id)!.options.push({
      id: opt.id,
      name: opt.translated?.name || opt.name || '',
    });
  }
  return Array.from(groupMap.values())
    .sort((a, b) => a.position - b.position)
    .map(({ id, name, options }) => ({ id, name, options }));
}

export interface ManufacturerAgg {
  id: string;
  name: string;
  translated?: { name?: string };
}

export interface PropertyOptionAgg {
  id: string;
  name: string;
  translated?: { name?: string };
  group?: { id: string; name: string; translated?: { name?: string } };
}

export interface ProductListAggregations {
  manufacturer?: { entities: ManufacturerAgg[] };
  properties?: { entities: PropertyOptionAgg[] };
  price?: { min: string | number; max: string | number };
  rating?: { max: number };
}

export interface ProductListResponse {
  elements: ShopwareProduct[];
  total: number;
  aggregations?: ProductListAggregations;
}

export async function getProducts(params?: {
  limit?: number;
  page?: number;
  sort?: string;
  filter?: any[];
  search?: string;
  categoryId?: string;
  manufacturer?: string[];
  properties?: string[];
  minPrice?: number;
  maxPrice?: number;
  rating?: number;
}): Promise<ProductListResponse> {
  const {
    limit = 12,
    page = 1,
    sort,
    filter,
    search,
    categoryId,
    manufacturer,
    properties,
    minPrice,
    maxPrice,
    rating,
  } = params || {};

  let result: ProductListResponse;

  if (search) {
    result = await shopwareApi('/search', {
      headers: { 'sw-include-seo-urls': 'true' },
      body: {
        search,
        limit,
        p: page,
        ...(sort && {
          order: sort,
        }),
      },
    });
    await enrichProductsCanonical(result.elements ?? []);
    return result;
  }

  if (categoryId) {
    const postFilters: any[] = [...(filter ?? [])];
    if (manufacturer?.length) {
      postFilters.push({ type: 'equalsAny', field: 'manufacturerId', value: manufacturer });
    }
    if (properties?.length) {
      postFilters.push({ type: 'equalsAny', field: 'properties.id', value: properties });
    }
    if (minPrice != null || maxPrice != null) {
      postFilters.push({
        type: 'range',
        field: 'cheapestPrice',
        parameters: {
          ...(minPrice != null && { gte: minPrice }),
          ...(maxPrice != null && { lte: maxPrice }),
        },
      });
    }
    if (rating != null) {
      postFilters.push({ type: 'range', field: 'ratingAverage', parameters: { gte: rating } });
    }

    result = await shopwareApi(`/product-listing/${categoryId}`, {
      headers: { 'sw-include-seo-urls': 'true' },
      body: {
        limit,
        p: page,
        ...(sort && { order: sort }),
        ...(postFilters.length && { filter: postFilters }),
        associations: {
          cover: { associations: { media: {} } },
          manufacturer: {},
          categories: {},
          seoUrls: {},
        },
      },
    });
    await enrichProductsCanonical(result.elements ?? []);
    return result;
  }

  result = await shopwareApi('/product', {
    headers: { 'sw-include-seo-urls': 'true' },
    body: {
      limit,
      p: page,
      ...(sort && { order: sort }),
      ...(filter && { filter }),
      associations: {
        cover: {
          associations: {
            media: {},
          },
        },
        manufacturer: {},
        seoUrls: {},
      },
    },
  });
  await enrichProductsCanonical(result.elements ?? []);
  return result;
}

export async function getProduct(productId: string): Promise<{
  product: ShopwareProduct;
  configurator?: any[];
}> {
  const result = await shopwareApi<{ product: ShopwareProduct; configurator?: any[] }>(`/product/${productId}`, {
    body: {
      associations: {
        media: {
          associations: { media: {} },
          sort: [{ field: 'position', order: 'ASC' }],
        },
        manufacturer: { associations: { media: {} } },
        categories: { associations: { seoUrls: {} } },
        deliveryTime: {},
        seoUrls: {},
        properties: { associations: { group: {} } },
        cmsPage: {
          associations: {
            sections: {
              associations: {
                blocks: {
                  associations: { slots: {} },
                },
              },
            },
          },
        },
      },
      includes: {
        category: ['id', 'name', 'level', 'breadcrumb', 'translated', 'seoUrls'],
        seo_url: ['seoPathInfo'],
      },
    },
    headers: { 'sw-include-seo-urls': 'true' },
  });
  if (result?.product) {
    await enrichProductsCanonical([result.product]);
  }
  return result;
}

/**
 * Resolves "mapped" CMS slot values using the real product data.
 * Shopware fills these server-side; in headless we do it here.
 */
export function resolveProductCmsSlots(
  sections: any[],
  product: ShopwareProduct,
  extras?: { reviews?: ProductReviewsResponse | null; crossSellings?: CrossSelling[] }
): any[] {
  const name = product.translated?.name || product.name || '';
  const description = product.translated?.description || product.description || '';

  const MAP: Record<string, string> = {
    'product.name': name,
    'product.description': description,
    'product.manufacturer.name': product.manufacturer?.name || '',
  };

  return sections.map((section: any) => ({
    ...section,
    blocks: (section.blocks?.elements ?? section.blocks ?? []).map((block: any) => ({
      ...block,
      slots: (block.slots?.elements ?? block.slots ?? []).map((slot: any) => {
        const slotType = (slot.type ?? '').replace(/^cms[-_]/, '').replace(/_/g, '-');
        // Resolve mapped content slots (e.g. product.name → actual name)
        const source = slot.config?.content?.source;
        const mappingKey = slot.config?.content?.value;
        if (source === 'mapped' && mappingKey && MAP[mappingKey] !== undefined) {
          return { ...slot, data: { ...slot.data, content: MAP[mappingKey] } };
        }
        // Inject manufacturer data into manufacturer-logo slots
        if (slotType === 'product-manufacturer-logo' || slotType === 'manufacturer-logo' || slotType.includes('manufacturer')) {
          return { ...slot, data: { ...slot.data, manufacturer: product.manufacturer ?? null } };
        }
        // Inject the resolved product into the buy-box slot
        if (slotType === 'buy-box') {
          return { ...slot, data: { ...slot.data, product } };
        }
        // Inject product + reviews into the description/reviews slot
        if (slotType === 'product-description-reviews') {
          return {
            ...slot,
            data: { ...slot.data, product, reviews: extras?.reviews ?? slot.data?.reviews ?? null },
          };
        }
        // Inject product + cross-selling groups into the cross-selling slot
        if (slotType === 'cross-selling') {
          return {
            ...slot,
            data: { ...slot.data, product, crossSellings: extras?.crossSellings ?? slot.data?.crossSellings ?? [] },
          };
        }
        return slot;
      }),
    })),
  }));
}

export interface PropertyGroupOption {
  id: string;
  name: string;
  colorHexCode: string | null;
  position: number;
  translated?: { name: string };
  media?: {
    url: string;
    alt: string | null;
    thumbnails?: Array<{ url: string; width: number; height: number }>;
  } | null;
}

export interface PropertyGroup {
  id: string;
  name: string;
  displayType: string; // 'text', 'color', 'image'
  sortingType: string;
  translated?: { name: string };
  options?: PropertyGroupOption[];
}

interface RawConfiguratorOption extends PropertyGroupOption {
  group?: {
    id: string;
    name: string;
    displayType: string;
    sortingType: string;
    translated?: { name: string };
  };
}

export function groupRawOptions(rawOptions: RawConfiguratorOption[]): { elements: PropertyGroup[] } {
  if (rawOptions.length > 0 && Array.isArray((rawOptions[0] as any).options)) {
    return { elements: rawOptions as unknown as PropertyGroup[] };
  }

  const groupMap = new Map<string, PropertyGroup & { options: PropertyGroupOption[] }>();
  for (const opt of rawOptions) {
    const g = opt.group;
    if (!g) continue;
    if (!groupMap.has(g.id)) {
      groupMap.set(g.id, {
        id: g.id,
        name: g.name,
        displayType: g.displayType || 'text',
        sortingType: g.sortingType || 'alphanumeric',
        translated: g.translated,
        options: [],
      });
    }
    groupMap.get(g.id)!.options.push({
      id: opt.id,
      name: opt.name,
      colorHexCode: opt.colorHexCode ?? null,
      position: opt.position ?? 0,
      translated: opt.translated,
      media: opt.media ?? null,
    });
  }
  return { elements: Array.from(groupMap.values()) };
}

export async function getProductConfigurator(
  productId: string
): Promise<{ elements: PropertyGroup[] }> {
  try {
    const response = await shopwareApi<any>(
      `/product/${productId}/configurator`,
      { body: {} }
    );
    const rawOptions: RawConfiguratorOption[] =
      response?.elements ??
      response?.configurator ??
      (Array.isArray(response) ? response : []);

    if (rawOptions.length > 0) {
      return groupRawOptions(rawOptions);
    }
  } catch {
    // fall through to strategy 2
  }

  const resp = await shopwareApi<any>(`/product/${productId}`, {
    body: {
      associations: {
        configuratorSettings: {
          associations: {
            option: {
              associations: {
                group: {},
                media: {},
              },
            },
            media: {},
          },
        },
      },
    },
  });

  const settings: any[] = resp?.product?.configuratorSettings ?? [];
  const rawOptions: RawConfiguratorOption[] = settings
    .map((s: any) => ({
      id: s.option?.id,
      name: s.option?.name ?? '',
      colorHexCode: s.option?.colorHexCode ?? null,
      position: s.option?.position ?? 0,
      translated: s.option?.translated,
      media: s.media ?? s.option?.media ?? null,
      group: s.option?.group,
    }))
    .filter((o: any) => o.id && o.group);

  return groupRawOptions(rawOptions);
}

export async function findProductVariant(
  parentId: string,
  options: Record<string, string>,
  switchedGroup: string
): Promise<{ variantId: string }> {
  return shopwareApi(`/product/${parentId}/find-variant`, {
    body: { switchedGroup, options },
  });
}

export interface ShopwareCategory {
  id: string;
  name: string;
  parentId: string | null;
  childCount: number;
  level: number;
  children?: ShopwareCategory[];
  translated?: {
    name: string;
  };
  type: string;
  description: string | null;
  media?: {
    url: string;
  };
  breadcrumb?: string[];
  // Default-language breadcrumb/name, attached server-side on non-default
  // locales so category URL slugs stay canonical. See getCategoryUrl.
  canonicalBreadcrumb?: string[];
  canonicalName?: string;
  seoUrls?: Array<{
    seoPathInfo: string;
  }>;
}

// Client-side cache for navigation trees. Several components (CategoryNav,
// MobileCategoryNav, Footer) request the same roots on every page, which
// otherwise fan out into many identical /navigation requests on the critical
// path. Keyed by rootId+depth; only used in the browser so server renders never
// share state across requests (which would leak language). A full page reload —
// including the one triggered on language switch — clears it.
const navCache = new Map<string, Promise<ShopwareCategory[]>>();

export async function getNavigation(
  rootId: string = 'main-navigation',
  depth: number = 3
): Promise<ShopwareCategory[]> {
  if (typeof window !== 'undefined') {
    const key = `${rootId}:${depth}`;
    const cached = navCache.get(key);
    if (cached) return cached;
    const promise = fetchNavigation(rootId, depth).catch((e) => {
      navCache.delete(key); // don't cache failures
      throw e;
    });
    navCache.set(key, promise);
    return promise;
  }
  return fetchNavigation(rootId, depth);
}

async function fetchNavigation(
  rootId: string,
  depth: number
): Promise<ShopwareCategory[]> {
  // Do NOT add `includes` here — it breaks nested children hydration in some Shopware versions.
  const res = await shopwareApi(`/navigation/${rootId}/${rootId}`, {
    headers: { 'sw-include-seo-urls': 'true' },
    body: {
      depth,
      associations: { seoUrls: {} },
    },
  });
  const elements: ShopwareCategory[] = Array.isArray(res)
    ? res
    : Array.isArray(res?.elements)
      ? res.elements
      : [];

  // Attach default-language breadcrumb so hrefs stay canonical while the
  // visible labels remain translated.
  const def = await canonicalLanguageId();
  if (def && elements.length) {
    const map = await getCanonicalCategoryMap(def);
    attachCanonicalToCategories(elements, map);
  }

  return elements;
}

export async function getCategory(categoryId: string): Promise<ShopwareCategory> {
  return shopwareApi(`/category/${categoryId}`, {
    body: {},
  });
}

export interface CartItem {
  id: string;
  referencedId: string;
  label: string;
  quantity: number;
  type: string;
  price: {
    unitPrice: number;
    totalPrice: number;
    quantity: number;
  };
  cover?: {
    url: string;
  };
  payload?: {
    options?: Array<{ group: string; option: string }>;
    [key: string]: any;
  };
}

export interface Cart {
  token: string;
  lineItems: CartItem[];
  price: {
    totalPrice: number;
    netPrice: number;
    positionPrice: number;
    taxStatus: string;
    calculatedTaxes?: Array<{ taxRate: number; tax: number; price: number }>;
  };
  deliveries: any[];
  errors: Record<string, any>;
}

export async function getCart(): Promise<Cart> {
  return shopwareApi('/checkout/cart', { method: 'GET' });
}

export async function addPromotion(code: string): Promise<Cart> {
  return shopwareApi('/checkout/cart/line-item', {
    body: {
      items: [
        {
          type: 'promotion',
          referencedId: code,
        },
      ],
    },
  });
}

export async function addToCart(
  productId: string,
  quantity: number = 1
): Promise<Cart> {
  return shopwareApi('/checkout/cart/line-item', {
    body: {
      items: [
        {
          id: productId,
          referencedId: productId,
          type: 'product',
          quantity,
        },
      ],
    },
  });
}

/**
 * Add several products to the cart in a single request. Used by "Repeat order"
 * (reorder) to re-add every product line item from a past order.
 */
export async function addProductsToCart(
  items: Array<{ id: string; quantity: number }>
): Promise<Cart> {
  return shopwareApi('/checkout/cart/line-item', {
    body: {
      items: items.map((i) => ({
        id: i.id,
        referencedId: i.id,
        type: 'product',
        quantity: i.quantity,
      })),
    },
  });
}

export async function updateCartItem(
  lineItemId: string,
  quantity: number
): Promise<Cart> {
  return shopwareApi('/checkout/cart/line-item', {
    method: 'PATCH',
    body: {
      items: [
        {
          id: lineItemId,
          quantity,
        },
      ],
    },
  });
}

export async function removeFromCart(lineItemId: string): Promise<Cart> {
  return shopwareApi(`/checkout/cart/line-item`, {
    method: 'DELETE',
    body: {
      ids: [lineItemId],
    },
  });
}

export interface CustomerAddress {
  id: string;
  salutationId?: string;
  salutation?: { displayName: string; translated?: { displayName: string } };
  firstName: string;
  lastName: string;
  company?: string;
  department?: string;
  street: string;
  additionalAddressLine1?: string;
  zipcode: string;
  city: string;
  countryId: string;
  country?: { id: string; name: string; translated?: { name: string } };
}

export interface Customer {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  salutationId: string;
  customerNumber: string;
  guest: boolean;
  group: { name: string };
  defaultBillingAddressId?: string;
  defaultShippingAddressId?: string;
  defaultBillingAddress?: CustomerAddress;
  defaultShippingAddress?: CustomerAddress;
}

export async function login(
  email: string,
  password: string
): Promise<{ contextToken: string }> {
  const result = await shopwareApi('/account/login', {
    body: { email, password },
  });
  return result;
}

export async function register(data: {
  email: string;
  password: string;
  firstName: string;
  lastName: string;
  salutationId: string;
  storefrontUrl: string;
  // 'business' enables commercial-account fields (company + VAT id)
  accountType?: 'private' | 'business';
  vatIds?: string[];
  billingAddress: {
    street: string;
    zipcode: string;
    city: string;
    countryId: string;
    company?: string;
    department?: string;
  };
}): Promise<Customer> {
  return shopwareApi('/account/register', { body: data });
}

export async function registerGuest(data: {
  email: string;
  firstName: string;
  lastName: string;
  salutationId: string;
  storefrontUrl: string;
  billingAddress: {
    street: string;
    zipcode: string;
    city: string;
    countryId: string;
  };
}): Promise<Customer> {
  return shopwareApi('/account/register', { body: { ...data, guest: true } });
}

export async function logout(): Promise<void> {
  await shopwareApi('/account/logout', { method: 'POST' });
  if (typeof window !== 'undefined') {
    localStorage.removeItem('sw-context-token');
  }
  contextToken = null;
}

export async function getCustomerProfile(): Promise<Customer> {
  return shopwareApi('/account/customer', {
    body: {
      associations: {
        defaultBillingAddress: {
          associations: { country: {}, salutation: {} },
        },
        defaultShippingAddress: {
          associations: { country: {}, salutation: {} },
        },
        salutation: {},
      },
    },
  });
}

export interface CmsPage {
  id: string;
  name: string;
  type: string;
  sections: CmsSection[];
}

export interface CmsSection {
  id: string;
  type: string; // 'default', 'sidebar'
  blocks: CmsBlock[];
}

export interface CmsBlock {
  id: string;
  type: string; // 'image', 'text', 'product-listing', 'image-text', etc.
  slots: CmsSlot[];
}

export interface CmsSlot {
  id: string;
  type: string;
  slot: string;
  data: any;
  config: any;
}

export async function getCmsPage(pageId: string): Promise<CmsPage> {
  return shopwareApi(`/cms/${pageId}`);
}

export async function getCategoryWithCmsPage(categoryId: string): Promise<any> {
  const category = await shopwareApi<any>(`/category/${categoryId}`, {
    headers: { 'sw-include-seo-urls': 'true' },
    body: {
      associations: {
        media: {},
        cmsPage: {},
        seoUrls: {},
      },
    },
  });

  // Attach the default-language breadcrumb so the canonical redirect builds a
  // language-independent slug (e.g. /Clothing, never /Bekleidung).
  const def = await canonicalLanguageId();
  if (def) {
    const map = await getCanonicalCategoryMap(def);
    const target = category?.category ?? category;
    if (target?.id) {
      const c = map.get(target.id);
      if (c) {
        target.canonicalName = c.name;
        target.canonicalBreadcrumb = c.breadcrumb;
      }
    }
  }

  const cmsPageId =
    category?.cmsPageId ??
    category?.category?.cmsPageId ??
    category?.cmsPage?.id ??
    category?.category?.cmsPage?.id ??
    null;

  if (!cmsPageId) {
    return category;
  }

  // Check if embedded CMS page is already complete (has sections)
  const embeddedCms = category?.cmsPage ?? category?.category?.cmsPage;
  if (embeddedCms?.sections?.length) {
    // Embedded CMS page is sufficient, no need to fetch separately
    return category;
  }

  // Only fetch the full CMS page if the embedded version is incomplete
  let resolvedCmsPage: any = null;
  try {
    resolvedCmsPage = await shopwareApi(`/cms/${cmsPageId}`);
  } catch {
    // fall back to category-embedded cmsPage
  }

  if (resolvedCmsPage) {
    return { ...category, cmsPage: resolvedCmsPage };
  }

  return category;
}

export interface ShippingMethod {
  id: string;
  name: string;
  description: string | null;
  deliveryTime?: {
    name: string;
    min: number;
    max: number;
    unit: string;
  };
  prices: Array<{
    currencyPrice: Array<{
      currencyId: string;
      gross: number;
      net: number;
    }>;
  }>;
  translated?: {
    name: string;
    description: string;
  };
}

// Payment methods (by technical shortName) hidden from the storefront for now.
// Direct Debit needs a SEPA/bank-details form that isn't implemented yet.
// Empty this array to re-enable a method everywhere it would surface.
export const EXCLUDED_PAYMENT_SHORT_NAMES = ['debit_payment'];

/** True if a payment method is currently hidden from the storefront. */
export function isPaymentMethodExcluded(method: { shortName?: string }): boolean {
  return EXCLUDED_PAYMENT_SHORT_NAMES.includes(method.shortName ?? '');
}

export interface PaymentMethod {
  id: string;
  name: string;
  /** Technical key, e.g. "invoice_payment", "debit_payment". */
  shortName?: string;
  description: string | null;
  distinguishableName: string;
  translated?: {
    name: string;
    description: string;
    distinguishableName: string;
  };
  synchronous: boolean;
  asynchronous: boolean;
  afterOrderEnabled: boolean;
  mediaId?: string;
  media?: {
    url: string;
  };
}

export interface Country {
  id: string;
  name: string;
  iso: string;
  iso3: string;
  active: boolean;
  translated?: {
    name: string;
  };
}

export interface Salutation {
  id: string;
  salutationKey: string;
  displayName: string;
  translated?: {
    displayName: string;
  };
}

export interface Order {
  id: string;
  orderNumber: string;
  orderDateTime: string;
  amountTotal: number;
  amountNet: number;
  shippingTotal?: number;
  customerComment?: string | null;
  stateMachineState: {
    name: string;
    translated: { name: string };
  };
  lineItems: Array<{
    id: string;
    label: string;
    quantity: number;
    unitPrice: number;
    totalPrice: number;
    productId?: string;
    cover?: { url: string; alt?: string | null };
    type?: string;
    payload?: {
      options?: Array<{ group: string; option: string }>;
      [key: string]: any;
    };
  }>;
  deliveries: Array<{
    shippingMethod: {
      name: string;
      translated?: { name: string };
    };
    shippingOrderAddress?: {
      firstName: string;
      lastName: string;
      street: string;
      zipcode: string;
      city: string;
      country?: { name: string };
    };
    shippingCosts?: { totalPrice: number };
    stateMachineState?: {
      name: string;
      translated?: { name: string };
    };
  }>;
  transactions: Array<{
    id?: string;
    createdAt?: string;
    paymentMethod: {
      id?: string;
      name: string;
      shortName?: string;
      translated?: { name: string };
    };
    stateMachineState?: {
      name: string;
      translated?: { name: string };
    };
  }>;
  price?: {
    totalPrice: number;
    positionPrice: number;
    taxStatus: string;
  };
}

/**
 * The "active" order transaction. Changing the payment method on a placed order
 * appends a NEW transaction (Shopware never edits the old one), so `transactions[0]`
 * can be stale. The most recently created transaction reflects the current method.
 */
export function getActiveTransaction(order: Order): Order['transactions'][number] | undefined {
  const txns = order.transactions || [];
  if (txns.length <= 1) return txns[0];
  return [...txns].sort((a, b) => {
    const ta = a.createdAt ? Date.parse(a.createdAt) : 0;
    const tb = b.createdAt ? Date.parse(b.createdAt) : 0;
    return tb - ta;
  })[0];
}

/** Display name of the order's current payment method (latest transaction). */
export function getOrderPaymentMethodName(order: Order): string | undefined {
  const pm = getActiveTransaction(order)?.paymentMethod;
  return pm?.translated?.name || pm?.name;
}

export async function getShippingMethods(): Promise<{ elements: ShippingMethod[] }> {
  return shopwareApi('/shipping-method', {
    body: {},
  });
}

export async function getPaymentMethods(): Promise<{ elements: PaymentMethod[] }> {
  return shopwareApi('/payment-method', {
    body: {},
  });
}

export async function getCountries(): Promise<{ elements: Country[] }> {
  return shopwareApi('/country', {
    body: {
      limit: 100,
      sort: [{ field: 'name', order: 'ASC' }],
      filter: [{ type: 'equals', field: 'active', value: true }],
    },
  });
}

export async function getSalutations(): Promise<{ elements: Salutation[] }> {
  return shopwareApi('/salutation');
}

export async function updateContext(data: {
  shippingMethodId?: string;
  paymentMethodId?: string;
  languageId?: string;
  currencyId?: string;
  countryId?: string;
}): Promise<any> {
  return shopwareApi('/context', {
    method: 'PATCH',
    body: data,
  });
}

export async function getContext(): Promise<any> {
  return shopwareApi('/context', { method: 'GET' });
}

/**
 * Force the persisted cart to recalculate its deliveries (shipping costs),
 * surcharges, taxes and totals.
 *
 * The Store API's `GET /checkout/cart` returns the *cached* cart and does NOT
 * recalculate after a pure context change (e.g. switching the shipping or
 * payment method). So a paid shipping method's price would never appear — the
 * shipping cost stays at whatever it was when the items were first added
 * (usually the free default method). Re-submitting the current product line
 * items via PATCH triggers a full server-side recalculation and returns the
 * fresh cart, mirroring what Shopware's own storefront does on method change.
 */
export async function recalculateCart(): Promise<Cart> {
  const cart = await getCart();
  const items = (cart.lineItems || []).filter(
    (i) => i.type === 'product' && i.quantity > 0
  );
  if (items.length === 0) return cart;
  try {
    return await shopwareApi('/checkout/cart/line-item', {
      method: 'PATCH',
      body: { items: items.map((i) => ({ id: i.id, quantity: i.quantity })) },
    });
  } catch {
    // Recalculation is best-effort — fall back to the cached cart
    return cart;
  }
}

/**
 * Switch the active shipping method and return the recalculated cart (with the
 * new shipping cost reflected in deliveries + totals).
 */
export async function setShippingMethod(shippingMethodId: string): Promise<Cart> {
  await updateContext({ shippingMethodId });
  return recalculateCart();
}

/**
 * Switch the active payment method and return the recalculated cart (payment
 * surcharges, if any, are reflected in the totals).
 */
export async function setPaymentMethod(paymentMethodId: string): Promise<Cart> {
  await updateContext({ paymentMethodId });
  return recalculateCart();
}

export async function placeOrder(data?: {
  customerComment?: string;
  affiliateCode?: string;
  campaignCode?: string;
}): Promise<Order> {
  return shopwareApi('/checkout/order', {
    body: data ?? {},
  });
}

export async function handlePayment(
  orderId: string,
  finishUrl: string,
  errorUrl: string
): Promise<{ redirectUrl?: string }> {
  return shopwareApi('/handle-payment', {
    body: {
      orderId,
      finishUrl,
      errorUrl,
    },
  });
}

/**
 * Change the payment method on an existing (placed) order. Mirrors Shopware's
 * account "edit order → change payment" flow. After this, call handlePayment
 * to (re)initiate the transaction — async methods return a redirectUrl.
 */
export async function changeOrderPaymentMethod(
  orderId: string,
  paymentMethodId: string
): Promise<void> {
  await shopwareApi('/order/payment', {
    body: { orderId, paymentMethodId },
  });
}

export async function getOrders(params?: {
  limit?: number;
  page?: number;
}): Promise<{ orders: { elements: Order[]; total: number } }> {
  const { limit = 10, page = 1 } = params || {};
  return shopwareApi('/order', {
    method: 'POST',
    body: {
      limit,
      p: page,
      associations: {
        lineItems: {},
        deliveries: { associations: { shippingMethod: {} } },
        transactions: {
          associations: { paymentMethod: {} },
          sort: [{ field: 'createdAt', order: 'DESC' }],
        },
      },
    },
  });
}

export async function getOrder(orderId: string): Promise<Order | null> {
  const result = await shopwareApi('/order', {
    method: 'POST',
    body: {
      filter: [{ type: 'equals', field: 'id', value: orderId }],
      associations: {
        lineItems: {
          associations: {
            cover: {},
          },
        },
        deliveries: {
          associations: {
            shippingMethod: {},
            shippingOrderAddress: { associations: { country: {} } },
          },
        },
        transactions: {
          associations: { paymentMethod: {} },
          sort: [{ field: 'createdAt', order: 'DESC' }],
        },
      },
    },
  });
  return result?.orders?.elements?.[0] ?? null;
}

export async function updateProfile(data: {
  firstName: string;
  lastName: string;
  salutationId: string;
}): Promise<void> {
  await shopwareApi('/account/change-profile', { body: data });
}

export async function changePassword(data: {
  password: string;
  newPassword: string;
  newPasswordConfirm: string;
}): Promise<void> {
  await shopwareApi('/account/change-password', { body: data });
}

export async function recoverPassword(data: {
  email: string;
  storefrontUrl: string;
}): Promise<void> {
  await shopwareApi('/account/recovery-password', { body: data });
}

export async function resetPassword(data: {
  hash: string;
  newPassword: string;
  newPasswordConfirm: string;
}): Promise<void> {
  await shopwareApi('/account/recovery-password-confirm', { body: data });
}

export async function changeEmail(data: {
  email: string;
  emailConfirmation: string;
  password: string;
}): Promise<void> {
  await shopwareApi('/account/change-email', { body: data });
}

export async function getCustomerAddresses(): Promise<{ elements: CustomerAddress[] }> {
  const res = await shopwareApi<any>('/account/list-address', {
    method: 'POST',
    body: {
      associations: { country: {}, salutation: {} },
    },
  });
  if (Array.isArray(res)) return { elements: res as CustomerAddress[] };
  if (Array.isArray(res?.elements)) return { elements: res.elements as CustomerAddress[] };
  return { elements: [] };
}

export async function createAddress(
  data: Omit<CustomerAddress, 'id' | 'salutation' | 'country'>
): Promise<CustomerAddress> {
  return shopwareApi('/account/address', { body: data });
}

export async function updateAddress(
  addressId: string,
  data: Omit<CustomerAddress, 'id' | 'salutation' | 'country'>
): Promise<CustomerAddress> {
  return shopwareApi(`/account/address/${addressId}`, {
    method: 'PATCH',
    body: data,
  });
}

export async function deleteAddress(addressId: string): Promise<void> {
  await shopwareApi(`/account/address/${addressId}`, { method: 'DELETE' });
}

export async function setDefaultBillingAddress(addressId: string): Promise<void> {
  await shopwareApi(`/account/address/default-billing/${addressId}`, { method: 'PATCH' });
}

export async function setDefaultShippingAddress(addressId: string): Promise<void> {
  await shopwareApi(`/account/address/default-shipping/${addressId}`, { method: 'PATCH' });
}

export async function getWishlist(): Promise<{ products: { elements: ShopwareProduct[]; total: number } }> {
  return shopwareApi('/customer/wishlist', { method: 'GET' });
}

export async function addToWishlist(productId: string): Promise<void> {
  await shopwareApi(`/customer/wishlist/add/${productId}`, { method: 'POST' });
}

export async function removeFromWishlist(productId: string): Promise<void> {
  await shopwareApi(`/customer/wishlist/delete/${productId}`, { method: 'DELETE' });
}

export async function mergeWishlist(productIds: string[]): Promise<void> {
  await shopwareApi('/customer/wishlist/merge', {
    method: 'POST',
    body: { productIds },
  });
}

export async function getSystemConfig(
  domain: string
): Promise<Record<string, any>> {
  return shopwareApi(`/system-config?domain=${domain}`, { method: 'GET' });
}

export async function searchSuggest(
  query: string
): Promise<ProductListResponse> {
  return shopwareApi('/search-suggest', {
    body: { search: query },
  });
}

export interface ContactFormData {
  salutationId: string;
  firstName: string;
  lastName: string;
  email: string;
  subject: string;
  comment: string;
  phone?: string;
  navigationId?: string;
}

export async function submitContactForm(data: ContactFormData): Promise<void> {
  await shopwareApi('/contact-form', { body: data });
}

export async function subscribeNewsletter(
  email: string,
  option: 'subscribe' | 'unsubscribe' | 'direct' = 'subscribe',
  extra?: { salutationId?: string; firstName?: string; lastName?: string }
): Promise<void> {
  if (option === 'unsubscribe') {
    await shopwareApi('/newsletter/unsubscribe', { body: { email } });
    return;
  }
  await shopwareApi('/newsletter/subscribe', {
    body: {
      email,
      option,
      storefrontUrl: getStorefrontUrl(),
      ...extra,
    },
  });
}

// ─── Cross-Selling ──────────────────────────────────────────────────────────

export interface CrossSelling {
  crossSelling: {
    id: string;
    name: string;
    position: number;
    type: string;
    active: boolean;
    translated?: { name: string };
  };
  products: ShopwareProduct[];
  total: number;
}

export async function getProductCrossSelling(
  productId: string
): Promise<CrossSelling[]> {
  const res = await shopwareApi<any>(`/product/${productId}/cross-selling`, {
    body: {},
  });
  // Shopware returns an array of cross-selling groups
  if (Array.isArray(res)) return res;
  if (Array.isArray(res?.elements)) return res.elements;
  return [];
}

// ─── Product Reviews ────────────────────────────────────────────────────────

export interface ProductReview {
  id: string;
  productId: string;
  customerId: string | null;
  externalUser: string | null;
  points: number;
  title: string;
  content: string;
  createdAt: string;
  customer?: {
    firstName: string;
    lastName: string;
  } | null;
}

export interface ProductReviewsResponse {
  elements: ProductReview[];
  total: number;
}

export async function getProductReviews(
  productId: string,
  params?: { limit?: number; page?: number; sort?: string }
): Promise<ProductReviewsResponse> {
  const { limit = 10, page = 1, sort = '-createdAt' } = params || {};
  return shopwareApi(`/product/${productId}/reviews`, {
    body: {
      limit,
      p: page,
      sort: [
        {
          field: sort.startsWith('-') ? sort.slice(1) : sort,
          order: sort.startsWith('-') ? 'DESC' : 'ASC',
        },
      ],
    },
  });
}

export async function submitProductReview(
  productId: string,
  data: {
    title: string;
    content: string;
    points: number;
  }
): Promise<void> {
  await shopwareApi(`/product/${productId}/review`, {
    body: data,
  });
}

// ─── SEO URLs ───────────────────────────────────────────────────────────────

export interface SeoUrl {
  id: string;
  salesChannelId: string;
  foreignKey: string; // entity ID (product ID, category ID, etc.)
  routeName: string; // 'frontend.detail.page', 'frontend.navigation.page'
  pathInfo: string; // internal path like /detail/abc123
  seoPathInfo: string; // the SEO slug like 'my-product-name'
  isCanonical: boolean;
}

/**
 * Resolve a SEO URL path to a Shopware entity.
 * Uses POST /store-api/seo-url with a filter on seoPathInfo.
 */
export async function resolveSeoUrl(
  path: string
): Promise<{ routeName: string; foreignKey: string } | null> {
  try {
    const res = await shopwareApi<{ elements?: SeoUrl[] }>('/seo-url', {
      body: {
        filter: [
          {
            type: 'equals',
            field: 'seoPathInfo',
            value: path,
          },
        ],
        limit: 1,
      },
    });
    const match = res.elements?.[0];
    if (match) {
      return { routeName: match.routeName, foreignKey: match.foreignKey };
    }
    return null;
  } catch {
    return null;
  }
}

/**
 * Get the SEO URL path for a product.
 * Returns the seoPathInfo string or null.
 */
export function getProductSeoPath(product: ShopwareProduct): string | null {
  if (product.seoUrls?.length) {
    return product.seoUrls[0].seoPathInfo;
  }
  return null;
}

/**
 * Slugify a single URL segment to match Shopware's storefront convention:
 *   "Free time & electronics" → "Free-time-electronics"
 *   "Variant product"         → "Variant-product"
 *   "T-Shirts"                → "T-Shirts"
 *
 * Drops anything that isn't a unicode letter/digit/space/hyphen, then
 * collapses whitespace to single hyphens and trims edge hyphens.
 * Headless sales channels cannot have Shopware SEO URLs assigned, so the
 * storefront constructs URLs client-side from name / productNumber /
 * category breadcrumb.
 */
export function slugifySegment(s: string): string {
  return s
    .trim()
    .replace(/[^A-Za-z0-9\s-]+/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-')
    .replace(/^-+|-+$/g, '');
}

/**
 * Build the href for a product link.
 *   1. Use Shopware seoUrls when present (non-headless sales channels)
 *   2. Otherwise: /{slug(name)}/{productNumber}
 *   3. Last-resort fallback: /products/{id}
 *
 * The slug uses `canonicalName` (default-language) when present so the URL is
 * identical in every language. The trailing productNumber — not the slug — is
 * what actually resolves the product, so the slug is purely cosmetic/SEO.
 */
export function getProductUrl(product: ShopwareProduct, categoryId?: string): string {
  const seoPath = getProductSeoPath(product);
  if (seoPath) {
    return `/${seoPath}`;
  }
  const name = product.canonicalName || product.translated?.name || product.name;
  if (name && product.productNumber) {
    return `/${slugifySegment(name)}/${encodeURIComponent(product.productNumber)}`;
  }
  return categoryId
    ? `/products/${product.id}?categoryId=${categoryId}`
    : `/products/${product.id}`;
}

/**
 * Build the href for a category link.
 *   1. Use Shopware seoUrls when present (non-headless sales channels)
 *   2. Otherwise: /{slug(breadcrumb1)}/{slug(breadcrumb2)}/... — drop the root entry
 *   3. Last-resort fallback: /category/{id}
 */
export function getCategoryUrl(category: {
  id: string;
  name?: string;
  translated?: { name?: string };
  breadcrumb?: string[];
  canonicalBreadcrumb?: string[];
  canonicalName?: string;
  seoUrls?: Array<{ seoPathInfo: string }>;
}): string {
  if (category.seoUrls?.length) {
    return `/${category.seoUrls[0].seoPathInfo}`;
  }
  // Prefer the default-language breadcrumb so the slug never translates.
  const crumbs = category.canonicalBreadcrumb ?? category.breadcrumb;
  if (crumbs && crumbs.length > 1) {
    // Shopware's breadcrumb includes the root catalogue entry — drop it.
    return `/${crumbs.slice(1).map(slugifySegment).join('/')}`;
  }
  if (crumbs && crumbs.length === 1) {
    return `/${slugifySegment(crumbs[0])}`;
  }
  const name = category.canonicalName || category.translated?.name || category.name;
  if (name) {
    return `/${slugifySegment(name)}`;
  }
  return `/category/${category.id}`;
}

/**
 * Find a product by its productNumber (the unique SKU-like identifier).
 * Used by the catch-all SEO route to resolve /{slug}/{productNumber} URLs.
 */
export async function getProductByNumber(
  productNumber: string
): Promise<ShopwareProduct | null> {
  try {
    const res = await shopwareApi<{ elements?: ShopwareProduct[] }>('/product', {
      body: {
        filter: [{ type: 'equals', field: 'productNumber', value: productNumber }],
        limit: 1,
      },
    });
    return res.elements?.[0] ?? null;
  } catch {
    return null;
  }
}

/**
 * Resolve a slug path like ['Clothing', 'Men'] to a category by walking the
 * navigation tree from the root and matching each segment against the
 * slugified name.
 *
 * Matches against the canonical (default-language) name when available so a
 * canonical slug like /Clothing still resolves while browsing /de-DE/, where
 * the live tree carries German labels. getNavigation attaches `canonicalName`
 * on non-default locales; on the default locale it's undefined and the
 * translated name (which *is* the default) is used.
 */
export async function resolveCategoryByPath(
  segments: string[]
): Promise<ShopwareCategory | null> {
  if (!segments.length) return null;
  const tree = await getNavigation('main-navigation', segments.length).catch(
    () => [] as ShopwareCategory[]
  );

  let current: ShopwareCategory[] = tree;
  let found: ShopwareCategory | null = null;

  for (const segment of segments) {
    const match = current.find((c) => {
      const canonical = c.canonicalName;
      const translated = c.translated?.name || c.name || '';
      return (
        (canonical && slugifySegment(canonical) === segment) ||
        slugifySegment(translated) === segment
      );
    });
    if (!match) return null;
    found = match;
    current = match.children ?? [];
  }

  return found;
}

/**
 * Resolves the active currency ISO code.
 * 1. Explicit argument wins
 * 2. Cookie `sw-currency-iso` (readable both server- and client-side)
 * 3. env default
 *
 * Cookie-only on both sides — *never* read localStorage here. localStorage is
 * client-only, so if we used it the server would render "EUR" while the
 * client renders e.g. "USD", causing a React hydration mismatch. The
 * SalesChannelProvider migration useEffect mirrors any legacy localStorage
 * value into the cookie, so on the next request both sides will agree.
 */
/**
 * Active currency ISO code for the current request (cookie → env → EUR).
 * Exposed for structured data / metadata that needs the raw code.
 */
export function getActiveCurrencyIso(): string {
  return resolveCurrency();
}

function resolveCurrency(explicit?: string): string {
  if (explicit) return explicit;
  if (typeof window === 'undefined') {
    const fromCookie = readServerCookie('sw-currency-iso');
    if (fromCookie) return fromCookie;
  } else {
    const fromCookie = readBrowserCookie('sw-currency-iso');
    if (fromCookie) return fromCookie;
  }
  return process.env.NEXT_PUBLIC_CURRENCY || 'EUR';
}

export function formatPrice(
  price: number,
  currency?: string,
  locale: string = process.env.NEXT_PUBLIC_LOCALE || 'en-GB'
): string {
  const iso = resolveCurrency(currency);
  try {
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: iso,
    }).format(price);
  } catch {
    return `${iso} ${price.toFixed(2)}`;
  }
}

// ─── Languages & Currencies ─────────────────────────────────────────────────

export interface ShopwareLanguage {
  id: string;
  name: string;
  locale?: {
    id: string;
    code: string;
    name: string;
    territory: string;
  };
  translated?: { name: string };
}

export interface ShopwareCurrency {
  id: string;
  name: string;
  isoCode: string;
  symbol: string;
  shortName: string;
  factor: number;
  translated?: { name: string; shortName: string };
}

export async function getLanguages(): Promise<{ elements: ShopwareLanguage[] }> {
  const res = await shopwareApi<any>('/language', {
    body: {
      associations: { locale: {} },
      limit: 50,
    },
  });
  if (Array.isArray(res)) return { elements: res };
  if (Array.isArray(res?.elements)) return { elements: res.elements };
  return { elements: [] };
}

export async function getCurrencies(): Promise<{ elements: ShopwareCurrency[] }> {
  const res = await shopwareApi<any>('/currency', {
    body: { limit: 50 },
  });
  if (Array.isArray(res)) return { elements: res };
  if (Array.isArray(res?.elements)) return { elements: res.elements };
  return { elements: [] };
}

// ─── Theme Configuration ────────────────────────────────────────────────────
// Note: Theme data is NOT exposed via the Store API (headless) for security reasons.
// Theme logos must be configured via environment variables.
//
// In your .env.local, add:
//   NEXT_PUBLIC_LOGO_DESKTOP=https://your-shopware.com/media/...
//   NEXT_PUBLIC_LOGO_TABLET=https://your-shopware.com/media/...
//   NEXT_PUBLIC_LOGO_MOBILE=https://your-shopware.com/media/...
//
// To get these URLs:
// 1. Go to Shopware Admin → Settings → Themes → Your Theme
// 2. Expand "Media" section
// 3. Right-click each logo → Copy image link for desktop/tablet/mobile
// 4. Paste into .env.local

export interface ThemeLogo {
  desktop?: string | null;
  tablet?: string | null;
  mobile?: string | null;
}

/**
 * @deprecated - Use environment variables instead (NEXT_PUBLIC_LOGO_DESKTOP, etc.)
 * This function is kept for reference and potential future Admin API integration.
 * The Store API does not expose theme data for security reasons.
 */
export async function getThemeLogoUrls(): Promise<ThemeLogo> {
  // Store API doesn't expose theme endpoints or system-config
  // Use environment variables instead in the Header component
  return {};
}
