// Locale-aware dates, dayjs-style: `date(value).format(...)`. Pure JS (no `Intl` — QuickJS hosts
// don't ship it reliably), so output is byte-identical on every platform. Two locales: English and
// Russian.
//
// ── Designed for chisel fusion ──────────────────────────────────────────────────────────────────
// A date is a *single-scalar* value (epoch milliseconds), which is the 1-component analog of a Vec3
// chain. The wrapper is written so a future chisel date-fuser can lower `date(root).<chain>()` to
// scalar ms arithmetic and drop the wrapper allocation, the same way Vec3 chains fuse:
//   • IMMUTABLE — every method returns a NEW `DateValue`, never mutates `t` (aliasing safety).
//   • ONE canonical scalar — `t` (epoch ms), also reachable via `valueOf()`; that's the "component".
//   • Terminals DELEGATE to the module-private free functions `formatImpl` / `timeAgoImpl`, taking
//     `(ms, …)`. So `date(x).format(p)` can fuse to `formatImpl(toMs(x), p)` with no wrapper.
//   • Linear-unit arithmetic (ms…week) is closed-form scalar math → fusable; calendar units
//     (month/year) and local-tz `startOf`/`endOf` use `Date` and are left to the real method.
// Even before that pass lands, each method is a class method, so chisel's method-granular DCE drops
// whichever ones a project doesn't use.

export type Locale = "en" | "ru"
export type DateInput = Date | number | string | DateValue
export type Unit = "ms" | "second" | "minute" | "hour" | "day" | "week" | "month" | "year"

// ---- locale data -------------------------------------------------------------------------------
// Russian needs the month in two grammatical cases: genitive ("5 сентября") when a day number is
// present, nominative ("Сентябрь 2024") when it stands alone. `format` picks by whether the pattern
// contains a day (`D`) token — the same rule day.js's ru locale uses.

type LocaleData = {
  months: readonly string[]
  monthsGenitive: readonly string[]
  monthsShort: readonly string[]
  weekdays: readonly string[] // Sunday..Saturday (JS getDay() order)
  weekdaysShort: readonly string[]
  am: string
  pm: string
}

const EN: LocaleData = {
  months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
  monthsGenitive: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
  monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
  weekdays: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
  weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
  am: "AM",
  pm: "PM",
}

const RU: LocaleData = {
  months: ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"],
  monthsGenitive: ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"],
  monthsShort: ["янв", "фев", "мар", "апр", "мая", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"],
  weekdays: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],
  weekdaysShort: ["вс", "пн", "вт", "ср", "чт", "пт", "сб"],
  am: "AM",
  pm: "PM",
}

const DATA: Record<Locale, LocaleData> = { en: EN, ru: RU }

const resolveLocale = (locale?: Locale): Locale => {
  if (locale) return locale === "ru" ? "ru" : "en"
  const lang = typeof _creatorUtils !== "undefined" ? _creatorUtils.language : ""
  return lang && lang.toLowerCase().indexOf("ru") === 0 ? "ru" : "en"
}

// Milliseconds per linear unit. Absent keys (month/year) mark the non-fusable calendar units.
const MS: Partial<Record<Unit, number>> = {
  ms: 1,
  second: 1000,
  minute: 60_000,
  hour: 3_600_000,
  day: 86_400_000,
  week: 604_800_000,
}

const toMs = (value: DateInput): number => {
  if (value instanceof DateValue) return value.t
  if (value instanceof Date) return value.getTime()
  if (typeof value === "number") return value
  return new Date(value).getTime()
}

// ---- formatting (the fusion escape target: takes a scalar ms, holds the heavy tables) -----------

const pad2 = (n: number): string => (n < 10 ? "0" + n : "" + n)

// Longest tokens first so `MMMM` matches before `MM`, `YYYY` before `YY`, etc.
const TOKEN = /YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g

const formatImpl = (ms: number, pattern: string, locale?: Locale): string => {
  const d = new Date(ms)
  const L = DATA[resolveLocale(locale)]
  const hasDay = /D/.test(pattern) // genitive month only when a day number accompanies it
  const months = hasDay ? L.monthsGenitive : L.months

  const year = d.getFullYear()
  const month = d.getMonth()
  const day = d.getDate()
  const weekday = d.getDay()
  const hours = d.getHours()
  const hour12 = hours % 12 || 12
  const minutes = d.getMinutes()
  const seconds = d.getSeconds()

  return pattern.replace(TOKEN, (t) => {
    switch (t) {
      case "YYYY": return "" + year
      case "YY": return pad2(year % 100)
      case "MMMM": return months[month]
      case "MMM": return L.monthsShort[month]
      case "MM": return pad2(month + 1)
      case "M": return "" + (month + 1)
      case "DD": return pad2(day)
      case "D": return "" + day
      case "dddd": return L.weekdays[weekday]
      case "ddd": return L.weekdaysShort[weekday]
      case "HH": return pad2(hours)
      case "H": return "" + hours
      case "hh": return pad2(hour12)
      case "h": return "" + hour12
      case "mm": return pad2(minutes)
      case "m": return "" + minutes
      case "ss": return pad2(seconds)
      case "s": return "" + seconds
      case "A": return hours < 12 ? L.am : L.pm
      case "a": return (hours < 12 ? L.am : L.pm).toLowerCase()
      default: return t
    }
  })
}

// Russian plural picks one of three forms by the number: 1 минута / 2 минуты / 5 минут.
const ruPlural = (n: number, one: string, few: string, many: string): string => {
  const mod10 = n % 10
  const mod100 = n % 100
  if (mod10 === 1 && mod100 !== 11) return one
  if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few
  return many
}

type RelUnit = { limit: number; secs: number; en: [string, string]; ru: [string, string, string] }

// Ordered thresholds. `limit` is the upper bound (in seconds) this unit covers; `secs` is the unit's
// length used to compute the count.
const REL_UNITS: readonly RelUnit[] = [
  { limit: 60, secs: 1, en: ["second", "seconds"], ru: ["секунду", "секунды", "секунд"] },
  { limit: 3600, secs: 60, en: ["minute", "minutes"], ru: ["минуту", "минуты", "минут"] },
  { limit: 86400, secs: 3600, en: ["hour", "hours"], ru: ["час", "часа", "часов"] },
  { limit: 2592000, secs: 86400, en: ["day", "days"], ru: ["день", "дня", "дней"] },
  { limit: 31536000, secs: 2592000, en: ["month", "months"], ru: ["месяц", "месяца", "месяцев"] },
  { limit: Infinity, secs: 31536000, en: ["year", "years"], ru: ["год", "года", "лет"] },
]

const timeAgoImpl = (ms: number, locale: Locale | undefined, nowMs: number): string => {
  const loc = resolveLocale(locale)
  const diffSecs = (nowMs - ms) / 1000
  const future = diffSecs < 0
  const abs = Math.abs(diffSecs)

  if (abs < 45) return loc === "ru" ? "только что" : "just now"

  let count = 1
  let unit = REL_UNITS[REL_UNITS.length - 1]
  for (const u of REL_UNITS) {
    if (abs < u.limit) { unit = u; count = Math.max(1, Math.round(abs / u.secs)); break }
  }

  if (loc === "ru") {
    const word = ruPlural(count, unit.ru[0], unit.ru[1], unit.ru[2])
    return future ? `через ${count} ${word}` : `${count} ${word} назад`
  }
  const word = count === 1 ? unit.en[0] : unit.en[1]
  return future ? `in ${count} ${word}` : `${count} ${word} ago`
}

// ---- the immutable wrapper ---------------------------------------------------------------------

/**
 * An immutable moment in time — the value returned by [`date()`]. Wraps a single epoch-millisecond
 * scalar (`t`); every method returns a new `DateValue` or a plain scalar, never mutating. Construct
 * it with `date(...)`, not `new`.
 */
export class DateValue {
  /** Epoch milliseconds — the one canonical scalar. Prefer `valueOf()`; this is public so `date(a)`
   *  can read it back cheaply and so tooling can treat it as the value's single component. */
  readonly t: number

  constructor(ms: number) {
    this.t = ms
  }

  // -- materialize (delegate to the free functions; the fuser can inline these to `*Impl(t, …)`) --

  /**
   * Format against a `day.js`-style token pattern (default `"D MMMM YYYY"`). Locale defaults to the
   * current device language.
   *
   * Tokens: `YYYY`/`YY` year · `MMMM`/`MMM`/`MM`/`M` month · `DD`/`D` day · `dddd`/`ddd` weekday ·
   * `HH`/`H` 24-hour · `hh`/`h` 12-hour · `mm`/`m` minute · `ss`/`s` second · `A`/`a` AM/PM.
   *
   * ```ts
   * date().format()                     // "10 July 2026"  /  "10 июля 2026"
   * date(ts).format("DD.MM.YYYY HH:mm") // "10.07.2026 14:05"
   * date(ts).format("MMMM YYYY", "ru")  // "Июль 2026"  (nominative — no day token)
   * ```
   */
  format(pattern = "D MMMM YYYY", locale?: Locale): string {
    return formatImpl(this.t, pattern, locale)
  }

  /**
   * Human relative time vs `now` (default: the current time) — "5 minutes ago", "in 2 days",
   * "5 минут назад", "через 2 дня". Reads as "just now" under ~45 seconds; handles Russian number
   * agreement (`1 минуту` / `2 минуты` / `5 минут`). Locale defaults to the device language.
   */
  timeAgo(locale?: Locale, now: DateInput = Date.now()): string {
    return timeAgoImpl(this.t, locale, toMs(now))
  }

  // -- arithmetic (pure, chainable; linear units are closed-form scalar math → fusable) -----------

  /** Add `n` of a unit, returning a new value. Linear units (`ms`…`week`) are scalar ms math;
   *  `month`/`year` are calendar-aware (clamp on short months, like day.js). */
  add(n: number, unit: Unit): DateValue {
    const ms = MS[unit]
    if (ms !== undefined) return new DateValue(this.t + n * ms)
    // Calendar units: shift month/year but clamp the day so Jan 31 + 1 month → Feb 28 (not Mar 3).
    const d = new Date(this.t)
    const day = d.getDate()
    d.setDate(1)
    if (unit === "year") d.setFullYear(d.getFullYear() + n)
    else d.setMonth(d.getMonth() + n) // "month"
    const daysInMonth = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate()
    d.setDate(Math.min(day, daysInMonth))
    return new DateValue(d.getTime())
  }

  /** Subtract `n` of a unit. Equivalent to `add(-n, unit)`. */
  subtract(n: number, unit: Unit): DateValue {
    return this.add(-n, unit)
  }

  /** Snap down to the start of a unit (local time): start of day/hour/month/year, etc. */
  startOf(unit: Unit): DateValue {
    const d = new Date(this.t)
    switch (unit) {
      case "year": d.setMonth(0); d.setDate(1); d.setHours(0, 0, 0, 0); break
      case "month": d.setDate(1); d.setHours(0, 0, 0, 0); break
      case "day": d.setHours(0, 0, 0, 0); break
      case "hour": d.setMinutes(0, 0, 0); break
      case "minute": d.setSeconds(0, 0); break
      case "second": d.setMilliseconds(0); break
      case "week": {
        d.setHours(0, 0, 0, 0)
        d.setDate(d.getDate() - d.getDay()) // week starts Sunday
        break
      }
    }
    return new DateValue(d.getTime())
  }

  /** Snap up to the end of a unit (local time): the last millisecond of the day/hour/month/… */
  endOf(unit: Unit): DateValue {
    return this.startOf(unit).add(1, unit).add(-1, "ms")
  }

  // -- scalar terminals (bare number / boolean → allocate nothing when fused) ---------------------

  /** Epoch milliseconds. Enables `+date(x)`, `date(a) < date(b)`, `date(a) - date(b)` via coercion. */
  valueOf(): number {
    return this.t
  }

  /** Whole seconds since the epoch (Unix time). */
  unix(): number {
    return Math.floor(this.t / 1000)
  }

  /** Signed difference to `other`, in whole `unit`s (default `ms`), truncated toward zero. */
  diff(other: DateInput, unit: Unit = "ms"): number {
    const raw = this.t - toMs(other)
    const per = MS[unit]
    if (per !== undefined) return Math.trunc(raw / per)
    // month/year: count calendar steps.
    const a = new Date(this.t)
    const b = new Date(toMs(other))
    let months = (a.getFullYear() - b.getFullYear()) * 12 + (a.getMonth() - b.getMonth())
    if (a.getDate() < b.getDate()) months -= Math.sign(months) || 0
    return unit === "year" ? Math.trunc(months / 12) : months
  }

  isBefore(other: DateInput): boolean { return this.t < toMs(other) }
  isAfter(other: DateInput): boolean { return this.t > toMs(other) }
  isSame(other: DateInput): boolean { return this.t === toMs(other) }

  // -- field accessors (scalar terminals) ---------------------------------------------------------

  year(): number { return new Date(this.t).getFullYear() }
  /** 0-based month (0 = January), matching `Date.getMonth`. */
  month(): number { return new Date(this.t).getMonth() }
  /** Day of the month, 1–31. */
  day(): number { return new Date(this.t).getDate() }
  /** Day of the week, 0 (Sunday)–6 (Saturday), matching `Date.getDay`. */
  weekday(): number { return new Date(this.t).getDay() }
  hour(): number { return new Date(this.t).getHours() }
  minute(): number { return new Date(this.t).getMinutes() }
  second(): number { return new Date(this.t).getSeconds() }

  /** A native `Date` snapshot of this value. */
  toDate(): Date {
    return new Date(this.t)
  }
}

/**
 * Create a `DateValue` — a locale-aware, immutable moment, dayjs-style. Accepts a `Date`, a
 * millisecond timestamp (`Date.now()`), a parseable string, or another `DateValue`; with no argument
 * it's the current time. Chain arithmetic and finish with `format` / `timeAgo`, both of which
 * understand **English** and **Russian** and default to the current device language
 * ([`device.language`](../runtime/device.ts)).
 *
 * ```ts
 * date().format("dddd, D MMMM")            // "Friday, 10 July"  /  "пятница, 10 июля"
 * date(ts).timeAgo()                       // "5 minutes ago"    /  "5 минут назад"
 * date().add(3, "day").startOf("day").format("D MMMM")
 * date(a).diff(b, "hour")                  // signed whole hours
 * date(a).isBefore(b)                      // boolean
 * ```
 */
export const date = (value: DateInput = Date.now()): DateValue => new DateValue(toMs(value))
