/**
 * Handles non-English input and generates the final suggestion list.
 * Translates localized words to English before parsing, then converts
 * suggestion labels back to the user's language for display.
 */

import {
  WEEKDAY_MAP,
  MONTH_MAP,
  UNIT_MAP,
  WORD_NUMBER_MAP,
  RELATIVE_DAY_MAP,
  TIME_OF_DAY_MAP,
  sanitize,
  stripNoise,
  normalizeDigits,
} from './tokenMaps';

import { parseDateFromText } from './parser';
import { buildSuggestionCandidates, MAX_SUGGESTIONS } from './suggestions';

// ─── English Reference Data ─────────────────────────────────────────────────

const EN_WEEKDAYS_LIST = [
  'monday',
  'tuesday',
  'wednesday',
  'thursday',
  'friday',
  'saturday',
  'sunday',
];

const EN_MONTHS_LIST = [
  'january',
  'february',
  'march',
  'april',
  'may',
  'june',
  'july',
  'august',
  'september',
  'october',
  'november',
  'december',
];

const EN_DEFAULTS = {
  UNITS: {
    MINUTE: 'minute',
    MINUTES: 'minutes',
    HOUR: 'hour',
    HOURS: 'hours',
    DAY: 'day',
    DAYS: 'days',
    WEEK: 'week',
    WEEKS: 'weeks',
    MONTH: 'month',
    MONTHS: 'months',
    YEAR: 'year',
    YEARS: 'years',
  },
  RELATIVE: {
    TOMORROW: 'tomorrow',
    DAY_AFTER_TOMORROW: 'day after tomorrow',
    NEXT_WEEK: 'next week',
    NEXT_MONTH: 'next month',
    THIS_WEEKEND: 'this weekend',
    NEXT_WEEKEND: 'next weekend',
  },
  TIME_OF_DAY: {
    MORNING: 'morning',
    AFTERNOON: 'afternoon',
    EVENING: 'evening',
    NIGHT: 'night',
    NOON: 'noon',
    MIDNIGHT: 'midnight',
  },
  WORD_NUMBERS: {
    ONE: 'one',
    TWO: 'two',
    THREE: 'three',
    FOUR: 'four',
    FIVE: 'five',
    SIX: 'six',
    SEVEN: 'seven',
    EIGHT: 'eight',
    NINE: 'nine',
    TEN: 'ten',
    TWELVE: 'twelve',
    FIFTEEN: 'fifteen',
    TWENTY: 'twenty',
    THIRTY: 'thirty',
  },
  ORDINALS: {
    FIRST: 'first',
    SECOND: 'second',
    THIRD: 'third',
    FOURTH: 'fourth',
    FIFTH: 'fifth',
  },
  MERIDIEM: { AM: 'am', PM: 'pm' },
  HALF: 'half',
  NEXT: 'next',
  THIS: 'this',
  AT: 'at',
  IN: 'in',
  OF: 'of',
  AFTER: 'after',
  WEEK: 'week',
  DAY: 'day',
  FROM_NOW: 'from now',
  NEXT_YEAR: 'next year',
};

const STRUCTURAL_WORDS = [
  'at',
  'in',
  'next',
  'this',
  'from',
  'now',
  'after',
  'half',
  'same',
  'time',
  'weekend',
  'end',
  'of',
  'the',
  'eod',
  'am',
  'pm',
  'week',
  'day',
  'first',
  'second',
  'third',
  'fourth',
  'fifth',
];

const ENGLISH_VOCAB = new Set([
  ...Object.keys(WEEKDAY_MAP),
  ...Object.keys(MONTH_MAP),
  ...Object.keys(UNIT_MAP),
  ...Object.keys(WORD_NUMBER_MAP),
  ...Object.keys(RELATIVE_DAY_MAP),
  ...Object.keys(TIME_OF_DAY_MAP),
  ...EN_WEEKDAYS_LIST,
  ...EN_MONTHS_LIST,
  ...STRUCTURAL_WORDS,
]);

// ─── Regex for token replacement ────────────────────────────────────────────

const MONTH_NAMES = Object.keys(MONTH_MAP).join('|');
const MONTH_NAME_RE = new RegExp(`\\b(?:${MONTH_NAMES})\\b`, 'i');
const NUM_TOD_RE =
  /\b(\d{1,2}(?::\d{2})?)\s+(morning|noon|afternoon|evening|night)\b/g;
const TOD_TO_MERIDIEM = {
  morning: 'am',
  noon: 'pm',
  afternoon: 'pm',
  evening: 'pm',
  night: 'pm',
};

// ─── Translation Cache ──────────────────────────────────────────────────────

const safeString = v => (v == null ? '' : String(v));
const MAX_PAIRS_CACHE = 20;
const pairsCache = new Map();
const CACHE_SECTIONS = [
  'UNITS',
  'RELATIVE',
  'TIME_OF_DAY',
  'WORD_NUMBERS',
  'ORDINALS',
  'MERIDIEM',
];
const SINGLE_KEYS = [
  'HALF',
  'NEXT',
  'THIS',
  'AT',
  'IN',
  'OF',
  'AFTER',
  'WEEK',
  'DAY',
  'FROM_NOW',
  'NEXT_YEAR',
];

/** Create a string key from translations so we can cache results. */
const translationSignature = translations => {
  if (!translations || typeof translations !== 'object') return 'none';
  return [
    ...CACHE_SECTIONS.flatMap(section => {
      const values = translations[section] || {};
      return Object.keys(values)
        .sort()
        .map(k => `${section}.${k}:${safeString(values[k]).toLowerCase()}`);
    }),
    ...SINGLE_KEYS.map(
      k => `${k}:${safeString(translations[k]).toLowerCase()}`
    ),
  ].join('|');
};

/** Build a list of [localWord, englishWord] pairs from the translations and browser locale. */
const buildReplacementPairsUncached = (translations, locale) => {
  const pairs = [];
  const seen = new Set();
  const t = translations || {};

  const addPair = (local, en) => {
    const l = sanitize(safeString(local));
    const e = safeString(en).toLowerCase();
    const key = `${l}\0${e}`;
    if (l && e && l !== e && !seen.has(key)) {
      seen.add(key);
      pairs.push([l, e]);
    }
  };

  CACHE_SECTIONS.forEach(section => {
    const localSection = t[section] || {};
    const enSection = EN_DEFAULTS[section] || {};
    Object.keys(enSection).forEach(key => {
      addPair(localSection[key], enSection[key]);
    });
  });

  SINGLE_KEYS.forEach(key => addPair(t[key], EN_DEFAULTS[key]));

  try {
    const wdFmt = new Intl.DateTimeFormat(locale, { weekday: 'long' });
    // Jan 1, 2024 is a Monday — aligns with EN_WEEKDAYS_LIST[0]='monday'
    EN_WEEKDAYS_LIST.forEach((en, i) => {
      addPair(wdFmt.format(new Date(2024, 0, i + 1)), en);
    });
  } catch {
    /* locale not supported */
  }

  try {
    const moFmt = new Intl.DateTimeFormat(locale, { month: 'long' });
    EN_MONTHS_LIST.forEach((en, i) => {
      addPair(moFmt.format(new Date(2024, i, 1)), en);
    });
  } catch {
    /* locale not supported */
  }

  pairs.sort((a, b) => b[0].length - a[0].length);
  return pairs;
};

/** Same as above but cached. Keeps up to 20 entries to avoid rebuilding every call. */
const buildReplacementPairs = (translations, locale) => {
  const cacheKey = `${locale || ''}:${translationSignature(translations)}`;
  if (pairsCache.has(cacheKey)) return pairsCache.get(cacheKey);
  const pairs = buildReplacementPairsUncached(translations, locale);
  if (pairsCache.size >= MAX_PAIRS_CACHE)
    pairsCache.delete(pairsCache.keys().next().value);
  pairsCache.set(cacheKey, pairs);
  return pairs;
};

// ─── Token Replacement ──────────────────────────────────────────────────────

const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

/** Swap localized words for their English versions in the text. */
const substituteLocalTokens = (text, pairs) => {
  let r = text;
  pairs.forEach(([local, en]) => {
    const re = new RegExp(`(?<=^|\\s)${escapeRegex(local)}(?=\\s|$)`, 'g');
    r = r.replace(re, en);
  });
  return r;
};

/** Drop any words the parser wouldn't understand (keeps English words and numbers). */
const filterToEnglishVocab = text =>
  normalizeDigits(text)
    .replace(/(\d+)h\b/g, '$1:00')
    .split(/\s+/)
    .filter(w => /[\d:]/.test(w) || ENGLISH_VOCAB.has(w.toLowerCase()))
    .join(' ')
    .replace(/\s+/g, ' ')
    .trim();

/** Move "next year" to the right spot so the parser can read it (after the month, before time). */
const repositionNextYear = text => {
  if (!MONTH_NAME_RE.test(text)) return text;
  let r = text.replace(/\b(?:next\s+)?year\b/i, m =>
    /next/i.test(m) ? m : 'next year'
  );
  if (!/\bnext\s+year\b/i.test(r)) return r;
  const withoutNY = r.replace(/\bnext\s+year\b/i, '').trim();
  const timeRe = /(?:(?:at\s+)?\d{1,2}(?::\d{2})?\s*(?:am|pm)?)\s*$/i;
  const timePart = withoutNY.match(timeRe);
  if (timePart) {
    const beforeTime = withoutNY.slice(0, timePart.index).trim();
    r = `${beforeTime} next year ${timePart[0].trim()}`;
  } else {
    r = `${withoutNY} next year`;
  }
  return r;
};

/** Run the full translation pipeline: swap tokens, filter, fix am/pm, reposition "next year". */
const replaceTokens = (text, pairs) => {
  const substituted = substituteLocalTokens(text, pairs);
  const filtered = filterToEnglishVocab(substituted);
  const fixed = filtered.replace(
    NUM_TOD_RE,
    (_, t, tod) => `${t}${TOD_TO_MERIDIEM[tod]}`
  );
  return stripNoise(repositionNextYear(fixed));
};

/** Convert English words back to the user's language for display. */
const reverseTokens = (text, pairs) =>
  pairs.reduce(
    (r, [local, en]) =>
      r.replace(
        new RegExp(`(?<=^|\\s)${escapeRegex(en)}(?=\\s|$)`, 'g'),
        local
      ),
    text
  );

// ─── Main Suggestion Generator ──────────────────────────────────────────────

/**
 * Generate snooze suggestions from what the user has typed so far.
 * Works with any language if translations are provided. Returns up to 5
 * unique results, each with a label, date, and unix timestamp.
 *
 * @param {string} text - what the user typed
 * @param {Date} [referenceDate] - treat as "now" (defaults to current time)
 * @param {{ translations?: object, locale?: string }} [options] - i18n config
 * @returns {Array<{ label: string, date: Date, unix: number }>}
 */
export const generateDateSuggestions = (
  text,
  referenceDate = new Date(),
  { translations, locale } = {}
) => {
  if (!text || typeof text !== 'string') return [];
  const normalized = sanitize(text);
  if (!normalized) return [];

  const stripped = stripNoise(normalized);
  const pairs =
    locale && locale !== 'en'
      ? buildReplacementPairs(translations, locale)
      : [];

  // Try English parse first, then translated parse if we have locale pairs.
  // This avoids the problem where a single overlapping word (e.g. "in" in German)
  // would skip token translation entirely.
  const directParse = parseDateFromText(stripped, referenceDate);

  const translated = pairs.length ? replaceTokens(normalized, pairs) : null;
  const translatedParse =
    translated && translated !== stripped
      ? parseDateFromText(translated, referenceDate)
      : null;

  // Prefer direct English parse; fall back to translated parse
  const useTranslated = !directParse && !!translatedParse;
  const englishInput = useTranslated ? translated : stripped;

  const seen = new Set();
  const results = [];

  const exact = directParse || translatedParse;
  if (exact) {
    seen.add(exact.unix);
    const exactLabel =
      useTranslated && pairs.length
        ? reverseTokens(englishInput, pairs)
        : englishInput;
    results.push({ label: exactLabel, query: englishInput, ...exact });
  }

  buildSuggestionCandidates(englishInput).some(candidate => {
    if (results.length >= MAX_SUGGESTIONS) return true;
    const result = parseDateFromText(candidate, referenceDate);
    if (result && !seen.has(result.unix)) {
      seen.add(result.unix);
      const label =
        useTranslated && pairs.length
          ? reverseTokens(candidate, pairs)
          : candidate;
      results.push({ label, query: candidate, ...result });
    }
    return false;
  });

  return results;
};
