import { ASUKA_PARAMETRIC_VARIATION_PRODUCT_SCOPE } from '../types/parametric3d';
import type {
  AsukaParametricConstraint,
  AsukaParametricDefinition,
  AsukaParametricEvaluation,
  AsukaParametricExpression,
  AsukaParametricIssue,
  AsukaParametricParameter,
  AsukaParametricRuntimeMeasurements,
  AsukaParametricRuntimePartMeasurement,
  AsukaParametricValue,
  AsukaParametricVectorExpression,
  AsukaParametricVariationControls,
  AsukaParametricVariationScope,
} from '../types/parametric3d';

const clone = <T,>(value: T): T => {
  if (value == null) return value;
  try { return JSON.parse(JSON.stringify(value)) as T; } catch { return value; }
};

const isFiniteNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);

const getPath = (input: unknown, path: string): unknown => {
  const normalized = String(path ?? '').trim();
  if (!normalized) return undefined;
  const parts = normalized.split('.').filter(Boolean);
  let current: any = input;
  for (const part of parts) {
    if (current == null || (typeof current !== 'object' && !Array.isArray(current))) return undefined;
    current = current[part];
  }
  return current;
};

type Token = { type: 'number' | 'string' | 'identifier' | 'operator' | 'paren' | 'comma' | 'eof'; value: string };

const tokenize = (expression: string): Token[] => {
  const input = String(expression ?? '');
  const tokens: Token[] = [];
  let index = 0;
  const push = (type: Token['type'], value: string) => tokens.push({ type, value });
  while (index < input.length) {
    const char = input[index];
    if (/\s/.test(char)) { index += 1; continue; }
    if (char === '"' || char === "'") {
      const quote = char;
      index += 1;
      let value = '';
      while (index < input.length) {
        const next = input[index++];
        if (next === quote) break;
        if (next === '\\' && index < input.length) {
          const escaped = input[index++];
          value += escaped === 'n' ? '\n' : escaped === 't' ? '\t' : escaped;
        } else value += next;
      }
      push('string', value);
      continue;
    }
    if (/\d/.test(char) || (char === '.' && /\d/.test(input[index + 1] ?? ''))) {
      const start = index;
      index += 1;
      while (index < input.length && /[\d._eE+-]/.test(input[index])) {
        const current = input[index];
        const previous = input[index - 1];
        if ((current === '+' || current === '-') && previous !== 'e' && previous !== 'E') break;
        index += 1;
      }
      push('number', input.slice(start, index).replace(/_/g, ''));
      continue;
    }
    if (/[A-Za-z_$]/.test(char)) {
      const start = index;
      index += 1;
      while (index < input.length && /[A-Za-z0-9_$.]/.test(input[index])) index += 1;
      push('identifier', input.slice(start, index));
      continue;
    }
    const three = input.slice(index, index + 3);
    const two = input.slice(index, index + 2);
    if (['===', '!=='].includes(three)) { push('operator', three); index += 3; continue; }
    if (['&&', '||', '==', '!=', '<=', '>=', '**'].includes(two)) { push('operator', two); index += 2; continue; }
    if ('+-*/%<>!'.includes(char)) { push('operator', char); index += 1; continue; }
    if (char === '(' || char === ')') { push('paren', char); index += 1; continue; }
    if (char === ',') { push('comma', char); index += 1; continue; }
    throw new Error(`Unsupported token "${char}" in parametric expression.`);
  }
  tokens.push({ type: 'eof', value: '' });
  return tokens;
};

const FUNCTIONS: Record<string, (...args: number[]) => number> = {
  min: (...args) => Math.min(...args),
  max: (...args) => Math.max(...args),
  abs: (value) => Math.abs(value),
  round: (value) => Math.round(value),
  floor: (value) => Math.floor(value),
  ceil: (value) => Math.ceil(value),
  sqrt: (value) => Math.sqrt(value),
  pow: (value, power) => Math.pow(value, power),
  clamp: (value, min, max) => Math.max(min, Math.min(max, value)),
};

class ExpressionParser {
  private index = 0;
  constructor(private readonly tokens: Token[], private readonly context: Record<string, unknown>) {}
  private peek(): Token { return this.tokens[this.index] ?? { type: 'eof', value: '' }; }
  private take(): Token { return this.tokens[this.index++] ?? { type: 'eof', value: '' }; }
  private match(value: string): boolean {
    if (this.peek().value !== value) return false;
    this.index += 1;
    return true;
  }
  parse(): unknown {
    const value = this.parseOr();
    if (this.peek().type !== 'eof') throw new Error(`Unexpected token "${this.peek().value}".`);
    return value;
  }
  private parseOr(): unknown {
    let value = this.parseAnd();
    while (this.match('||')) {
      // Always parse the right-hand side so the token stream is consumed.
      // Using JavaScript's || directly here would short-circuit the parser.
      const right = this.parseAnd();
      value = Boolean(value) || Boolean(right);
    }
    return value;
  }
  private parseAnd(): unknown {
    let value = this.parseEquality();
    while (this.match('&&')) {
      // Always parse the right-hand side so expressions such as
      // `false && value > 0` remain syntactically valid.
      const right = this.parseEquality();
      value = Boolean(value) && Boolean(right);
    }
    return value;
  }
  private parseEquality(): unknown {
    let value = this.parseComparison();
    while (['==', '!=', '===', '!=='].includes(this.peek().value)) {
      const op = this.take().value;
      const right = this.parseComparison();
      if (op === '==') value = value == right; // intentional loose comparison for JSON values
      else if (op === '!=') value = value != right;
      else if (op === '===') value = value === right;
      else value = value !== right;
    }
    return value;
  }
  private parseComparison(): unknown {
    let value = this.parseAdditive();
    while (['<', '<=', '>', '>='].includes(this.peek().value)) {
      const op = this.take().value;
      const right = this.parseAdditive() as any;
      if (op === '<') value = (value as any) < right;
      else if (op === '<=') value = (value as any) <= right;
      else if (op === '>') value = (value as any) > right;
      else value = (value as any) >= right;
    }
    return value;
  }
  private parseAdditive(): unknown {
    let value: any = this.parseMultiplicative();
    while (this.peek().value === '+' || this.peek().value === '-') {
      const op = this.take().value;
      const right: any = this.parseMultiplicative();
      value = op === '+' ? value + right : Number(value) - Number(right);
    }
    return value;
  }
  private parseMultiplicative(): unknown {
    let value: unknown = this.parsePower();
    while (['*', '/', '%'].includes(this.peek().value)) {
      const op = this.take().value;
      const leftNumber = Number(value);
      const rightNumber = Number(this.parsePower());
      if (op === '*') value = leftNumber * rightNumber;
      else if (op === '/') value = leftNumber / rightNumber;
      else value = leftNumber % rightNumber;
    }
    return value;
  }
  private parsePower(): unknown {
    const value = this.parseUnary();
    if (this.match('**')) return Math.pow(Number(value), Number(this.parsePower()));
    return value;
  }
  private parseUnary(): unknown {
    if (this.match('!')) return !this.parseUnary();
    if (this.match('-')) return -Number(this.parseUnary());
    if (this.match('+')) return Number(this.parseUnary());
    return this.parsePrimary();
  }
  private parsePrimary(): unknown {
    const token = this.take();
    if (token.type === 'number') return Number(token.value);
    if (token.type === 'string') return token.value;
    if (token.type === 'paren' && token.value === '(') {
      const value = this.parseOr();
      if (!this.match(')')) throw new Error('Missing closing parenthesis.');
      return value;
    }
    if (token.type !== 'identifier') throw new Error(`Unexpected token "${token.value}".`);
    if (token.value === 'true') return true;
    if (token.value === 'false') return false;
    if (token.value === 'null') return null;
    if (this.match('(')) {
      const fn = FUNCTIONS[token.value];
      if (!fn) throw new Error(`Unknown parametric function "${token.value}".`);
      const args: number[] = [];
      if (!this.match(')')) {
        do { args.push(Number(this.parseOr())); } while (this.match(','));
        if (!this.match(')')) throw new Error('Missing closing function parenthesis.');
      }
      return fn(...args);
    }
    const resolved = getPath(this.context, token.value);
    if (resolved === undefined) throw new Error(`Unknown parametric value "${token.value}".`);
    return resolved;
  }
}

/** Evaluate one safe parametric expression without using eval/new Function. */
export const evaluateParametricExpression = (
  expression: AsukaParametricExpression,
  context: Record<string, unknown>,
): unknown => {
  if (typeof expression !== 'string') return expression;
  const source = expression.trim();
  if (!source) return '';
  return new ExpressionParser(tokenize(source), context).parse();
};

const resolveNumberBoundary = (
  boundary: number | AsukaParametricExpression | undefined,
  context: Record<string, unknown>,
): number | null => {
  if (boundary == null) return null;
  try {
    const value = Number(evaluateParametricExpression(boundary, context));
    return Number.isFinite(value) ? value : null;
  } catch { return null; }
};

export const normalizeParametricParameterValue = (
  parameter: AsukaParametricParameter,
  input: unknown,
  context: Record<string, unknown> = {},
): AsukaParametricValue => {
  if (parameter.type === 'boolean') return input === true || input === 1 || String(input).toLowerCase() === 'true';
  if (parameter.type === 'select') {
    const options = Array.isArray(parameter.options) ? parameter.options : [];
    const candidate = options.find((option) => String(option.value) === String(input));
    return (candidate?.value ?? parameter.default ?? options[0]?.value ?? null) as AsukaParametricValue;
  }
  let value = Number(input ?? parameter.default ?? 0);
  if (!Number.isFinite(value)) value = Number(parameter.default ?? 0) || 0;
  if (parameter.type === 'integer') value = Math.round(value);
  const min = resolveNumberBoundary(parameter.min, context);
  const max = resolveNumberBoundary(parameter.max, context);
  if (min != null) value = Math.max(min, value);
  if (max != null) value = Math.min(max, value);
  const step = Number(parameter.step);
  if (Number.isFinite(step) && step > 0) {
    const origin = min ?? 0;
    value = origin + Math.round((value - origin) / step) * step;
    const precision = Math.max(0, String(step).split('.')[1]?.length ?? 0);
    value = Number(value.toFixed(Math.min(10, precision + 2)));
  }
  return value;
};

export const createParametricDefaultValues = (
  definition: AsukaParametricDefinition | null | undefined,
): Record<string, AsukaParametricValue> => {
  const out: Record<string, AsukaParametricValue> = {};
  for (const parameter of definition?.parameters ?? []) {
    const id = String(parameter?.id ?? '').trim();
    if (!id) continue;
    out[id] = normalizeParametricParameterValue(parameter, parameter.default, out);
  }
  return out;
};

const buildMeasurementContext = (measurements?: AsukaParametricRuntimeMeasurements | null): Record<string, unknown> => ({
  product: clone(measurements?.product ?? { width: 0, height: 0, depth: 0 }),
  part: clone(measurements?.parts ?? {}),
});

const evaluateComputedValues = (
  definition: AsukaParametricDefinition,
  context: Record<string, unknown>,
): Record<string, AsukaParametricValue> => {
  const computed: Record<string, AsukaParametricValue> = {};
  const entries = Object.entries(definition.computedValues ?? {});
  // A few passes allow computed values to depend on earlier/later declarations while
  // still rejecting cycles as unresolved values instead of executing arbitrary code.
  for (let pass = 0; pass < Math.max(1, entries.length + 1); pass += 1) {
    let changed = false;
    for (const [id, expression] of entries) {
      try {
        const value = evaluateParametricExpression(expression, { ...context, ...computed, computed });
        if (value !== undefined && computed[id] !== value) {
          computed[id] = value as AsukaParametricValue;
          changed = true;
        }
      } catch {}
    }
    if (!changed) break;
  }
  return computed;
};

const evaluateConstraints = (
  constraints: AsukaParametricConstraint[] | undefined,
  context: Record<string, unknown>,
): AsukaParametricIssue[] => {
  const issues: AsukaParametricIssue[] = [];
  for (const constraint of constraints ?? []) {
    let valid = false;
    try { valid = Boolean(evaluateParametricExpression(constraint.expression, context)); } catch { valid = false; }
    if (valid) continue;
    issues.push({
      id: constraint.id,
      severity: constraint.severity === 'warning' ? 'warning' : 'error',
      message: String(constraint.message ?? 'Invalid parameter combination.'),
      parameters: Array.isArray(constraint.parameters) ? [...constraint.parameters] : undefined,
    });
  }
  return issues;
};

/** Resolve defaults, normalize values, compute dependencies and validate constraints. */
export const evaluateParametricDefinition = (
  definition: AsukaParametricDefinition,
  inputValues: Record<string, unknown> = {},
  measurements?: AsukaParametricRuntimeMeasurements | null,
): AsukaParametricEvaluation => {
  if (!definition || definition.schemaVersion !== 1) throw new TypeError('parametric.schemaVersion must be 1.');
  const values: Record<string, AsukaParametricValue> = {};
  const baseContext = buildMeasurementContext(measurements);
  for (const parameter of definition.parameters ?? []) {
    const id = String(parameter?.id ?? '').trim();
    if (!id) continue;
    values[id] = normalizeParametricParameterValue(
      parameter,
      Object.prototype.hasOwnProperty.call(inputValues, id) ? inputValues[id] : parameter.default,
      { ...baseContext, ...values },
    );
  }
  const computedValues = evaluateComputedValues(definition, { ...baseContext, ...values, values });
  const context: Record<string, unknown> = {
    ...baseContext,
    ...values,
    ...computedValues,
    values,
    computed: computedValues,
  };
  const issues = evaluateConstraints(definition.constraints, context);
  return { values, computedValues, context, issues, valid: !issues.some((issue) => issue.severity === 'error') };
};

export const isParametricExpressionTruthy = (
  expression: AsukaParametricExpression | undefined,
  context: Record<string, unknown>,
  fallback = true,
): boolean => {
  if (expression == null || expression === '') return fallback;
  try { return Boolean(evaluateParametricExpression(expression, context)); } catch { return fallback; }
};

export const resolveParametricNumber = (
  expression: AsukaParametricExpression | undefined,
  context: Record<string, unknown>,
  fallback: number,
): number => {
  if (expression == null || expression === '') return fallback;
  try {
    const value = Number(evaluateParametricExpression(expression, context));
    return isFiniteNumber(value) ? value : fallback;
  } catch { return fallback; }
};

export type AsukaParametricAxis = 'x' | 'y' | 'z';
export type AsukaParametricVector = Record<AsukaParametricAxis, number>;

const PARAMETRIC_ATTACHMENT_AXES: Array<{
  axis: AsukaParametricAxis;
  size: 'width' | 'height' | 'depth';
  center: 'centerX' | 'centerY' | 'centerZ';
}> = [
  { axis: 'x', size: 'width', center: 'centerX' },
  { axis: 'y', size: 'height', center: 'centerY' },
  { axis: 'z', size: 'depth', center: 'centerZ' },
];

const normalizeParametricAnchor = (value: number): number => Math.max(-0.5, Math.min(0.5, value));

const inferParametricAttachmentAnchors = (
  support: AsukaParametricRuntimePartMeasurement,
  target: AsukaParametricRuntimePartMeasurement,
  axis: typeof PARAMETRIC_ATTACHMENT_AXES[number],
): { support: number; target: number } => {
  const supportSize = Math.max(0, Number(support?.[axis.size]) || 0);
  const targetSize = Math.max(0, Number(target?.[axis.size]) || 0);
  const supportCenter = Number(support?.[axis.center]) || 0;
  const targetCenter = Number(target?.[axis.center]) || 0;
  if (supportSize <= 0.000001) return { support: 0, target: 0 };

  const delta = targetCenter - supportCenter;
  const normalized = normalizeParametricAnchor(delta / supportSize);
  const supportHalf = supportSize * 0.5;
  const targetHalf = targetSize * 0.5;
  const outsideTolerance = Math.max(0.0001, Math.min(supportHalf, targetHalf) * 0.2);

  if (targetCenter - targetHalf >= supportCenter + supportHalf - outsideTolerance) {
    return { support: 0.5, target: -0.5 };
  }
  if (targetCenter + targetHalf <= supportCenter - supportHalf + outsideTolerance) {
    return { support: -0.5, target: 0.5 };
  }
  return { support: normalized, target: 0 };
};

const resolveParametricAnchorValue = (
  expression: AsukaParametricExpression | undefined,
  context: Record<string, unknown>,
  inferred: number,
): number => {
  const keyword = typeof expression === 'string' ? expression.trim().toLowerCase() : '';
  if (!keyword || keyword === 'auto') return inferred;
  if (keyword === 'min') return -0.5;
  if (keyword === 'center') return 0;
  if (keyword === 'max') return 0.5;
  return normalizeParametricAnchor(resolveParametricNumber(expression, context, inferred));
};

/**
 * Resolve the world offset that keeps a target part attached to a resized support.
 * The result is baseline-relative and can therefore be safely recomputed on every
 * parameter mutation without accumulating transforms.
 */
export const resolveParametricAttachmentOffset = (input: {
  support: AsukaParametricRuntimePartMeasurement;
  target: AsukaParametricRuntimePartMeasurement;
  supportSize?: Partial<AsukaParametricVector>;
  targetSize?: Partial<AsukaParametricVector>;
  supportOffset?: Partial<AsukaParametricVector>;
  supportAnchor?: AsukaParametricVectorExpression;
  targetAnchor?: AsukaParametricVectorExpression;
  positionOffset?: AsukaParametricVectorExpression;
  context?: Record<string, unknown>;
}): AsukaParametricVector => {
  const context = input.context ?? {};
  const offset: AsukaParametricVector = { x: 0, y: 0, z: 0 };
  for (const axis of PARAMETRIC_ATTACHMENT_AXES) {
    const inferred = inferParametricAttachmentAnchors(input.support, input.target, axis);
    const supportAnchor = resolveParametricAnchorValue(input.supportAnchor?.[axis.axis], context, inferred.support);
    const targetAnchor = resolveParametricAnchorValue(input.targetAnchor?.[axis.axis], context, inferred.target);
    const supportBaselineSize = Math.max(0, Number(input.support?.[axis.size]) || 0);
    const targetBaselineSize = Math.max(0, Number(input.target?.[axis.size]) || 0);
    const supportSize = Math.max(0, Number(input.supportSize?.[axis.axis] ?? supportBaselineSize) || 0);
    const targetSize = Math.max(0, Number(input.targetSize?.[axis.axis] ?? targetBaselineSize) || 0);
    offset[axis.axis] = (Number(input.supportOffset?.[axis.axis]) || 0)
      + supportAnchor * (supportSize - supportBaselineSize)
      - targetAnchor * (targetSize - targetBaselineSize)
      + resolveParametricNumber(input.positionOffset?.[axis.axis], context, 0);
  }
  return offset;
};


/** Normalize one contextual target so ids, labels and slug variants compare consistently. */
export const normalizeParametricVariationScopeTarget = (value: unknown): string => String(value ?? '')
  .trim()
  .toLowerCase()
  .normalize('NFD')
  .replace(/[\u0300-\u036f]/g, '')
  .replace(/[^a-z0-9@]+/g, '');

export type ResolvedParametricVariationControls = Required<Pick<AsukaParametricVariationControls, 'enabled' | 'placement' | 'scopes' | 'compact'>>
  & Pick<AsukaParametricVariationControls, 'title' | 'description'>;

export type ParametricVariationScopeContext = {
  /** True while the Variations browser is displaying the product/element root. */
  productRoot?: boolean;
  /** Stable Product 3D part id emitted by Babylon or stored on the variation group. */
  partId?: string;
  /** Human label used as a resilient secondary alias. */
  partLabel?: string;
  /** Additional ids, labels, mesh/material ids or semantic aliases. */
  aliases?: string[];
};

export type ResolvedParametricVariationScope = ResolvedParametricVariationControls & {
  scopeTarget: string;
  parameterIds: string[];
  scopeTitle?: string;
  scopeDescription?: string;
};

/** Normalize the opt-in Variations presentation without leaking product-specific semantics into the core. */
export const resolveParametricVariationControls = (
  definition: AsukaParametricDefinition | null | undefined,
): ResolvedParametricVariationControls | null => {
  if (!definition || definition.schemaVersion !== 1) return null;
  const controls = definition.ui?.variationControls;
  if (controls?.enabled !== true) return null;
  const validIds = new Set((definition.parameters ?? []).map((parameter) => String(parameter?.id ?? '').trim()).filter(Boolean));
  const merged = new Map<string, AsukaParametricVariationScope>();
  for (const rawScope of Array.isArray(controls.scopes) ? controls.scopes : []) {
    const target = String(rawScope?.target ?? '').trim();
    const targetKey = normalizeParametricVariationScopeTarget(target);
    if (!target || !targetKey) continue;
    const parameterIds = Array.from(new Set((Array.isArray(rawScope?.parameterIds) ? rawScope.parameterIds : [])
      .map((id) => String(id ?? '').trim())
      .filter((id) => validIds.has(id))));
    const previous = merged.get(targetKey);
    merged.set(targetKey, {
      target: previous?.target || target,
      parameterIds: Array.from(new Set([...(previous?.parameterIds ?? []), ...parameterIds])),
      title: String(previous?.title ?? rawScope?.title ?? '').trim() || undefined,
      description: String(previous?.description ?? rawScope?.description ?? '').trim() || undefined,
    });
  }
  return {
    enabled: true,
    placement: controls.placement === 'after' ? 'after' : 'before',
    scopes: Array.from(merged.values()),
    compact: controls.compact !== false,
    title: String(controls.title ?? '').trim() || undefined,
    description: String(controls.description ?? '').trim() || undefined,
  };
};

/** Resolve only the parameters assigned to the currently displayed product/part context. */
export const resolveParametricVariationScope = (
  definition: AsukaParametricDefinition | null | undefined,
  context: ParametricVariationScopeContext = {},
): ResolvedParametricVariationScope | null => {
  const controls = resolveParametricVariationControls(definition);
  if (!controls) return null;
  const candidates = context.productRoot
    ? [ASUKA_PARAMETRIC_VARIATION_PRODUCT_SCOPE]
    : [context.partId, context.partLabel, ...(Array.isArray(context.aliases) ? context.aliases : [])];
  const candidateKeys = new Set(candidates.map(normalizeParametricVariationScopeTarget).filter(Boolean));
  if (!candidateKeys.size) return null;
  const scope = controls.scopes.find((entry) => candidateKeys.has(normalizeParametricVariationScopeTarget(entry.target)));
  if (!scope || !scope.parameterIds.length) return null;
  const partTitle = String(context.partLabel ?? context.partId ?? '').trim();
  const isProductScope = normalizeParametricVariationScopeTarget(scope.target)
    === normalizeParametricVariationScopeTarget(ASUKA_PARAMETRIC_VARIATION_PRODUCT_SCOPE);
  return {
    ...controls,
    scopeTarget: scope.target,
    parameterIds: scope.parameterIds,
    scopeTitle: scope.title || (isProductScope ? controls.title : (partTitle ? `${partTitle} · Parameters` : controls.title)),
    scopeDescription: scope.description || (isProductScope ? controls.description : undefined),
  };
};
