import { isFdiTooth, toFdi } from '../../common/fdi';
import type { ToothIntent, UnresolvedItem } from './voice.types';

export type ToothResolution = {
  /** Unique FDI codes, sorted (matching normalizeTeeth's ordering). */
  teeth: string[];
  unresolved: UnresolvedItem[];
};

/** Everything here parses untrusted model output, so nothing may throw. */
function normalizedFdi(intent: ToothIntent): string {
  const raw = (intent as { fdi?: unknown }).fdi;
  // Trimmed for parity with normalizeTeeth — '14 ' is tooth 14 through the treatment API
  // and must not be "malformed" here.
  return typeof raw === 'string' ? raw.trim() : '';
}

/**
 * Resolve one spoken tooth reference to an FDI code, or null.
 *
 * Never guesses and never clamps: a position of 9, a deciduous tooth, or a malformed
 * intent resolves to null so the caller can surface it as "not understood" rather than
 * silently selecting a neighbouring tooth.
 */
export function resolveToothIntent(intent: ToothIntent): string | null {
  if (!intent || typeof intent !== 'object') return null;

  if (intent.kind === 'explicit') {
    const fdi = normalizedFdi(intent);
    return isFdiTooth(fdi) ? fdi : null;
  }

  if (intent.kind === 'positional') {
    if (intent.arch !== 'upper' && intent.arch !== 'lower') return null;
    if (intent.side !== 'patient_right' && intent.side !== 'patient_left')
      return null;
    return toFdi(intent.arch, intent.side, intent.position);
  }

  return null;
}

function unresolvedReason(intent: ToothIntent): UnresolvedItem['reason'] {
  if (!intent || typeof intent !== 'object') return 'malformed';

  if (intent.kind === 'explicit') {
    const fdi = normalizedFdi(intent);
    // Quadrants 1-4 are permanent and would already have resolved, so a well-formed
    // quadrant+position reaching here is quadrant 5-8: deciduous. Anything else is noise.
    return /^[1-8][1-8]$/.test(fdi) ? 'not_permanent_tooth' : 'malformed';
  }

  if (intent.kind === 'positional') {
    const positionBad =
      !Number.isInteger(intent.position) ||
      intent.position < 1 ||
      intent.position > 8;
    return positionBad ? 'position_out_of_range' : 'malformed';
  }

  return 'malformed';
}

function spokenOf(intent: ToothIntent): string {
  const spoken = (intent as { spoken?: unknown })?.spoken;
  return typeof spoken === 'string' && spoken.trim() ? spoken.trim() : '';
}

/**
 * Resolve a list of spoken tooth references.
 *
 * Duplicates collapse — a clinician may name the same tooth twice in one sentence — and
 * anything unresolvable is reported rather than dropped, so the review sheet can show the
 * user exactly which words were not understood.
 */
export function resolveToothIntents(
  intents: readonly ToothIntent[],
): ToothResolution {
  const teeth = new Set<string>();
  const unresolved: UnresolvedItem[] = [];
  const seenUnresolved = new Set<string>();

  // Not `intents ?? []`: a model may return an object or a number here, and a
  // non-iterable must degrade like any other malformed shape rather than throw.
  const list: readonly ToothIntent[] = Array.isArray(intents)
    ? (intents as readonly ToothIntent[])
    : [];

  for (const intent of list) {
    const fdi = resolveToothIntent(intent);
    if (fdi) {
      teeth.add(fdi);
      continue;
    }
    const reason = unresolvedReason(intent);
    const spoken = spokenOf(intent);
    // Only dedupe items we can actually tell apart. Without `spoken`, two distinct lost
    // references would collapse into one blank review row and a tooth would vanish.
    if (spoken) {
      const key = `${spoken}::${reason}`;
      if (seenUnresolved.has(key)) continue;
      seenUnresolved.add(key);
    }
    unresolved.push({ spoken, reason });
  }

  return { teeth: [...teeth].sort(), unresolved };
}
