/**
 * Date formatting — how timestamps render throughout the app (spec §07 →
 * Formatting). Governs the Snippet Library list dates and the metadata panel's
 * Created/Modified, driven by the user's `formatting.dateFormat` setting.
 *
 * Portable core: no browser APIs, no React. `Intl` is deliberately avoided for
 * the custom tokens so output is locale-stable and unit-testable; month/day
 * names are the fixed English set. Pure: an ISO string in, a display string out.
 *
 *   - smart  → relative, human-friendly ("Today", "Yesterday", "3d ago", then a
 *              full locale date for older items).
 *   - iso    → a full ISO 8601 timestamp.
 *   - custom → the user's format string (token grammar below); falls back to ISO
 *              when the pattern is empty.
 */

/** The three date-display modes (mirrors UserSettings.formatting.dateFormat). */
export type DateFormatMode = 'smart' | 'iso' | 'custom';

const DAY_MS = 24 * 60 * 60 * 1000;

const MONTHS_SHORT = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec',
];
const MONTHS_LONG = [
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
  'July',
  'August',
  'September',
  'October',
  'November',
  'December',
];

/** Two-digit zero-pad (mirrors snippet.ts → generateSnippetName). */
function pad(n: number): string {
  return String(n).padStart(2, '0');
}

/**
 * Relative, human-friendly rendering (spec §07 → Smart). Same day → "Today",
 * one calendar day back → "Yesterday", under a week → "Nd ago", else the full
 * locale date. Comparison is by calendar day (local), so "Yesterday" doesn't
 * flip on the exact hour. `now` is injectable for deterministic tests.
 */
export function formatSmart(date: Date, now: Date): string {
  const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
  const days = Math.floor((startOfDay(now) - startOfDay(date)) / DAY_MS);
  if (days <= 0) return 'Today';
  if (days === 1) return 'Yesterday';
  if (days < 7) return `${days}d ago`;
  return date.toLocaleDateString();
}

/** Full ISO 8601 timestamp (spec §07 → ISO 8601). */
export function formatIso(date: Date): string {
  return date.toISOString();
}

// Tokens longest-first so MMMM matches before MMM before MM before M, etc.
const TOKEN = /yyyy|yy|MMMM|MMM|MM|M|dd|d|HH|H|hh|h|mm|m|ss|s|a/g;

/**
 * Format a date with a date-fns-style token pattern, in **local** time
 * (spec §07 → Custom). Supported tokens: `yyyy yy MMMM MMM MM M dd d HH H hh h
 * mm m ss s a`. Text between tokens is preserved verbatim (no quoting), which is
 * enough for patterns like `yyyy-MM-dd HH:mm`; a stray literal letter that
 * happens to be a token (e.g. a `d` in prose) would be substituted — the field
 * is a power-user affordance, so this stays simple and predictable.
 */
export function formatCustom(date: Date, pattern: string): string {
  const h24 = date.getHours();
  const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
  const map: Record<string, string> = {
    yyyy: String(date.getFullYear()),
    yy: pad(date.getFullYear() % 100),
    MMMM: MONTHS_LONG[date.getMonth()],
    MMM: MONTHS_SHORT[date.getMonth()],
    MM: pad(date.getMonth() + 1),
    M: String(date.getMonth() + 1),
    dd: pad(date.getDate()),
    d: String(date.getDate()),
    HH: pad(h24),
    H: String(h24),
    hh: pad(h12),
    h: String(h12),
    mm: pad(date.getMinutes()),
    m: String(date.getMinutes()),
    ss: pad(date.getSeconds()),
    s: String(date.getSeconds()),
    a: h24 < 12 ? 'AM' : 'PM',
  };
  return pattern.replace(TOKEN, (t) => map[t] ?? t);
}

/**
 * Render an ISO timestamp per the user's date-format setting (spec §07). An
 * unparseable timestamp is returned verbatim rather than rendered as "Invalid
 * Date", so a malformed stored value degrades gracefully. `customFormat` is used
 * only in `custom` mode and falls back to ISO when blank. `now` (for `smart`)
 * defaults to the current time; inject it in tests.
 */
export function formatDate(
  iso: string,
  mode: DateFormatMode,
  customFormat = '',
  now: Date = new Date(),
): string {
  const date = new Date(iso);
  if (Number.isNaN(date.getTime())) return iso;

  switch (mode) {
    case 'iso':
      return formatIso(date);
    case 'custom':
      return customFormat.trim() === '' ? formatIso(date) : formatCustom(date, customFormat);
    case 'smart':
    default:
      return formatSmart(date, now);
  }
}
