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

const ARCHES: readonly Arch[] = ['upper', 'lower'];
const SIDES: readonly PatientSide[] = ['patient_right', 'patient_left'];

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 {
  // The same normalisation the wire layer used to pick this branch, so the two agree.
  return normalizeFdiCode((intent as { fdi?: unknown }).fdi);
}

/**
 * Never guesses and never clamps: position 9, a deciduous tooth or a malformed intent all
 * resolve to null, so the caller surfaces "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;
    if (positionBad) return 'position_out_of_range';
    // The position was understood; the quadrant was never said. "دندون دو" names four
    // teeth, so "could not be read" would send the clinician after the wrong fault.
    const archMissing = intent.arch !== 'upper' && intent.arch !== 'lower';
    const sideMissing =
      intent.side !== 'patient_right' && intent.side !== 'patient_left';
    return archMissing || sideMissing ? 'tooth_missing_quadrant' : 'malformed';
  }

  return 'malformed';
}

/**
 * The teeth still consistent with what *was* heard — "دو" leaves four, "دو بالا" two. Not
 * a guess: the full set of readings, for the clinician to choose from.
 */
function quadrantCandidates(intent: ToothIntent): string[] {
  if (intent.kind !== 'positional') return [];
  const arches =
    intent.arch === 'upper' || intent.arch === 'lower' ? [intent.arch] : ARCHES;
  const sides =
    intent.side === 'patient_right' || intent.side === 'patient_left'
      ? [intent.side]
      : SIDES;

  const codes: string[] = [];
  for (const arch of arches) {
    for (const side of sides) {
      const fdi = toFdi(arch, side, intent.position);
      if (fdi) codes.push(fdi);
    }
  }
  return codes.sort();
}

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

/**
 * Duplicates collapse; anything unresolvable is reported rather than dropped, so the sheet
 * can show 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);
    const candidates =
      reason === 'tooth_missing_quadrant' ? quadrantCandidates(intent) : [];
    // Only dedupe what we can tell apart: without `spoken`, two lost references collapse
    // into one blank row and a tooth vanishes. Candidates are part of the identity.
    if (spoken) {
      const key = `${spoken}::${reason}::${candidates.join(',')}`;
      if (seenUnresolved.has(key)) continue;
      seenUnresolved.add(key);
    }
    unresolved.push(
      candidates.length > 0
        ? { spoken, reason, candidates }
        : { spoken, reason },
    );
  }

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