import type { ComponentChildren, FunctionalComponent } from 'preact';
import { useState } from 'preact/hooks';

import { Badge, Button, SectionCard, cn } from '../../core';
import { AsukaIcon } from '../../core/AsukaIcon';
import { Checkbox } from '../../core/Checkbox';
import { Form } from '../../core/Form';
import { ColorField, FormGrid, ImageField, ModelField, NumberField, SelectField, TextField } from '../../forms';
import type { AsukaUploadModuleConfig } from '../../../types/options';
import { ASUKA_PARAMETRIC_VARIATION_PRODUCT_SCOPE, type AsukaParametricDefinition, type AsukaParametricParameter, type AsukaParametricRule, type AsukaParametricVariationControls, type AsukaParametricVariationScope } from '../../../types/parametric3d';
import { normalizeParametricVariationScopeTarget } from '../../../core/parametric3d';
import { ElementParametricVariationControlsField } from './ElementParametricVariationControlsField';

type ElementVariationTreeFieldProps = {
  t: (key: string, fallback?: string) => string;
  form: any;
  createDefaultVariation: () => any;
  getFormValue: (values: any, path: string) => any;
  upload?: AsukaUploadModuleConfig;
};

type CompactEditorSectionProps = {
  title: ComponentChildren;
  description?: ComponentChildren;
  meta?: ComponentChildren;
  defaultOpen?: boolean;
  className?: string;
  children: ComponentChildren;
};

const CompactEditorSection: FunctionalComponent<CompactEditorSectionProps> = ({ title, description, meta, defaultOpen = false, className, children }) => {
  const [open, setOpen] = useState(defaultOpen);

  return (
    <section className={cn('ask-overflow-hidden ask-rounded-lg ask-border ask-border-borderSubtle ask-bg-surface', className)} data-asuka-editor-section>
      <button type="button" className="ask-flex ask-min-h-10 ask-w-full ask-items-center ask-gap-2.5 ask-px-3 ask-py-2 ask-text-left hover:ask-bg-surfaceAlt" aria-expanded={open} onClick={() => setOpen((value) => !value)}>
        <AsukaIcon name="chevron-right" size={12} className={cn('ask-shrink-0 ask-text-textMuted ask-transition-transform', open && 'ask-rotate-90')} />
        <span className="ask-min-w-0 ask-flex-1 ask-text-[11px] ask-font-semibold ask-text-textMain">{title}</span>
        {meta ? <span className="ask-shrink-0">{meta}</span> : null}
      </button>
      {open ? (
        <div className="ask-border-t ask-border-borderSubtle ask-p-2.5">
          {description ? <div className="ask-mb-2.5 ask-text-[10px] ask-leading-4 ask-text-textMuted">{description}</div> : null}
          {children}
        </div>
      ) : null}
    </section>
  );
};

const isProduct3DForm = (form: any): boolean => String((form?.values as any)?.type ?? '').trim() === 'product-3d';

const inferModelFormat = (value: string, fallback: string = 'glb'): 'glb' | 'gltf' => {
  const v = String(value ?? '')
    .trim()
    .toLowerCase();
  if (v.startsWith('data:model/gltf+json') || /\.gltf(?:[?#].*)?$/.test(v)) return 'gltf';
  if (v.startsWith('data:model/gltf-binary') || /\.glb(?:[?#].*)?$/.test(v)) return 'glb';
  return String(fallback ?? 'glb').toLowerCase() === 'gltf' ? 'gltf' : 'glb';
};

const readProduct3DAtPath = (form: any, path: string): any => {
  const value = form?.values ? getPathValue(form.values, `${path}.product3d`) : undefined;
  return value && typeof value === 'object' ? value : {};
};

const getPathValue = (values: any, path: string): any => {
  const parts = String(path ?? '')
    .replace(/\]/g, '')
    .split(/[.[]/)
    .filter(Boolean);
  let current = values;
  for (const part of parts) {
    if (current == null) return undefined;
    current = current[part as any];
  }
  return current;
};

const getParentVariationPath = (path: string): string => {
  const raw = String(path ?? '');
  return raw.replace(/\.variations\[\d+\]$/, '');
};

const isProduct3DPartGroupAtPath = (form: any, path: string): boolean => Boolean(getPathValue(form?.values, `${path}.metadata.asukaProduct3DPartGroup`) === true || getPathValue(form?.values, `${path}.product3d.partGroup`) === true);

const findProduct3DPartGroupPathForVariationPath = (form: any, path: string): string => {
  let cursor = String(path ?? '').trim();
  while (cursor) {
    if (isProduct3DPartGroupAtPath(form, cursor)) return cursor;
    const next = getParentVariationPath(cursor);
    if (!next || next === cursor) break;
    cursor = next;
  }
  return '';
};

const getProduct3DPartScopedTarget = (form: any, path: string): string => {
  const groupPath = findProduct3DPartGroupPathForVariationPath(form, path);
  if (!groupPath) return '';
  const groupProduct = readProduct3DAtPath(form, groupPath);
  return String(groupProduct?.materialTarget ?? groupProduct?.partId ?? getPathValue(form.values, `${groupPath}.metadata.asukaProduct3DPartId`) ?? '').trim();
};

const getProduct3DPartScopedMeshIds = (form: any, path: string): string[] => {
  const target = getProduct3DPartScopedTarget(form, path);
  if (!target) return [];
  const parts = Array.isArray((form?.values as any)?.product3d?.parts) ? (form.values as any).product3d.parts : [];
  const part = parts.find((item: any) => String(item?.materialTarget ?? item?.id ?? '').trim() === target || String(item?.id ?? '').trim() === target);
  return String(Array.isArray(part?.meshNames) ? part.meshNames.join(',') : (part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId ?? ''))
    .split(',')
    .map((item) => item.trim())
    .filter(Boolean);
};

const hasProduct3DPartGroups = (form: any): boolean => {
  const variations = Array.isArray((form?.values as any)?.variations) ? (form.values as any).variations : [];
  const walk = (list: any[]): boolean =>
    list.some((variation: any) => {
      if (!variation || typeof variation !== 'object') return false;
      if (variation?.metadata?.asukaProduct3DPartGroup === true || variation?.product3d?.partGroup === true) return true;
      return Array.isArray(variation?.variations) ? walk(variation.variations) : false;
    });
  return walk(variations);
};

const getDefaultTargetMesh = (form: any): string => {
  const product3d = (form?.values as any)?.product3d;
  // Product/support print areas are managed by DrawingAreasModule, not ElementEditor.
  // Variations own their material target; fallback to the generic placeholder mesh.
  return String(product3d?.materialTarget ?? 'product').trim() || 'product';
};

const getVariationMaterialTarget = (form: any, path: string): string => {
  const partTarget = getProduct3DPartScopedTarget(form, path);
  if (partTarget) return partTarget;
  const variationProduct = readProduct3DAtPath(form, path);
  const parentProduct = readProduct3DAtPath(form, getParentVariationPath(path));
  return String(variationProduct.materialTarget ?? parentProduct.materialTarget ?? getDefaultTargetMesh(form)).trim() || 'product';
};

const getVariationMaterial = (form: any, path: string, target: string): any => {
  const variationProduct = readProduct3DAtPath(form, path);
  const variationMaterials = variationProduct.materials && typeof variationProduct.materials === 'object' ? variationProduct.materials : {};
  return variationMaterials[target] && typeof variationMaterials[target] === 'object' ? variationMaterials[target] : {};
};

const readTextureRepeatValue = (material: any, axis: 'x' | 'y'): number => {
  const metadata = material?.metadata && typeof material.metadata === 'object' ? material.metadata : {};
  const candidates = axis === 'x' ? [material?.textureRepeatX, material?.repeatX, material?.textureScaleX, material?.uScale, material?.textureRepeat, metadata?.textureRepeatX, metadata?.repeatX, metadata?.uScale] : [material?.textureRepeatY, material?.repeatY, material?.textureScaleY, material?.vScale, material?.textureRepeat, metadata?.textureRepeatY, metadata?.repeatY, metadata?.vScale];
  for (const candidate of candidates) {
    const value = Number(candidate);
    if (Number.isFinite(value) && value > 0) return Math.max(0.01, Math.min(100, value));
  }
  return 1;
};

const patchVariationProduct3D = (form: any, path: string, patch: Record<string, any>) => {
  const current = readProduct3DAtPath(form, path);
  form.setValue(`${path}.product3d`, { ...current, ...patch });
};

export const mergeProduct3DVariationMaterialPatch = (materialInput: any, patch: Record<string, any>): Record<string, any> => {
  const nextMaterial = {
    ...(materialInput && typeof materialInput === 'object' ? materialInput : {}),
    ...patch,
  } as Record<string, any>;
  if (Object.keys(patch).some((field) => field !== 'label')) delete nextMaterial.useNativeMaterial;
  return nextMaterial;
};

const patchVariationMaterial = (form: any, path: string, target: string, patch: Record<string, any>) => {
  const scopedTarget = getProduct3DPartScopedTarget(form, path);
  const key = String(scopedTarget || target || '').trim() || 'product';
  const current = readProduct3DAtPath(form, path);
  const materials = current.materials && typeof current.materials === 'object' ? current.materials : {};
  const material = materials[key] && typeof materials[key] === 'object' ? materials[key] : {};
  const targetMeshIds = scopedTarget ? getProduct3DPartScopedMeshIds(form, path) : [];
  const nextMaterial = mergeProduct3DVariationMaterialPatch(
    {
      ...material,
      label: material.label ?? key,
      ...(targetMeshIds.length ? { targetMeshIds } : {}),
      metadata: {
        ...(material?.metadata && typeof material.metadata === 'object' ? material.metadata : {}),
        ...(targetMeshIds.length ? { targetMeshIds } : {}),
      },
    },
    patch,
  );
  // Under a detected part group, keep only the current part material in this child
  // variation. Otherwise a stale `product` material can be kept beside the scoped
  // target and recolor the entire 3D model when this variation is applied.
  patchVariationProduct3D(form, path, {
    materialTarget: key,
    materials: scopedTarget ? { [key]: nextMaterial } : { ...materials, [key]: nextMaterial },
  });
};

const cleanParametricId = (value: unknown): string => String(value ?? '').trim();

const readParametricDefinition = (form: any): AsukaParametricDefinition | null => {
  const value = form?.values?.parametric;
  return value && value.schemaVersion === 1 && Array.isArray(value.parameters) ? value : null;
};

const normalizeVariationParametricControls = (input: any): { enabled: boolean; parameterIds: string[]; title?: string; description?: string } | undefined => {
  if (!input || typeof input !== 'object') return undefined;
  const parameterIds: string[] = Array.from(new Set<string>((Array.isArray(input.parameterIds) ? input.parameterIds : []).map(cleanParametricId).filter(Boolean) as string[]));
  const title = cleanParametricId(input.title);
  const description = cleanParametricId(input.description);
  if (!parameterIds.length && input.enabled !== true && !title && !description) return undefined;
  return {
    enabled: input.enabled === false ? false : true,
    parameterIds,
    ...(title ? { title } : {}),
    ...(description ? { description } : {}),
  };
};

const normalizeVariationControlScopes = (scopes: AsukaParametricVariationScope[] | undefined, validParameterIds: Set<string>): AsukaParametricVariationScope[] => {
  const merged = new Map<string, AsukaParametricVariationScope>();
  for (const scope of Array.isArray(scopes) ? scopes : []) {
    const target = cleanParametricId(scope?.target);
    const key = normalizeParametricVariationScopeTarget(target);
    if (!target || !key) continue;
    const parameterIds = Array.from(new Set((Array.isArray(scope?.parameterIds) ? scope.parameterIds : []).map(cleanParametricId).filter((id) => validParameterIds.has(id))));
    const previous = merged.get(key);
    merged.set(key, {
      target: previous?.target || target,
      parameterIds: Array.from(new Set([...(previous?.parameterIds ?? []), ...parameterIds])),
      title: cleanParametricId(previous?.title ?? scope?.title) || undefined,
      description: cleanParametricId(previous?.description ?? scope?.description) || undefined,
    });
  }
  return Array.from(merged.values());
};

const getVariationParametricScopeTarget = (form: any, path: string): { id: string; label: string } | null => {
  const groupPath = findProduct3DPartGroupPathForVariationPath(form, path);
  if (groupPath) {
    if (groupPath !== path) return null;
    const id = getProduct3DPartScopedTarget(form, path);
    if (!id) return null;
    const label = String(getPathValue(form.values, `${path}.name`) ?? id).trim() || id;
    return { id, label };
  }

  if (hasProduct3DPartGroups(form)) return null;

  return {
    id: ASUKA_PARAMETRIC_VARIATION_PRODUCT_SCOPE,
    label: 'Whole product',
  };
};

const slugifyParametricId = (value: unknown): string =>
  String(value ?? '')
    .trim()
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '') || 'parameter';

const uniqueParametricId = (definition: AsukaParametricDefinition, base: string): string => {
  const existing = new Set((definition.parameters ?? []).map((parameter) => cleanParametricId(parameter.id)).filter(Boolean));
  const cleanBase = slugifyParametricId(base);
  if (!existing.has(cleanBase)) return cleanBase;
  let index = 2;
  while (existing.has(`${cleanBase}-${index}`)) index += 1;
  return `${cleanBase}-${index}`;
};

const createParametricDefinitionFromProduct = (form: any): AsukaParametricDefinition => {
  const parts: Record<string, any> = {};
  const sourceParts = Array.isArray((form?.values as any)?.product3d?.parts) ? (form.values as any).product3d.parts : [];
  for (const part of sourceParts) {
    const id = cleanParametricId(part?.id ?? part?.materialTarget ?? part?.label);
    if (!id) continue;
    const selectors = String(Array.isArray(part?.meshNames) ? part.meshNames.join(',') : (part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId ?? id))
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean);
    parts[id] = {
      selectors: selectors.length ? selectors : [id],
      metadata: { label: String(part?.label ?? id).trim() || id },
    };
  }
  return {
    schemaVersion: 1,
    parameters: [],
    groups: [{ id: 'dynamic', label: 'Dynamic parameters', parameters: [] }],
    parts,
    computedValues: {},
    rules: [],
    constraints: [],
    ui: {
      variationControls: {
        enabled: true,
        placement: 'before',
        title: 'Dimensions & parameters',
        description: '',
        scopes: [],
        compact: true,
      },
    },
  };
};

const ensureParametricTargetPart = (definition: AsukaParametricDefinition, targetIdInput: string, targetLabel?: string): AsukaParametricDefinition => {
  const targetId = cleanParametricId(targetIdInput);
  if (!targetId) return definition;
  if ((definition.parts ?? {})[targetId]) return definition;
  return {
    ...definition,
    parts: {
      ...(definition.parts ?? {}),
      [targetId]: {
        selectors: targetId === ASUKA_PARAMETRIC_VARIATION_PRODUCT_SCOPE ? ['*'] : [targetId],
        metadata: { label: cleanParametricId(targetLabel) || targetId },
      },
    },
  };
};

type DynamicParametricAction = 'scale-x' | 'scale-y' | 'scale-z' | 'scale-uniform' | 'rotate-x' | 'rotate-y' | 'rotate-z' | 'offset-x' | 'offset-y' | 'offset-z' | 'visibility';

const PARAMETRIC_ACTION_OPTIONS: Array<{
  value: DynamicParametricAction;
  label: string;
  defaultLabel: string;
  unit?: string;
  min: number;
  max: number;
  step: number;
  defaultValue: number | boolean;
}> = [
  {
    value: 'scale-x',
    label: 'Scale width / X',
    defaultLabel: 'Width',
    unit: '%',
    min: 50,
    max: 200,
    step: 5,
    defaultValue: 100,
  },
  {
    value: 'scale-y',
    label: 'Scale height / Y',
    defaultLabel: 'Height',
    unit: '%',
    min: 50,
    max: 200,
    step: 5,
    defaultValue: 100,
  },
  {
    value: 'scale-z',
    label: 'Scale depth / Z',
    defaultLabel: 'Depth',
    unit: '%',
    min: 50,
    max: 200,
    step: 5,
    defaultValue: 100,
  },
  {
    value: 'scale-uniform',
    label: 'Scale uniformly',
    defaultLabel: 'Size',
    unit: '%',
    min: 50,
    max: 200,
    step: 5,
    defaultValue: 100,
  },
  {
    value: 'rotate-x',
    label: 'Rotate X',
    defaultLabel: 'Rotation X',
    unit: '°',
    min: -90,
    max: 90,
    step: 1,
    defaultValue: 0,
  },
  {
    value: 'rotate-y',
    label: 'Rotate Y',
    defaultLabel: 'Rotation Y',
    unit: '°',
    min: -90,
    max: 90,
    step: 1,
    defaultValue: 0,
  },
  {
    value: 'rotate-z',
    label: 'Rotate Z',
    defaultLabel: 'Rotation',
    unit: '°',
    min: -90,
    max: 90,
    step: 1,
    defaultValue: 0,
  },
  {
    value: 'offset-x',
    label: 'Move X',
    defaultLabel: 'Horizontal position',
    min: -1,
    max: 1,
    step: 0.05,
    defaultValue: 0,
  },
  {
    value: 'offset-y',
    label: 'Move Y',
    defaultLabel: 'Vertical position',
    min: -1,
    max: 1,
    step: 0.05,
    defaultValue: 0,
  },
  {
    value: 'offset-z',
    label: 'Move Z',
    defaultLabel: 'Depth position',
    min: -1,
    max: 1,
    step: 0.05,
    defaultValue: 0,
  },
  {
    value: 'visibility',
    label: 'Show / hide',
    defaultLabel: 'Visible',
    min: 0,
    max: 1,
    step: 1,
    defaultValue: true,
  },
];

const buildParametricRuleForAction = (action: DynamicParametricAction, parameterId: string, target: string): AsukaParametricRule => {
  if (action === 'visibility') return { id: `${parameterId}:visibility`, type: 'visibility', target, visible: parameterId };
  const percentExpression = `${parameterId} / 100`;
  if (action === 'scale-x')
    return {
      id: `${parameterId}:scale-x`,
      type: 'transform',
      target,
      scale: { x: percentExpression },
    };
  if (action === 'scale-y')
    return {
      id: `${parameterId}:scale-y`,
      type: 'transform',
      target,
      scale: { y: percentExpression },
    };
  if (action === 'scale-z')
    return {
      id: `${parameterId}:scale-z`,
      type: 'transform',
      target,
      scale: { z: percentExpression },
    };
  if (action === 'scale-uniform')
    return {
      id: `${parameterId}:scale`,
      type: 'transform',
      target,
      scale: { x: percentExpression, y: percentExpression, z: percentExpression },
    };
  if (action === 'rotate-x')
    return {
      id: `${parameterId}:rotate-x`,
      type: 'transform',
      target,
      rotationOffset: { x: parameterId },
    };
  if (action === 'rotate-y')
    return {
      id: `${parameterId}:rotate-y`,
      type: 'transform',
      target,
      rotationOffset: { y: parameterId },
    };
  if (action === 'rotate-z')
    return {
      id: `${parameterId}:rotate-z`,
      type: 'transform',
      target,
      rotationOffset: { z: parameterId },
    };
  if (action === 'offset-x')
    return {
      id: `${parameterId}:offset-x`,
      type: 'transform',
      target,
      positionOffset: { x: parameterId },
    };
  if (action === 'offset-y')
    return {
      id: `${parameterId}:offset-y`,
      type: 'transform',
      target,
      positionOffset: { y: parameterId },
    };
  return {
    id: `${parameterId}:offset-z`,
    type: 'transform',
    target,
    positionOffset: { z: parameterId },
  };
};

const buildParametricMeasurementForAction = (action: DynamicParametricAction, target: string): AsukaParametricParameter['measurement'] | undefined => {
  if (action === 'scale-x') return { kind: 'length', dimension: 'width', target, storage: 'percent' };
  if (action === 'scale-y') return { kind: 'length', dimension: 'height', target, storage: 'percent' };
  if (action === 'scale-z') return { kind: 'length', dimension: 'depth', target, storage: 'percent' };
  if (action === 'scale-uniform') return { kind: 'length', dimension: 'width', target, storage: 'percent' };
  if (action === 'offset-x') return { kind: 'length', dimension: 'width', target, storage: 'world-unit' };
  if (action === 'offset-y') return { kind: 'length', dimension: 'height', target, storage: 'world-unit' };
  if (action === 'offset-z') return { kind: 'length', dimension: 'depth', target, storage: 'world-unit' };
  return undefined;
};

const ElementVariationParametricScopeField: FunctionalComponent<{
  t: ElementVariationTreeFieldProps['t'];
  form: any;
  path: string;
}> = ({ t, form, path }) => {
  const [newLabel, setNewLabel] = useState('');
  const [newAction, setNewAction] = useState<DynamicParametricAction>('scale-x');
  const [newMin, setNewMin] = useState<number | string>('');
  const [newMax, setNewMax] = useState<number | string>('');
  const [newDefault, setNewDefault] = useState<number | string>('');
  const [newStep, setNewStep] = useState<number | string>('');

  const target = getVariationParametricScopeTarget(form, path);
  if (!target) return null;

  const rawDefinition = readParametricDefinition(form);
  const definition = rawDefinition ?? createParametricDefinitionFromProduct(form);
  const current = (definition.ui?.variationControls ?? {}) as AsukaParametricVariationControls;
  const controlsEnabled = current.enabled === true;
  const allParameterIds = definition.parameters.map((parameter) => cleanParametricId(parameter.id)).filter(Boolean);
  const validParameterIds = new Set(allParameterIds);
  const scopes = normalizeVariationControlScopes(current.scopes, validParameterIds);
  const targetKey = normalizeParametricVariationScopeTarget(target.id);
  const scope = scopes.find((item) => normalizeParametricVariationScopeTarget(item.target) === targetKey);
  const localControls = normalizeVariationParametricControls(getPathValue(form.values, `${path}.parametricControls`));
  const selected = new Set([...(scope?.parameterIds ?? []), ...(localControls?.parameterIds ?? [])].filter((id) => validParameterIds.has(id)));

  const actionDefaults = PARAMETRIC_ACTION_OPTIONS.find((option) => option.value === newAction) ?? PARAMETRIC_ACTION_OPTIONS[0];
  const isBooleanAction = newAction === 'visibility';

  const writeDefinition = (nextDefinition: AsukaParametricDefinition) => {
    form.setValue('parametric', nextDefinition);
  };

  const patchDefinitionControls = (baseDefinition: AsukaParametricDefinition, changes: Partial<AsukaParametricVariationControls>): AsukaParametricDefinition => ({
    ...baseDefinition,
    ui: {
      ...(baseDefinition.ui ?? {}),
      variationControls: {
        ...((baseDefinition.ui?.variationControls ?? {}) as AsukaParametricVariationControls),
        enabled: true,
        placement: current.placement === 'after' ? 'after' : 'before',
        compact: current.compact !== false,
        ...changes,
      },
    },
  });

  const replaceTargetParameters = (parameterIds: string[], baseDefinition: AsukaParametricDefinition = definition) => {
    const normalizedIds = Array.from(
      new Set(
        parameterIds.map(cleanParametricId).filter((id) => {
          const ids = new Set((baseDefinition.parameters ?? []).map((parameter) => cleanParametricId(parameter.id)).filter(Boolean));
          return ids.has(id);
        }),
      ),
    );
    const baseControls = (baseDefinition.ui?.variationControls ?? {}) as AsukaParametricVariationControls;
    const baseScopes = normalizeVariationControlScopes(baseControls.scopes, new Set((baseDefinition.parameters ?? []).map((parameter) => cleanParametricId(parameter.id)).filter(Boolean)));
    const nextScope: AsukaParametricVariationScope = {
      target: target.id,
      parameterIds: normalizedIds,
      ...(scope?.title ? { title: scope.title } : {}),
      ...(scope?.description ? { description: scope.description } : {}),
    };
    const nextScopes = baseScopes.filter((item) => normalizeParametricVariationScopeTarget(item.target) !== targetKey);
    if (nextScope.parameterIds.length) nextScopes.push(nextScope);
    const nextDefinition = patchDefinitionControls(baseDefinition, { scopes: nextScopes });
    writeDefinition(nextDefinition);
    form.setValue(`${path}.parametricControls`, {
      enabled: true,
      parameterIds: normalizedIds,
      ...(localControls?.title ? { title: localControls.title } : {}),
      ...(localControls?.description ? { description: localControls.description } : {}),
    });
  };

  const toggleParameter = (parameterId: string, checked: boolean) => {
    const next = new Set(selected);
    if (checked) next.add(parameterId);
    else next.delete(parameterId);
    replaceTargetParameters(allParameterIds.filter((id) => next.has(id)));
  };

  const enableControls = () => {
    const next = patchDefinitionControls(definition, { scopes });
    writeDefinition(next);
  };

  const addDynamicParameter = () => {
    const base = rawDefinition ?? createParametricDefinitionFromProduct(form);
    const targetId = target.id === ASUKA_PARAMETRIC_VARIATION_PRODUCT_SCOPE ? ASUKA_PARAMETRIC_VARIATION_PRODUCT_SCOPE : target.id;
    const withTarget = ensureParametricTargetPart(base, targetId, target.label);
    const label = cleanParametricId(newLabel) || actionDefaults.defaultLabel;
    const parameterId = uniqueParametricId(withTarget, `${target.label}-${label}`);
    const measurement = buildParametricMeasurementForAction(newAction, targetId);
    const parameter: AsukaParametricParameter = isBooleanAction
      ? { id: parameterId, label, type: 'boolean', default: true }
      : {
          id: parameterId,
          label,
          type: 'number',
          ...(actionDefaults.unit ? { unit: actionDefaults.unit } : {}),
          ...(measurement ? { measurement } : {}),
          min: Number(newMin !== '' ? newMin : actionDefaults.min),
          max: Number(newMax !== '' ? newMax : actionDefaults.max),
          step: Number(newStep !== '' ? newStep : actionDefaults.step),
          default: Number(newDefault !== '' ? newDefault : actionDefaults.defaultValue),
        };
    const rule = buildParametricRuleForAction(newAction, parameterId, targetId);
    const nextDefinition: AsukaParametricDefinition = {
      ...withTarget,
      parameters: [...(withTarget.parameters ?? []), parameter],
      groups:
        Array.isArray(withTarget.groups) && withTarget.groups.length
          ? withTarget.groups.map((group, index) =>
              index === 0
                ? {
                    ...group,
                    parameters: Array.from(new Set([...(group.parameters ?? []), parameterId])),
                  }
                : group,
            )
          : [{ id: 'dynamic', label: 'Dynamic parameters', parameters: [parameterId] }],
      rules: [...(withTarget.rules ?? []), rule],
    };
    replaceTargetParameters([...Array.from(selected), parameterId], nextDefinition);
    setNewLabel('');
    setNewMin('');
    setNewMax('');
    setNewDefault('');
    setNewStep('');
  };

  return (
    <CompactEditorSection
      className="ask-col-span-2 max-lg:ask-col-span-1"
      title={t('elements.parametric.inlineScopeTitle', 'Dynamic parameters')}
      description={t('elements.parametric.inlineScopeDescriptionCreate', 'Create or reuse the parameters that belong to this variation step. Asuka stores the shared recipe automatically.')}
      meta={
        <span className="ask-rounded-full ask-border ask-border-borderSubtle ask-bg-surfaceAlt ask-px-2 ask-py-0.5 ask-text-[9px] ask-font-semibold ask-text-textMuted">
          {selected.size} / {allParameterIds.length}
        </span>
      }
    >
      {!controlsEnabled ? (
        <div className="ask-mb-2 ask-rounded-lg ask-border ask-border-primary/20 ask-bg-primary/5 ask-p-2.5">
          <div className="ask-text-[10px] ask-leading-4 ask-text-textMuted">{t('elements.parametric.enableInlineHint', 'Enable customer-facing dimension controls before assigning them to variations.')}</div>
          <Button type="button" size="sm" variant="secondary" className="ask-mt-2" onClick={enableControls}>
            {t('elements.parametric.enableVariationControlsShort', 'Enable dimensions')}
          </Button>
        </div>
      ) : null}

      <div className="ask-mb-2 ask-flex ask-flex-wrap ask-items-center ask-gap-1.5">
        <span className="ask-rounded-full ask-bg-primary/8 ask-px-2 ask-py-1 ask-text-[9px] ask-font-medium ask-text-primary">{target.label}</span>
        {allParameterIds.length ? (
          <>
            <Button type="button" size="sm" variant="ghost" onClick={() => replaceTargetParameters(allParameterIds)}>
              {t('common.selectAll', 'Select all')}
            </Button>
            <Button type="button" size="sm" variant="ghost" onClick={() => replaceTargetParameters([])}>
              {t('common.clear', 'Clear')}
            </Button>
          </>
        ) : null}
      </div>

      <div className="ask-flex ask-flex-col ask-gap-2">
        {allParameterIds.length ? (
          <CompactEditorSection title={t('elements.parametric.reuseExisting', 'Reuse existing parameter')} meta={<span className="ask-text-[9px] ask-font-semibold ask-text-textMuted">{selected.size}</span>}>
            <div className="ask-grid ask-grid-cols-2 ask-gap-1.5 max-lg:ask-grid-cols-1">
              {definition.parameters.map((parameter) => {
                const id = cleanParametricId(parameter.id);
                if (!id) return null;
                return <Checkbox key={`${target.id}-${id}`} checked={selected.has(id)} label={String(parameter.label ?? id)} onChange={(event: any) => toggleParameter(id, !!event.currentTarget?.checked)} className="ask-rounded-lg ask-border ask-border-borderSubtle ask-bg-surfaceAlt ask-p-2 ask-text-[10px]" />;
              })}
            </div>
          </CompactEditorSection>
        ) : null}

        <CompactEditorSection title={t('elements.parametric.createNewParameter', 'Create a parameter for this variation')} description={t('elements.parametric.createNewParameterHint', 'Choose the user-facing control and the geometric action applied to the current product or part.')} defaultOpen={!allParameterIds.length}>
          <FormGrid columns={2} gap="sm" className="max-lg:ask-grid-cols-1">
            <TextField compact label={t('common.label', 'Label')} value={newLabel} placeholder={actionDefaults.defaultLabel} onValueChange={setNewLabel} />
            <SelectField
              compact
              label={t('elements.parametric.action', 'Action')}
              value={newAction}
              options={PARAMETRIC_ACTION_OPTIONS.map((option) => ({
                value: option.value,
                label: option.label,
              }))}
              onValueChange={(value) => setNewAction(value as DynamicParametricAction)}
            />
            {!isBooleanAction ? (
              <>
                <NumberField compact label={t('common.min', 'Min')} value={newMin === '' ? actionDefaults.min : newMin} step="any" onValueChange={(value) => setNewMin(value ?? '')} />
                <NumberField compact label={t('common.max', 'Max')} value={newMax === '' ? actionDefaults.max : newMax} step="any" onValueChange={(value) => setNewMax(value ?? '')} />
                <NumberField compact label={t('common.default', 'Default')} value={newDefault === '' ? (actionDefaults.defaultValue as number) : newDefault} step="any" onValueChange={(value) => setNewDefault(value ?? '')} />
                <NumberField compact label={t('common.step', 'Step')} value={newStep === '' ? actionDefaults.step : newStep} step="any" onValueChange={(value) => setNewStep(value ?? '')} />
              </>
            ) : null}
          </FormGrid>
          <Button type="button" size="sm" variant="secondary" className="ask-mt-2.5" onClick={addDynamicParameter}>
            {t('elements.parametric.addParameterHere', 'Add parameter here')}
          </Button>
        </CompactEditorSection>
      </div>
    </CompactEditorSection>
  );
};

const Product3DVariationMaterialFields: FunctionalComponent<{
  t: ElementVariationTreeFieldProps['t'];
  form: any;
  path: string;
  upload?: AsukaUploadModuleConfig;
}> = ({ t, form, path, upload }) => {
  const target = getVariationMaterialTarget(form, path);
  const isPartScoped = Boolean(getProduct3DPartScopedTarget(form, path));
  const material = getVariationMaterial(form, path, target);
  const product3d = readProduct3DAtPath(form, path);
  const modelUrl = String(product3d?.model?.url ?? '');
  const modelFormat = String(product3d?.model?.format ?? 'glb');

  return (
    <CompactEditorSection
      className="ask-col-span-2 max-lg:ask-col-span-1"
      title={t('elements.product3d.variationAppearance', 'Variation product appearance')}
      description={t('elements.product3d.variationAppearanceHelper', 'Each Product 3D variation owns the product model override, preview, color and texture used by the active 3D view.')}
      meta={<span className="ask-block ask-h-4 ask-w-4 ask-rounded-full ask-border ask-border-borderSubtle" style={{ backgroundColor: String(material.color ?? '#d7dde8') }} aria-hidden="true" />}
    >
      <div className="ask-flex ask-flex-col ask-gap-2">
        <CompactEditorSection title={t('elements.product3d.modelPreviewGroup', 'Model & preview')}>
          <FormGrid columns={2} gap="sm" className="max-lg:ask-grid-cols-1">
            <ModelField
              compact
              fieldClassName="sm:ask-col-span-2"
              label={t('elements.product3d.variationModelFile', 'Variation model override')}
              hint={t('elements.product3d.variationModelFileHint', 'Optional. Leave empty to use the main product model.')}
              placeholder="/models/tshirt-black.glb"
              accept=".glb,.gltf,model/gltf-binary,model/gltf+json,application/octet-stream,application/json"
              value={modelUrl}
              upload={upload}
              uploadSource="element-editor:product-3d-variation-model"
              uploadDefaultStrategy="base64"
              showPreview={false}
              onValueChange={(value) => {
                const nextFormat = inferModelFormat(value, modelFormat);
                patchVariationProduct3D(form, path, {
                  model: value ? { ...(product3d.model ?? {}), url: value, format: nextFormat } : undefined,
                });
              }}
            />
            <SelectField
              compact
              label={t('elements.product3d.modelFormat', 'Model format')}
              value={modelFormat}
              options={[
                { value: 'glb', label: 'GLB' },
                { value: 'gltf', label: 'glTF' },
              ]}
              onValueChange={(value) =>
                patchVariationProduct3D(form, path, {
                  model: modelUrl ? { ...(product3d.model ?? {}), url: modelUrl, format: value } : undefined,
                })
              }
            />
          </FormGrid>
        </CompactEditorSection>

        <CompactEditorSection title={t('elements.product3d.colorTargetGroup', 'Color & target')} defaultOpen>
          <FormGrid columns={2} gap="sm" className="max-lg:ask-grid-cols-1">
            <TextField
              compact
              label={t('elements.product3d.materialTarget', 'Target mesh/material')}
              value={target}
              hint={isPartScoped ? t('elements.product3d.partMaterialTargetLocked', 'Locked to this detected 3D part.') : undefined}
              readOnly={isPartScoped}
              onValueChange={(value) => {
                if (isPartScoped) return;
                patchVariationProduct3D(form, path, { materialTarget: value || 'product' });
              }}
            />
            <ColorField compact label={t('common.color', 'Color')} value={String(material.color ?? '#d7dde8')} onValueChange={(value) => patchVariationMaterial(form, path, target, { color: value })} />
          </FormGrid>
        </CompactEditorSection>

        <CompactEditorSection title={t('elements.product3d.texturesFinishGroup', 'Textures & finish')}>
          <FormGrid columns={2} gap="sm" className="max-lg:ask-grid-cols-1">
            <ImageField
              compact
              fieldClassName="sm:ask-col-span-2"
              label={t('elements.product3d.textureImage', 'Texture image')}
              hint={t('elements.product3d.textureImageHint', 'Optional base color texture. Leave empty to preserve the texture embedded in the GLB. It is multiplied by the selected color.')}
              placeholder="https://…/texture.png"
              accept="image/*,.png,.jpg,.jpeg,.webp,.gif,.svg"
              value={String(material.textureUrl ?? '')}
              upload={upload}
              uploadSource="element-editor:product-3d-variation-texture"
              uploadDefaultStrategy="base64"
              showPreview
              onValueChange={(value) => patchVariationMaterial(form, path, target, { textureUrl: value })}
            />
            <ImageField
              compact
              fieldClassName="sm:ask-col-span-2"
              label={t('elements.product3d.normalTextureImage', 'Normal map')}
              hint={t('elements.product3d.normalTextureImageHint', 'Optional surface detail map. Leave empty to preserve the normal map embedded in the GLB.')}
              placeholder="https://…/normal.png"
              accept="image/*,.png,.jpg,.jpeg,.webp"
              value={String(material.normalTextureUrl ?? '')}
              upload={upload}
              uploadSource="element-editor:product-3d-variation-normal-texture"
              uploadDefaultStrategy="base64"
              showPreview
              onValueChange={(value) => patchVariationMaterial(form, path, target, { normalTextureUrl: value })}
            />
            <NumberField
              compact
              label={t('elements.product3d.textureRepeatX', 'Texture repeat X')}
              hint={t('elements.product3d.textureRepeatHint', 'Higher values make the texture smaller and repeat more often on the surface.')}
              value={readTextureRepeatValue(material, 'x')}
              step="any"
              min="0.01"
              max="100"
              onValueChange={(value) =>
                patchVariationMaterial(form, path, target, {
                  textureRepeatX: Math.max(0.01, Number(value ?? 1) || 1),
                })
              }
            />
            <NumberField
              compact
              label={t('elements.product3d.textureRepeatY', 'Texture repeat Y')}
              hint={t('elements.product3d.textureRepeatYHint', 'Use the same value as X for a uniform pattern, or adjust it independently.')}
              value={readTextureRepeatValue(material, 'y')}
              step="any"
              min="0.01"
              max="100"
              onValueChange={(value) =>
                patchVariationMaterial(form, path, target, {
                  textureRepeatY: Math.max(0.01, Number(value ?? 1) || 1),
                })
              }
            />
            <NumberField compact label={t('elements.product3d.roughness', 'Roughness')} value={Number(material.roughness ?? 0.55)} step="0.05" min="0" max="1" onValueChange={(value) => patchVariationMaterial(form, path, target, { roughness: Number(value ?? 0) })} />
            <NumberField compact label={t('elements.product3d.metallic', 'Metallic')} value={Number(material.metallic ?? 0.05)} step="0.05" min="0" max="1" onValueChange={(value) => patchVariationMaterial(form, path, target, { metallic: Number(value ?? 0) })} />
          </FormGrid>
        </CompactEditorSection>
      </div>
    </CompactEditorSection>
  );
};

export const ElementVariationTreeField: FunctionalComponent<ElementVariationTreeFieldProps> = ({ t, form, createDefaultVariation, getFormValue, upload }) => {
  const isProduct3D = isProduct3DForm(form);

  return (
    <SectionCard
      title={t('elements.form.variations', 'Variations')}
      description={isProduct3D ? t('elements.variations.product3dHelper', 'For Product 3D elements, each variation defines the product model override, preview, material color and texture used by the active 3D support.') : t('elements.variations.helper', 'Add one or more sellable variations. Mark a single active variation as the default leaf.')}
      actions={
        <Badge variant="surface" size="sm">
          {t('elements.variations.count', 'At least one variation required')}
        </Badge>
      }
    >
      {isProduct3D ? <ElementParametricVariationControlsField t={t} form={form} /> : null}

      <Form.Recursive
        label={t('elements.form.variations', 'Variations')}
        name="variations"
        childName="variations"
        minItems={isProduct3D ? 0 : 1}
        defaultItem={createDefaultVariation()}
        addLabel={t('common.add', 'Add')}
        collapsibleItems
        itemTitle={({ item, index, level }) => {
          const name = String(item?.name ?? '').trim() || `${level ? t('elements.variations.choice', 'Choice') : t('elements.form.variation', 'Variation')} ${index + 1}`;
          const childCount = Array.isArray(item?.variations) ? item.variations.length : 0;
          return (
            <span className="ask-flex ask-min-w-0 ask-items-center ask-gap-2">
              <span className="ask-min-w-0 ask-flex-1">
                <span className="ask-block ask-truncate ask-text-[12px] ask-font-semibold ask-text-textMain">{name}</span>
                <span className="ask-block ask-text-[10px] ask-text-textMuted">
                  {level ? t('elements.variations.nestedChoice', 'Nested choice') : t('elements.variations.rootChoice', 'Main variation')}
                  {childCount > 0 ? ` - ${childCount} ${childCount > 1 ? t('elements.variations.children', 'choices') : t('elements.variations.child', 'choice')}` : ''}
                </span>
              </span>
              {item?.active ? <span className="ask-shrink-0 ask-rounded-full ask-bg-primary/10 ask-px-2 ask-py-1 ask-text-[9px] ask-font-semibold ask-text-primary">{t('elements.form.active', 'Active')}</span> : null}
            </span>
          );
        }}
      >
        {({ path }) => {
          const isPartGroup = isProduct3D && isProduct3DPartGroupAtPath(form, path);
          const activeUniqueMode = isProduct3D && hasProduct3DPartGroups(form) ? 'singleRequired' : 'treeSingleRequired';
          const variationName = String(getFormValue(form.values, `${path}.name`) ?? '');

          if (!isProduct3D) {
            return (
              <FormGrid columns={2} className="ask-col-span-2 ask-rounded-2xl ask-border ask-border-borderSubtle ask-bg-surface ask-p-4 max-lg:ask-grid-cols-1">
                <TextField label={t('common.name', 'Name')} required value={variationName} onValueChange={(value) => form.setValue(`${path}.name`, value)} />
                <NumberField label={t('common.price', 'Price')} value={getFormValue(form.values, `${path}.price`) ?? 0} inputMode="decimal" emptyValue={0} onValueChange={(value) => form.setValue(`${path}.price`, value ?? 0)} />
                <ImageField
                  fieldClassName="ask-col-span-2 max-lg:ask-col-span-1"
                  label={t('common.image', 'Image')}
                  required
                  hint={t('elements.variations.imageHint', 'Used as this variation thumbnail and as its visual on the canvas.')}
                  value={String(getFormValue(form.values, `${path}.image`) ?? '').trim() || String(getFormValue(form.values, `${path}.icon`) ?? '').trim()}
                  placeholder="https://..."
                  upload={upload}
                  uploadSource="element-editor:variation-image"
                  uploadDefaultStrategy="base64"
                  showPreview
                  onValueChange={(value) => {
                    form.setValue(`${path}.image`, value);
                    form.setValue(`${path}.icon`, '');
                  }}
                />
                <div className="ask-col-span-2 max-lg:ask-col-span-1">
                  <div className="ask-inline-flex ask-items-center ask-gap-2 ask-rounded-full ask-border ask-border-borderSubtle ask-bg-surfaceAlt ask-px-3 ask-py-2">
                    <Form.Item unique={activeUniqueMode as any} name="active" inline>
                      <Checkbox label={t('elements.form.active', 'Active')} />
                    </Form.Item>
                  </div>
                </div>
              </FormGrid>
            );
          }

          return (
            <div className="ask-col-span-2 ask-flex ask-flex-col ask-gap-2 max-lg:ask-col-span-1">
              <CompactEditorSection title={t('elements.variations.detailsTitle', 'Variation details')} defaultOpen={!variationName.trim()}>
                <FormGrid columns={2} gap="sm" className="max-lg:ask-grid-cols-1">
                  <TextField compact label={isPartGroup ? t('elements.product3d.partLabel', 'Part label') : t('common.name', 'Name')} required value={variationName} onValueChange={(value) => form.setValue(`${path}.name`, value)} />

                  {isPartGroup ? (
                    <div className="ask-rounded-lg ask-border ask-border-borderSubtle ask-bg-surfaceAlt ask-p-2.5 ask-text-[10px] ask-leading-4 ask-text-textMuted">{t('elements.product3d.partGroupVariationHelper', 'This is a detected 3D part. Add or edit its material choices as child variations below using the normal variation form.')}</div>
                  ) : (
                    <NumberField compact label={t('common.price', 'Price')} value={getFormValue(form.values, `${path}.price`) ?? 0} inputMode="decimal" emptyValue={0} onValueChange={(value) => form.setValue(`${path}.price`, value ?? 0)} />
                  )}

                  <ImageField
                    compact
                    fieldClassName="ask-col-span-2 max-lg:ask-col-span-1"
                    label={t('elements.product3d.variationPreview', 'Preview image')}
                    hint={t('elements.product3d.variationPreviewHint', 'Used as the customer-facing thumbnail for this variation. Color or texture is shown when left empty.')}
                    value={String(getFormValue(form.values, `${path}.image`) ?? '').trim()
                      || String(getFormValue(form.values, `${path}.icon`) ?? '').trim()
                      || String(getFormValue(form.values, `${path}.product3d.preview.url`) ?? '').trim()
                      || String(getFormValue(form.values, `${path}.product3d.previewUrl`) ?? '').trim()}
                    placeholder="https://..."
                    accept="image/*,.png,.jpg,.jpeg,.webp,.gif,.svg"
                    upload={upload}
                    uploadSource="element-editor:product-3d-variation-preview"
                    uploadDefaultStrategy="base64"
                    showPreview
                    onValueChange={(value) => {
                      const product3d = readProduct3DAtPath(form, path);
                      form.setValue(`${path}.image`, value);
                      form.setValue(`${path}.icon`, '');
                      patchVariationProduct3D(form, path, {
                        previewUrl: value || undefined,
                        preview: value ? { ...(product3d.preview ?? {}), url: value } : undefined,
                      });
                    }}
                  />
                  {!isPartGroup ? (
                    <div className="ask-col-span-2 max-lg:ask-col-span-1">
                      <div className="ask-inline-flex ask-items-center ask-gap-2 ask-rounded-full ask-border ask-border-borderSubtle ask-bg-surfaceAlt ask-px-2.5 ask-py-1.5">
                        <Form.Item unique={activeUniqueMode as any} name="active" inline>
                          <Checkbox label={t('elements.form.active', 'Active')} />
                        </Form.Item>
                      </div>
                    </div>
                  ) : null}
                </FormGrid>
              </CompactEditorSection>

              <ElementVariationParametricScopeField t={t} form={form} path={path} />

              {!isPartGroup ? <Product3DVariationMaterialFields t={t} form={form} path={path} upload={upload} /> : null}
            </div>
          );
        }}
      </Form.Recursive>
    </SectionCard>
  );
};
