export interface Product3DHoverLabelInput {
  meshCount?: number;
  partLabels?: unknown[];
  elementLabels?: unknown[];
  fallbackLabels?: unknown[];
}

function normalizePartId(value: unknown): string {
  return String(value ?? '')
    .trim()
    .toLowerCase()
    .replace(/^.*::/, '')
    .replace(/^asuka_(part|surface)_/i, '')
    .replace(/_print$/i, '')
    .replace(/[^a-z0-9]+/g, '_')
    .replace(/^_+|_+$/g, '');
}

function firstNonEmptyLabel(values: unknown[] | undefined): string {
  for (const value of Array.isArray(values) ? values : []) {
    const label = String(value ?? '').trim();
    if (label) return label;
  }
  return '';
}

/** Resolve the user-facing name of the variation group bound to a 3D part. */
export function resolveProduct3DPartVariationGroupLabel(
  variations: unknown,
  partIds: unknown[],
): string {
  const wanted = new Set((Array.isArray(partIds) ? partIds : []).map(normalizePartId).filter(Boolean));
  if (!wanted.size) return '';

  const visit = (items: unknown): string => {
    for (const item of Array.isArray(items) ? items : []) {
      if (!item || typeof item !== 'object') continue;
      const variation = item as any;
      const isPartGroup = variation?.product3d?.partGroup === true
        || variation?.metadata?.asukaProduct3DPartGroup === true;
      const partId = normalizePartId(
        variation?.product3d?.partId
        ?? variation?.metadata?.asukaProduct3DPartId
        ?? variation?.product3d?.materialTarget,
      );
      if (isPartGroup && partId && wanted.has(partId)) {
        return firstNonEmptyLabel([variation?.name, variation?.label]);
      }
      const nested = visit(variation?.variations);
      if (nested) return nested;
    }
    return '';
  };

  return visit(variations);
}

/** Resolve the editable catalog label for one runtime 3D part. */
export function resolveProduct3DPartDisplayLabel(
  variations: unknown,
  part: unknown,
): string {
  const value = part && typeof part === 'object' ? part as any : {};
  const configuredLabel = resolveProduct3DPartVariationGroupLabel(variations, [
    value?.id,
    value?.name,
    value?.label,
    value?.materialTarget,
    value?.metadata?.asukaProduct3DPartId,
    value?.metadata?.originalMaterialTarget,
    value?.metadata?.partId,
    ...(Array.isArray(value?.meshIds) ? value.meshIds : []),
    ...(Array.isArray(value?.meshNames) ? value.meshNames : []),
    ...(Array.isArray(value?.materialIds) ? value.materialIds : []),
    ...(Array.isArray(value?.materialNames) ? value.materialNames : []),
  ]);
  return configuredLabel || firstNonEmptyLabel([value?.label, value?.name, value?.id]);
}

/**
 * Resolve the canvas hover label for a Product 3D surface.
 *
 * A product made of exactly one renderable mesh has no useful part distinction,
 * so exposing an exporter-generated node name (for example `node_id5`) is noisy.
 * In that case the catalog/runtime product label wins. Multi-mesh products keep
 * the established part-first behavior so labels such as "Laces" or "Swoosh"
 * remain unchanged.
 */
export function resolveProduct3DHoverLabel(input: Product3DHoverLabelInput): string {
  const partLabel = firstNonEmptyLabel(input.partLabels);
  const elementLabel = firstNonEmptyLabel(input.elementLabels);
  const fallbackLabel = firstNonEmptyLabel(input.fallbackLabels);

  if (Number(input.meshCount) === 1) {
    return elementLabel || partLabel || fallbackLabel || 'Part';
  }

  return partLabel || elementLabel || fallbackLabel || 'Part';
}
