/**
 * FDI tooth geometry — permanent dentition only.
 *
 * Mirrors `frontend/src/components/treatment/fdiToothMeta.ts` and the adjacency rules in
 * `toothSelectionGroups.ts`. Adjacency is defined by position in the arch order, so the
 * midline pairs (11–21, 41–31) are neighbours, exactly as the chart treats them.
 */

import { toLatinDigits } from './digits';

export type Arch = 'upper' | 'lower';

/** Which side of the *patient*, not of the screen. Quadrant 1 is the patient's upper right. */
export type PatientSide = 'patient_right' | 'patient_left';

/**
 * Upper arch in chart order: patient's RIGHT (18) → midline → patient's LEFT (28) — the drawn
 * layout, which mirrors the patient's own sides. Never read a position off this array by
 * index; use `toFdi()`, which owns the side convention.
 */
export const FDI_UPPER_ARCH_ORDER = [
  '18',
  '17',
  '16',
  '15',
  '14',
  '13',
  '12',
  '11',
  '21',
  '22',
  '23',
  '24',
  '25',
  '26',
  '27',
  '28',
] as const;

/** Lower arch, same chart ordering: patient's RIGHT (48) → midline → patient's LEFT (38). */
export const FDI_LOWER_ARCH_ORDER = [
  '48',
  '47',
  '46',
  '45',
  '44',
  '43',
  '42',
  '41',
  '31',
  '32',
  '33',
  '34',
  '35',
  '36',
  '37',
  '38',
] as const;

export const FDI_TOOTH_IDS: ReadonlySet<string> = new Set<string>([
  ...FDI_UPPER_ARCH_ORDER,
  ...FDI_LOWER_ARCH_ORDER,
]);

export function isFdiTooth(value: unknown): value is string {
  return typeof value === 'string' && FDI_TOOTH_IDS.has(value);
}

/**
 * Clean up a tooth code the model echoed back. It is reading Persian speech, so it can hand
 * back "۲۶" or "2 6" from digit-by-digit dictation; neither matches literally, and the
 * near-miss does not fail loudly — the tooth just turns into "not understood".
 */
export function normalizeFdiCode(value: unknown): string {
  if (typeof value !== 'string') return '';
  return toLatinDigits(value).replace(/\s+/g, '');
}

function archOrder(tooth: string): readonly string[] | null {
  if ((FDI_UPPER_ARCH_ORDER as readonly string[]).includes(tooth))
    return FDI_UPPER_ARCH_ORDER;
  if ((FDI_LOWER_ARCH_ORDER as readonly string[]).includes(tooth))
    return FDI_LOWER_ARCH_ORDER;
  return null;
}

export function sameArch(a: string, b: string): boolean {
  const archA = archOrder(a);
  const archB = archOrder(b);
  return Boolean(archA && archB && archA === archB);
}

export function areArchNeighbors(a: string, b: string): boolean {
  const arch = archOrder(a);
  if (!arch || !sameArch(a, b)) return false;
  return Math.abs(arch.indexOf(a) - arch.indexOf(b)) === 1;
}

/** Inclusive span between two teeth of the same arch, in arch order. Null if not comparable. */
export function teethBetweenInclusive(a: string, b: string): string[] | null {
  const arch = archOrder(a);
  if (!arch || !sameArch(a, b)) return null;
  const i = arch.indexOf(a);
  const j = arch.indexOf(b);
  if (i < 0 || j < 0) return null;
  const [from, to] = i <= j ? [i, j] : [j, i];
  return [...arch.slice(from, to + 1)];
}

/**
 * Arch + patient side + position (1 = central incisor … 8 = third molar) → FDI code.
 *
 * The single place the patient-right convention lives. Getting it backwards mirrors every
 * quadrant into a valid-looking code for the wrong tooth, which no schema check can catch.
 */
export function toFdi(
  arch: Arch,
  side: PatientSide,
  position: number,
): string | null {
  if (!Number.isInteger(position) || position < 1 || position > 8) return null;
  let quadrant: number;
  if (arch === 'upper') {
    quadrant = side === 'patient_right' ? 1 : 2;
  } else {
    quadrant = side === 'patient_left' ? 3 : 4;
  }
  const code = `${quadrant}${position}`;
  return FDI_TOOTH_IDS.has(code) ? code : null;
}

/**
 * Along the arch, not lexically — a bridge reads 16-15-14, and 11 sits beside 21 across the
 * midline. Teeth from another arch sort to the end, stably.
 */
export function sortInArchOrder(teeth: readonly string[]): string[] {
  if (teeth.length === 0) return [];
  const arch = teeth.map((t) => archOrder(t)).find((a) => a !== null) ?? null;
  if (!arch) return [...teeth];
  const indexOf = (tooth: string) => {
    const i = arch.indexOf(tooth);
    return i === -1 ? Number.MAX_SAFE_INTEGER : i;
  };
  return [...teeth].sort((a, b) => indexOf(a) - indexOf(b));
}
