// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import moment from 'moment';
import { HourCyclePreference } from '../types/I18N.std.ts';
import type { LocalizerType } from '../types/Util.std.ts';
import { assertDev } from './assert.std.ts';
import { isYesterday, isToday, type RawTimestamp } from './timestamp.std.ts';
import { HOUR, MINUTE, MONTH, WEEK } from './durations/index.std.ts';

function getOptionsWithPreferences(
  options: Intl.DateTimeFormatOptions
): Intl.DateTimeFormatOptions {
  const hourCyclePreference = window.SignalContext.getHourCyclePreference();
  if (options.hour12 != null) {
    return options;
  }
  if (hourCyclePreference === HourCyclePreference.Prefer12) {
    return { ...options, hour12: true };
  }
  if (hourCyclePreference === HourCyclePreference.Prefer24) {
    return { ...options, hour12: false };
  }
  return options;
}

/**
 * Chrome doesn't implement hour12 correctly
 */
function fixBuggyOptions(
  locales: Array<string>,
  options: Intl.DateTimeFormatOptions
): Intl.DateTimeFormatOptions {
  const resolvedOptions = new Intl.DateTimeFormat(
    locales,
    options
  ).resolvedOptions();
  const resolvedLocale = new Intl.Locale(resolvedOptions.locale);
  let { hourCycle } = resolvedOptions;
  // Most languages should use either h24 or h12
  if (hourCycle === 'h24') {
    hourCycle = 'h23';
  }
  if (hourCycle === 'h11') {
    hourCycle = 'h12';
  }
  // Only Japanese should use h11 when using hour12 time
  if (hourCycle === 'h12' && resolvedLocale.language === 'ja') {
    hourCycle = 'h11';
  }
  return {
    ...options,
    hour12: undefined,
    hourCycle,
  };
}

function getCacheKey(
  locales: Array<string>,
  options: Intl.DateTimeFormatOptions
) {
  return `${locales.join(',')}:${Object.keys(options)
    .sort()
    // oxlint-disable-next-line typescript/no-explicit-any
    .map(key => `${key}=${(options as any)[key]}`)
    .join(',')}`;
}

const formatterCache = new Map<string, Intl.DateTimeFormat>();

export function getDateTimeFormatter(
  options: Intl.DateTimeFormatOptions
): Intl.DateTimeFormat {
  const preferredSystemLocales =
    window.SignalContext.getPreferredSystemLocales();
  const localeOverride = window.SignalContext.getLocaleOverride();
  const locales =
    localeOverride != null ? [localeOverride] : preferredSystemLocales;
  const optionsWithPreferences = getOptionsWithPreferences(options);
  const cacheKey = getCacheKey(locales, optionsWithPreferences);
  const cachedFormatter = formatterCache.get(cacheKey);
  if (cachedFormatter) {
    return cachedFormatter;
  }
  const fixedOptions = fixBuggyOptions(locales, optionsWithPreferences);
  const formatter = new Intl.DateTimeFormat(locales, fixedOptions);
  formatterCache.set(cacheKey, formatter);
  return formatter;
}

export function formatTimestamp(
  timestamp: number,
  options: Intl.DateTimeFormatOptions
): string {
  const formatter = getDateTimeFormatter(options);
  try {
    return formatter.format(timestamp);
  } catch (err) {
    assertDev(false, 'invalid timestamp');
    return '';
  }
}

export function formatDateTimeShort(
  i18n: LocalizerType,
  rawTimestamp: RawTimestamp
): string {
  const timestamp = rawTimestamp.valueOf();

  const now = Date.now();
  const diff = now - timestamp;

  if (diff < HOUR || isToday(timestamp)) {
    return formatTime(i18n, rawTimestamp, now);
  }

  const m = moment(timestamp);

  if (diff < WEEK && m.isSame(now, 'month')) {
    return formatTimestamp(timestamp, { weekday: 'short' });
  }

  if (Math.abs(m.diff(Date.now())) < 6 * MONTH) {
    return formatTimestamp(timestamp, {
      day: 'numeric',
      month: 'short',
    });
  }

  return formatTimestamp(timestamp, {
    day: 'numeric',
    month: 'short',
    year: 'numeric',
  });
}

export function formatDateTimeForAttachment(
  i18n: LocalizerType,
  rawTimestamp: RawTimestamp
): string {
  const timestamp = rawTimestamp.valueOf();

  const now = Date.now();
  const diff = now - timestamp;

  if (diff < HOUR || isToday(timestamp)) {
    return formatTime(i18n, rawTimestamp, now);
  }

  const m = moment(timestamp);

  if (diff < WEEK && m.isSame(now, 'month')) {
    return formatTimestamp(timestamp, {
      weekday: 'short',
      hour: 'numeric',
      minute: 'numeric',
    });
  }

  if (Math.abs(m.diff(Date.now())) < 6 * MONTH) {
    return formatTimestamp(timestamp, {
      day: 'numeric',
      month: 'short',
      hour: 'numeric',
      minute: 'numeric',
    });
  }

  return formatTimestamp(timestamp, {
    day: 'numeric',
    month: 'short',
    year: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
  });
}

export function formatDateTimeLong(
  i18n: LocalizerType,
  rawTimestamp: RawTimestamp
): string {
  const timestamp = rawTimestamp.valueOf();

  if (isToday(rawTimestamp)) {
    return i18n('icu:timestampFormat__long--today', {
      time: formatTimestamp(timestamp, {
        hour: 'numeric',
        minute: 'numeric',
      }),
    });
  }

  if (isYesterday(rawTimestamp)) {
    return i18n('icu:timestampFormat__long--yesterday', {
      time: formatTimestamp(timestamp, {
        hour: 'numeric',
        minute: 'numeric',
      }),
    });
  }

  return formatTimestamp(timestamp, {
    day: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    month: 'short',
    year: 'numeric',
  });
}

export function formatTime(
  i18n: LocalizerType,
  rawTimestamp: RawTimestamp,
  now: RawTimestamp,
  isRelativeTime?: boolean
): string {
  const timestamp = rawTimestamp.valueOf();
  const diff = now.valueOf() - timestamp;

  if (diff < MINUTE) {
    return i18n('icu:justNow');
  }

  if (diff < HOUR) {
    return i18n('icu:minutesAgo', {
      minutes: Math.floor(diff / MINUTE),
    });
  }

  if (isRelativeTime) {
    return i18n('icu:hoursAgo', {
      hours: Math.floor(diff / HOUR),
    });
  }

  return formatTimestamp(timestamp, {
    hour: 'numeric',
    minute: '2-digit',
  });
}

export function formatDate(
  i18n: LocalizerType,
  rawTimestamp: RawTimestamp
): string {
  if (isToday(rawTimestamp)) {
    return i18n('icu:today');
  }

  if (isYesterday(rawTimestamp)) {
    return i18n('icu:yesterday');
  }

  const m = moment(rawTimestamp);

  const timestamp = rawTimestamp.valueOf();

  if (Math.abs(m.diff(Date.now())) < 6 * MONTH) {
    return formatTimestamp(timestamp, {
      day: 'numeric',
      month: 'short',
      weekday: 'short',
    });
  }

  return formatTimestamp(timestamp, {
    day: 'numeric',
    month: 'short',
    year: 'numeric',
  });
}
