import {
  convertPhysicalLength,
  type AsukaPhysicalUnit,
} from './physicalMeasurement';
import { resolveParametricNumber } from './parametric3d';
import type { AsukaAssemblyMeasurementSettings } from '../types/assembly3d';
import type {
  AsukaParametricDefinition,
  AsukaParametricLengthDimension,
  AsukaParametricParameter,
  AsukaParametricParameterMeasurement,
  AsukaParametricRule,
} from '../types/parametric3d';

export interface AsukaParametricNumberPresentation {
  measured: boolean;
  value: number;
  min?: number;
  max?: number;
  step: number;
  unit?: string;
  toStoredValue: (displayValue: number) => number;
  toDisplayValue: (storedValue: number) => number;
}

const finite = (value: unknown, fallback = 0): number => {
  const number = Number(value);
  return Number.isFinite(number) ? number : fallback;
};

const positive = (value: unknown, fallback: number): number => {
  const number = Number(value);
  return Number.isFinite(number) && number > 0 ? number : fallback;
};

const normalizeLengthUnit = (value: unknown): AsukaPhysicalUnit => (
  value === 'm' || value === 'mm' || value === 'in' ? value : 'cm'
);

const resolveDimensionMeasurement = (
  context: Record<string, unknown>,
  target: string | undefined,
  dimension: AsukaParametricLengthDimension,
): number => {
  const partMeasurements = context?.part && typeof context.part === 'object'
    ? context.part as Record<string, Record<string, unknown>>
    : {};
  const normalizedTarget = String(target ?? '').trim();
  const source = normalizedTarget && normalizedTarget !== '@product'
    ? partMeasurements[normalizedTarget]
    : (
      (normalizedTarget ? partMeasurements[normalizedTarget] : undefined)
      ?? (context?.product && typeof context.product === 'object'
        ? context.product as Record<string, unknown>
        : undefined)
    );
  return Math.max(0, finite(source?.[dimension]));
};

const resolveMeasurement = (
  parameter: AsukaParametricParameter,
): AsukaParametricParameterMeasurement | null => {
  const measurement = parameter.measurement;
  if (!measurement || measurement.kind !== 'length') return null;
  if (!['width', 'height', 'depth'].includes(measurement.dimension)) return null;
  return measurement;
};

const compactExpression = (value: unknown): string => (
  typeof value === 'string' ? value.replace(/\s+/g, '') : ''
);

const expressionIsParameter = (value: unknown, parameterId: string): boolean => (
  compactExpression(value) === parameterId
);

const expressionIsPercentParameter = (value: unknown, parameterId: string): boolean => {
  const expression = compactExpression(value);
  return expression === `${parameterId}/100`
    || expression === `${parameterId}*0.01`
    || expression === `0.01*${parameterId}`;
};

const inferMeasurementFromRule = (
  rule: AsukaParametricRule,
  parameterId: string,
): AsukaParametricParameterMeasurement | null => {
  const axes: Array<{
    axis: 'x' | 'y' | 'z';
    dimension: AsukaParametricLengthDimension;
  }> = [
    { axis: 'x', dimension: 'width' },
    { axis: 'y', dimension: 'height' },
    { axis: 'z', dimension: 'depth' },
  ];

  if (rule.type === 'resize') {
    const match = axes.find(({ axis }) => expressionIsParameter(rule.size?.[axis], parameterId));
    return match
      ? { kind: 'length', dimension: match.dimension, target: rule.target, storage: 'world-unit' }
      : null;
  }
  if (rule.type !== 'transform') return null;

  const scaleMatch = axes.find(({ axis }) => expressionIsPercentParameter(rule.scale?.[axis], parameterId));
  if (scaleMatch) {
    return {
      kind: 'length',
      dimension: scaleMatch.dimension,
      target: rule.target,
      storage: 'percent',
    };
  }
  const offsetMatch = axes.find(({ axis }) => expressionIsParameter(rule.positionOffset?.[axis], parameterId));
  return offsetMatch
    ? {
        kind: 'length',
        dimension: offsetMatch.dimension,
        target: rule.target,
        storage: 'world-unit',
      }
    : null;
};

/**
 * Decorate legacy recipes at runtime. Their persisted JSON stays untouched,
 * while newly generated and older products share the same physical controls.
 */
export const withInferredParametricMeasurements = (
  definition: AsukaParametricDefinition,
): AsukaParametricDefinition => {
  let changed = false;
  const parameters = definition.parameters.map((parameter) => {
    if (parameter.measurement || (parameter.type !== 'number' && parameter.type !== 'integer')) return parameter;
    const measurement = definition.rules
      .map((rule) => inferMeasurementFromRule(rule, parameter.id))
      .find(Boolean);
    if (!measurement) return parameter;
    changed = true;
    return { ...parameter, measurement };
  });
  return changed ? { ...definition, parameters } : definition;
};

const worldToDisplay = (
  value: number,
  settings: AsukaAssemblyMeasurementSettings | null | undefined,
): number => {
  const worldUnitInMeters = positive(settings?.worldUnitInMeters, 1);
  const displayUnit = normalizeLengthUnit(settings?.displayUnit);
  return convertPhysicalLength(value * worldUnitInMeters, 'm', displayUnit);
};

const displayToWorld = (
  value: number,
  settings: AsukaAssemblyMeasurementSettings | null | undefined,
): number => {
  const worldUnitInMeters = positive(settings?.worldUnitInMeters, 1);
  const displayUnit = normalizeLengthUnit(settings?.displayUnit);
  return convertPhysicalLength(value, displayUnit, 'm') / worldUnitInMeters;
};

const getDefaultStep = (parameter: AsukaParametricParameter): number => (
  parameter.type === 'integer' ? 1 : 0.01
);

/**
 * Resolve one numeric recipe parameter into the common physical measurement
 * system used by Assembly and the product variation editors.
 */
export const resolveParametricNumberPresentation = (
  parameter: AsukaParametricParameter,
  storedValueInput: unknown,
  context: Record<string, unknown>,
  settings?: AsukaAssemblyMeasurementSettings | null,
): AsukaParametricNumberPresentation => {
  const storedValue = finite(storedValueInput, finite(parameter.default));
  const rawMin = resolveParametricNumber(parameter.min, context, Number.NEGATIVE_INFINITY);
  const rawMax = resolveParametricNumber(parameter.max, context, Number.POSITIVE_INFINITY);
  const rawStep = positive(parameter.step, getDefaultStep(parameter));
  const measurement = resolveMeasurement(parameter);

  if (!measurement) {
    return {
      measured: false,
      value: storedValue,
      ...(Number.isFinite(rawMin) ? { min: rawMin } : {}),
      ...(Number.isFinite(rawMax) ? { max: rawMax } : {}),
      step: rawStep,
      unit: parameter.unit,
      toStoredValue: (value) => finite(value),
      toDisplayValue: (value) => finite(value),
    };
  }

  const storage = measurement.storage
    ?? (parameter.unit === '%' ? 'percent' : 'world-unit');
  const baseline = resolveDimensionMeasurement(context, measurement.target, measurement.dimension);
  const sourceUnit = normalizeLengthUnit(measurement.unit);
  const displayUnit = normalizeLengthUnit(settings?.displayUnit);
  let toDisplayValue: (value: number) => number;
  let toStoredValue: (value: number) => number;

  if (storage === 'percent') {
    if (baseline <= 0.0000001) {
      return {
        measured: false,
        value: storedValue,
        ...(Number.isFinite(rawMin) ? { min: rawMin } : {}),
        ...(Number.isFinite(rawMax) ? { max: rawMax } : {}),
        step: rawStep,
        unit: parameter.unit,
        toStoredValue: (value) => finite(value),
        toDisplayValue: (value) => finite(value),
      };
    }
    toDisplayValue = (value) => worldToDisplay(baseline * finite(value) / 100, settings);
    toStoredValue = (value) => displayToWorld(finite(value), settings) / baseline * 100;
  } else if (storage === 'physical') {
    toDisplayValue = (value) => convertPhysicalLength(finite(value), sourceUnit, displayUnit);
    toStoredValue = (value) => convertPhysicalLength(finite(value), displayUnit, sourceUnit);
  } else {
    toDisplayValue = (value) => worldToDisplay(finite(value), settings);
    toStoredValue = (value) => displayToWorld(finite(value), settings);
  }

  const convertedStep = Math.abs(toDisplayValue(rawStep) - toDisplayValue(0));
  return {
    measured: true,
    value: toDisplayValue(storedValue),
    ...(Number.isFinite(rawMin) ? { min: toDisplayValue(rawMin) } : {}),
    ...(Number.isFinite(rawMax) ? { max: toDisplayValue(rawMax) } : {}),
    step: positive(convertedStep, displayUnit === 'mm' ? 1 : 0.01),
    unit: displayUnit,
    toStoredValue,
    toDisplayValue,
  };
};

export const formatParametricDisplayNumber = (value: unknown, stepInput = 0.01): string => {
  const number = finite(value);
  const step = positive(stepInput, 0.01);
  const decimals = step >= 1
    ? 2
    : Math.min(6, Math.max(2, Math.ceil(-Math.log10(step)) + 1));
  return String(Number(number.toFixed(decimals)));
};
