/**
 * Shared lookup tables and helper functions used by the parser,
 * suggestions, and localization modules.
 */

import {
  add,
  set,
  isValid,
  isAfter,
  nextMonday,
  nextTuesday,
  nextWednesday,
  nextThursday,
  nextFriday,
  nextSaturday,
  nextSunday,
} from 'date-fns';

// ─── Token Maps ──────────────────────────────────────────────────────────────
// All keys are lowercase. Short forms and full names both work.

/** Weekday name or short form → day index (0 = Sunday). */
export const WEEKDAY_MAP = {
  sunday: 0,
  sun: 0,
  monday: 1,
  mon: 1,
  tuesday: 2,
  tue: 2,
  tues: 2,
  wednesday: 3,
  wed: 3,
  thursday: 4,
  thu: 4,
  thur: 4,
  thurs: 4,
  friday: 5,
  fri: 5,
  saturday: 6,
  sat: 6,
};

/** Month name or short form → month index (0 = January). */
export const MONTH_MAP = {
  january: 0,
  jan: 0,
  february: 1,
  feb: 1,
  march: 2,
  mar: 2,
  april: 3,
  apr: 3,
  may: 4,
  june: 5,
  jun: 5,
  july: 6,
  jul: 6,
  august: 7,
  aug: 7,
  september: 8,
  sep: 8,
  sept: 8,
  october: 9,
  oct: 9,
  november: 10,
  nov: 10,
  december: 11,
  dec: 11,
};

/** Words like "today" or "tomorrow" → how many days from now. */
export const RELATIVE_DAY_MAP = {
  today: 0,
  tonight: 0,
  tonite: 0,
  tomorrow: 1,
  tmr: 1,
  tmrw: 1,
};

/** Unit shorthand → full unit name used by date-fns. */
export const UNIT_MAP = {
  m: 'minutes',
  min: 'minutes',
  mins: 'minutes',
  minute: 'minutes',
  minutes: 'minutes',
  h: 'hours',
  hr: 'hours',
  hrs: 'hours',
  hour: 'hours',
  hours: 'hours',
  d: 'days',
  day: 'days',
  days: 'days',
  w: 'weeks',
  wk: 'weeks',
  wks: 'weeks',
  week: 'weeks',
  weeks: 'weeks',
  mo: 'months',
  month: 'months',
  months: 'months',
  y: 'years',
  yr: 'years',
  yrs: 'years',
  year: 'years',
  years: 'years',
};

/** English number words → their numeric value. */
export const WORD_NUMBER_MAP = {
  a: 1,
  an: 1,
  one: 1,
  couple: 2,
  few: 3,
  two: 2,
  three: 3,
  four: 4,
  five: 5,
  six: 6,
  seven: 7,
  eight: 8,
  nine: 9,
  ten: 10,
  eleven: 11,
  twelve: 12,
  thirteen: 13,
  fourteen: 14,
  fifteen: 15,
  sixteen: 16,
  seventeen: 17,
  eighteen: 18,
  nineteen: 19,
  twenty: 20,
  thirty: 30,
  forty: 40,
  fifty: 50,
  sixty: 60,
  ninety: 90,
  half: 0.5,
};

/** Day index → the date-fns function that finds the next occurrence. */
export const NEXT_WEEKDAY_FN = {
  0: nextSunday,
  1: nextMonday,
  2: nextTuesday,
  3: nextWednesday,
  4: nextThursday,
  5: nextFriday,
  6: nextSaturday,
};

/** Time-of-day label → default hour and minute. */
export const TIME_OF_DAY_MAP = {
  morning: { hours: 9, minutes: 0 },
  noon: { hours: 12, minutes: 0 },
  afternoon: { hours: 14, minutes: 0 },
  evening: { hours: 18, minutes: 0 },
  night: { hours: 20, minutes: 0 },
  tonight: { hours: 20, minutes: 0 },
  midnight: { hours: 0, minutes: 0 },
  eod: { hours: 17, minutes: 0 },
  'end of day': { hours: 17, minutes: 0 },
  'end of the day': { hours: 17, minutes: 0 },
};

/** Allowed hour range per label — used to pick am or pm when not specified. */
export const TOD_HOUR_RANGE = {
  morning: [4, 12],
  noon: [11, 13],
  afternoon: [12, 18],
  evening: [16, 22],
  night: [18, 24],
  tonight: [18, 24],
  midnight: [23, 25],
};

/** What "half hour", "half day", etc. actually mean in date-fns terms. */
export const HALF_UNIT_DURATIONS = {
  hour: { minutes: 30 },
  day: { hours: 12 },
  week: { days: 3, hours: 12 },
  month: { days: 15 },
  year: { months: 6 },
};

const FRACTIONAL_CONVERT = {
  hours: { unit: 'minutes', factor: 60 },
  days: { unit: 'hours', factor: 24 },
  weeks: { unit: 'days', factor: 7 },
  months: { unit: 'days', factor: 30 },
  years: { unit: 'months', factor: 12 },
};

// ─── Unicode / Normalization ────────────────────────────────────────────────
// Turn non-ASCII digits and punctuation into plain ASCII so the
// parser only has to deal with standard characters.

const UNICODE_DIGIT_RANGES = [
  [0x30, 0x39],
  [0x660, 0x669], // Arabic-Indic
  [0x6f0, 0x6f9], // Eastern Arabic-Indic
  [0x966, 0x96f], // Devanagari
  [0x9e6, 0x9ef], // Bengali
  [0xa66, 0xa6f], // Gurmukhi
  [0xae6, 0xaef], // Gujarati
  [0xb66, 0xb6f], // Oriya
  [0xbe6, 0xbef], // Tamil
  [0xc66, 0xc6f], // Telugu
  [0xce6, 0xcef], // Kannada
  [0xd66, 0xd6f], // Malayalam
];

const toAsciiDigit = char => {
  const code = char.codePointAt(0);
  const range = UNICODE_DIGIT_RANGES.find(
    ([start, end]) => code >= start && code <= end
  );
  if (!range) return char;
  return String(code - range[0]);
};

/** Turn non-ASCII digits (Arabic, Devanagari, etc.) into 0-9. */
export const normalizeDigits = text => text.replace(/\p{Nd}/gu, toAsciiDigit);

const ARABIC_PUNCT_MAP = {
  '\u061f': '?',
  '\u060c': ',',
  '\u061b': ';',
  '\u066b': '.',
};

const NOISE_RE =
  /^(?:(?:can|could|will|would)\s+you\s+)?(?:(?:please|pls|plz|kindly)\s+)?(?:(?:snooze|remind(?:\s+me)?|set(?:\s+(?:a|the))?(?:\s+(?:reminder|deadline|snooze|timer))?|add(?:\s+(?:a|the))?(?:\s+(?:reminder|deadline|snooze))?|schedule|postpone|defer|delay|push)(?:\s+(?:it|this))?\s+)?(?:(?:on|to|for|at|until|till|by|from|after|within)\s+)?/;

const APPROX_RE = /^(?:approx(?:imately)?|around|about|roughly|~)\s+/;

/** Clean up raw input: lowercase, remove punctuation, collapse spaces. */
export const sanitize = text =>
  normalizeDigits(
    text
      .normalize('NFKC')
      .toLowerCase()
      .replace(/[\u200f\u200e\u066c\u0640]/g, '')
      .replace(/[\u064b-\u065f]/g, '')
      .replace(/\u00a0/g, ' ')
      .replace(/[\u061f\u060c\u061b\u066b]/g, c => ARABIC_PUNCT_MAP[c])
  )
    .replace(/[,!?;]+/g, ' ')
    .replace(/\.+$/g, '')
    .replace(/\s+/g, ' ')
    .trim();

/** Strip filler words like "please snooze for" and fix typos like "tommorow". */
export const stripNoise = text => {
  let r = text
    .replace(/\ba\s+fortnight\b/g, '2 weeks')
    .replace(/\bfortnight\b/g, '2 weeks')
    .replace(NOISE_RE, '')
    .replace(APPROX_RE, '')
    .replace(/^the\s+/, '')
    .replace(/\bnxt\b/g, 'next')
    .replace(/\ba\s+couple\s+of\b/g, 'couple')
    .replace(/\bcouple\s+of\b/g, 'couple')
    .replace(/\ba\s+couple\b/g, 'couple')
    .replace(/\ba\s+few\b/g, 'few')
    .replace(
      /\b(\d+)\s*(?:h|hr|hours?)[\s]*(\d+)\s*(?:m|min|minutes?)\b/g,
      (_, h, m) =>
        `${h} ${h === '1' ? 'hour' : 'hours'} ${m} ${m === '1' ? 'minute' : 'minutes'}`
    )
    .replace(/\b(\d+)h\b/g, (_, h) => `${h} ${h === '1' ? 'hour' : 'hours'}`)
    .replace(
      /\b(\d+)m\b/g,
      (_, m) => `${m} ${m === '1' ? 'minute' : 'minutes'}`
    )
    .replace(/\btomm?orow\b/g, 'tomorrow')
    .replace(/\s+later$/, '')
    .trim();
  // bare unit without number: "month later" → "1 month", "week" stays
  r = r.replace(/^(minutes?|hours?|days?|weeks?|months?|years?)$/, '1 $1');
  return r;
};

// ─── Utility Functions ──────────────────────────────────────────────────────

/** Turn a string into a number. Works with digits ("5") and words ("five"). */
export const parseNumber = str => {
  if (!str) return null;
  const lower = normalizeDigits(str.toLowerCase().trim());
  if (WORD_NUMBER_MAP[lower] !== undefined) return WORD_NUMBER_MAP[lower];
  const num = Number(lower);
  return Number.isNaN(num) ? null : num;
};

/** Set the time on a date, clearing seconds and milliseconds. */
export const applyTimeToDate = (date, hours, minutes = 0) =>
  set(date, { hours, minutes, seconds: 0, milliseconds: 0 });

/** Parse "3pm", "14:30", or "2:00am" into { hours, minutes }. Returns null if invalid. */
export const parseTimeString = timeStr => {
  if (!timeStr) return null;
  const match = timeStr
    .toLowerCase()
    .replace(/\s+/g, '')
    .match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm|a\.m\.?|p\.m\.?)?$/);
  if (!match) return null;

  const raw = parseInt(match[1], 10);
  const minutes = match[2] ? parseInt(match[2], 10) : 0;
  const meridiem = match[3]?.replace(/\./g, '');
  if (meridiem && (raw < 1 || raw > 12)) return null;

  const toHours = (h, m) => {
    if (m === 'pm' && h < 12) return h + 12;
    if (m === 'am' && h === 12) return 0;
    return h;
  };
  const hours = toHours(raw, meridiem);
  if (hours > 23 || minutes > 59) return null;
  return { hours, minutes };
};

/** Apply a time string to a date. Falls back to 9 AM if no time is given. */
export const applyTimeOrDefault = (date, timeStr, defaultHours = 9) => {
  if (timeStr) {
    const time = parseTimeString(timeStr);
    if (!time) return null;
    return applyTimeToDate(date, time.hours, time.minutes);
  }
  return applyTimeToDate(date, defaultHours, 0);
};

/** Build a Date only if the day actually exists (e.g. rejects Feb 30). */
export const strictDate = (year, month, day) => {
  const date = new Date(year, month, day);
  if (
    !isValid(date) ||
    date.getFullYear() !== year ||
    date.getMonth() !== month ||
    date.getDate() !== day
  )
    return null;
  return date;
};

/** Try up to 8 years ahead to find a valid future date (handles Feb 29 leap years). */
export const futureOrNextYear = (year, month, day, timeStr, now) => {
  for (let i = 0; i < 9; i += 1) {
    const base = strictDate(year + i, month, day);
    if (base) {
      const date = applyTimeOrDefault(base, timeStr);
      if (!date) return null;
      if (isAfter(date, now)) return date;
    }
  }
  return null;
};

/** If the date is already past, push it to the next day. */
export const ensureFutureOrNextDay = (date, now) =>
  isAfter(date, now) ? date : add(date, { days: 1 });

/** Figure out am/pm from context: "morning 6" → 6am, "evening 6" → 6pm. */
export const inferHoursFromTOD = (todLabel, rawHour, rawMinutes) => {
  const h = parseInt(rawHour, 10);
  const m = rawMinutes ? parseInt(rawMinutes, 10) : 0;
  if (Number.isNaN(h) || h < 1 || h > 12 || m > 59) return null;
  const range = TOD_HOUR_RANGE[todLabel];
  if (!range) return { hours: h, minutes: m };
  // Try both am and pm interpretations, pick the one in range
  const am = h === 12 ? 0 : h;
  const pm = h === 12 ? 12 : h + 12;
  const inRange = v => v >= range[0] && v < range[1];
  if (inRange(am)) return { hours: am, minutes: m };
  if (inRange(pm)) return { hours: pm, minutes: m };
  const mid = (range[0] + range[1]) / 2;
  return {
    hours: Math.abs(am - mid) <= Math.abs(pm - mid) ? am : pm,
    minutes: m,
  };
};

/** Add a duration that might be fractional, e.g. 1.5 hours becomes 90 minutes. */
export const addFractionalSafe = (date, unit, amount) => {
  if (Number.isInteger(amount)) return add(date, { [unit]: amount });
  if (amount % 1 !== 0.5) return null;
  const conv = FRACTIONAL_CONVERT[unit];
  if (conv) return add(date, { [conv.unit]: Math.round(amount * conv.factor) });
  return add(date, { [unit]: Math.round(amount) });
};
