/**
 * Compress a list of mooring numbers into a compact range string used
 * inside the Documenso EOI template (plan §4.6).
 *
 *   formatBerthRange([])                                  -> ''
 *   formatBerthRange(['A5'])                              -> 'A5'
 *   formatBerthRange(['A1', 'A2', 'A3'])                  -> 'A1-A3'
 *   formatBerthRange(['A1', 'A3'])                        -> 'A1, A3'
 *   formatBerthRange(['A1', 'A2', 'B5', 'B6', 'B7'])     -> 'A1-A2, B5-B7'
 *   formatBerthRange(['B5', 'A1', 'A2'])                  -> 'A1-A2, B5'   (sorted)
 *   formatBerthRange(['A1', 'A1', 'A2'])                  -> 'A1-A2'       (dedup)
 *
 * Inputs that don't match the canonical `^[A-Z]+\d+$` pattern (multi-
 * letter prefixes are fine, but suffixes like 'A1a' or pure numbers
 * are not) are passed through unchanged and joined with the rest by
 * comma. A warning is logged.
 *
 * The returned string is the **only** place berths are rendered as a
 * compressed range. CRM UI always shows berths as individual chips
 * (plan §1).
 */

// Note: this module is imported by client components (e.g. the EOI
// upload dialog), so we deliberately avoid `@/lib/logger` - that file
// pulls in `request-context.ts` which uses `node:async_hooks`, an
// import Turbopack can't include in a client bundle. `console.warn`
// works on both client and server and is more than enough for a
// formatter warning.

const CANONICAL = /^([A-Z]+)(\d+)$/;

interface ParsedMooring {
  prefix: string;
  number: number;
  raw: string;
}

function tryParse(raw: string): ParsedMooring | null {
  const m = CANONICAL.exec(raw.trim());
  if (!m) return null;
  return {
    prefix: m[1]!,
    number: parseInt(m[2]!, 10),
    raw: raw.trim(),
  };
}

export function formatBerthRange(mooringNumbers: readonly string[]): string {
  if (mooringNumbers.length === 0) return '';

  // Dedup while preserving the original-input order so the warning log
  // (if any) reports the actual offending input.
  const dedupSeen = new Set<string>();
  const dedup: string[] = [];
  for (const m of mooringNumbers) {
    const trimmed = m.trim();
    if (trimmed && !dedupSeen.has(trimmed)) {
      dedupSeen.add(trimmed);
      dedup.push(trimmed);
    }
  }

  const parsed: ParsedMooring[] = [];
  const passthrough: string[] = [];
  for (const m of dedup) {
    const p = tryParse(m);
    if (p) parsed.push(p);
    else {
      // Per-mooring debug intentionally omitted to keep noise down;
      // the single summary warn below makes the situation observable.
      passthrough.push(m);
    }
  }

  if (passthrough.length > 0) {
    console.warn(
      'formatBerthRange: %d non-canonical moorings passed through (samples: %o)',
      passthrough.length,
      passthrough.slice(0, 3),
    );
  }

  // Sort canonical-form moorings by (prefix, number).
  parsed.sort((a, b) => {
    if (a.prefix !== b.prefix) return a.prefix < b.prefix ? -1 : 1;
    return a.number - b.number;
  });

  // Compress consecutive runs within the same prefix into "A1-A3".
  const segments: string[] = [];
  let runStart: ParsedMooring | null = null;
  let runEnd: ParsedMooring | null = null;
  for (const p of parsed) {
    if (runStart && runEnd && p.prefix === runEnd.prefix && p.number === runEnd.number + 1) {
      runEnd = p;
      continue;
    }
    if (runStart && runEnd) {
      segments.push(formatRun(runStart, runEnd));
    }
    runStart = p;
    runEnd = p;
  }
  if (runStart && runEnd) {
    segments.push(formatRun(runStart, runEnd));
  }

  // Append non-canonical inputs verbatim.
  segments.push(...passthrough);

  return segments.join(', ');
}

function formatRun(start: ParsedMooring, end: ParsedMooring): string {
  return start.raw === end.raw ? start.raw : `${start.raw}-${end.raw}`;
}
