import { _now, _today, _tomorrow, _nextDayOfWeek, _atTime, _startOfDay, _endOfDay, _startOfWeek, _endOfWeek, _startOfMonth, _endOfMonth, _format, _formatDate, _formatDuration, _parse } from "agency-lang/stdlib-lib/date.js"
import { _advanceTimeImpl } from "agency-lang/stdlib-lib/builtins.js"

/** @module
@summary Work with instants as epoch-millisecond numbers, and calendar dates as strings.
An instant — a specific moment in time — is a number: milliseconds since 1970,
the same value JavaScript's Date.now() gives. Do math on it directly with the
usual operators and duration literals: `now() + 2h`, `deadline - now()`. A
calendar date — a whole day, like "today" — stays a "YYYY-MM-DD" string, because
which day it is depends on the timezone. Use `format` to turn an instant into a
readable string, `formatDate` for its calendar date, and `parse` to read an ISO
string back into an instant.

```ts
import { now, atTime, nextDayOfWeek, format } from "std::date"
import { createEvent } from "std::calendar"

node main() {
  // 3pm next Monday Pacific for one hour
  const tz = "America/Los_Angeles"
  const start = atTime(nextDayOfWeek("monday", tz), "15:00", tz)
  createEvent(summary: "Dentist", start: format(start, tz), end: format(start + 1h, tz))
}
```
*/

/** A day of the week, lowercase. */
export type DayOfWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday"

/** Get the current instant as epoch milliseconds. */
export def now(): number {
  """
  Get the current instant as epoch milliseconds (a number). To display it as a
  string, use format(now(), timezone). An instant is absolute and has no
  timezone of its own.
  """
  return _now()
}

/** Test-only seam (import test { _advanceTime }): advance the fake clock so a
time guard trips without spending real time. Only works under a test case with
"fakeClock": true; throws otherwise. */
def _advanceTime(ms: number) {
  """
  Test-only: move the fake clock forward `ms` milliseconds, tripping any time
  guard whose budget is now exceeded.

  @param ms - How many milliseconds of fake time to advance.
  """
  _advanceTimeImpl(ms)
}

/** Get today's date as a YYYY-MM-DD string. */
export def today(timezone: string = ""): string {
  """
  Get today's date as a YYYY-MM-DD string (e.g. "2026-05-05").

  @param timezone - IANA timezone name (defaults to the local timezone)
  """
  return _today(timezone)
}

/** Get tomorrow's date as a YYYY-MM-DD string. */
export def tomorrow(timezone: string = ""): string {
  """
  Get tomorrow's date as a YYYY-MM-DD string (e.g. "2026-05-06").

  @param timezone - IANA timezone name (defaults to the local timezone)
  """
  return _tomorrow(timezone)
}

/** Get the date of the next occurrence of a day of the week (e.g. "monday"). */
export def nextDayOfWeek(day: DayOfWeek, timezone: string = ""): string {
  """
  Get the next occurrence of a given day of the week as a YYYY-MM-DD string. For example, passing "tuesday" returns the date of next Tuesday.

  @param day - The day of the week
  @param timezone - IANA timezone name (defaults to the local timezone)
  """
  return _nextDayOfWeek(day, timezone)
}

/** Get the instant of a wall-clock time on a calendar date, in a timezone. */
export def atTime(date: string, time: string, timezone: string = ""): number {
  """
  Get the instant (epoch milliseconds) of a wall-clock time on a calendar date,
  in a timezone. Example: atTime("2026-05-05", "09:00", "America/New_York").
  Throws if the date or time cannot be parsed, so bad input fails loudly instead
  of becoming a silent NaN.

  @param date - The calendar date, "YYYY-MM-DD"
  @param time - The wall-clock time, "HH:MM" or "HH:MM:SS"
  @param timezone - IANA timezone name (defaults to local)
  """
  return _atTime(date, time, timezone)
}

/** Get midnight of the day containing an instant, as epoch milliseconds. */
export def startOfDay(instant?: number, timezone: string = ""): number {
  """
  Get midnight (00:00:00) of the day containing `instant`, in a timezone, as
  epoch milliseconds. Defaults to now(). Display it with format(...).

  @param instant - The instant whose day to use (defaults to now())
  @param timezone - IANA timezone name (defaults to local)
  """
  const at = instant ?? now()
  return _startOfDay(at, timezone)
}

/** Get the last millisecond of the day containing an instant. */
export def endOfDay(instant?: number, timezone: string = ""): number {
  """
  Get the last millisecond (23:59:59.999) of the day containing `instant`, in a
  timezone, as epoch milliseconds. Defaults to now(). An instant is always
  within [startOfDay, endOfDay] of its own day.

  @param instant - The instant whose day to use (defaults to now())
  @param timezone - IANA timezone name (defaults to local)
  """
  const at = instant ?? now()
  return _endOfDay(at, timezone)
}

/** Get midnight on Sunday of the week containing an instant. */
export def startOfWeek(instant?: number, timezone: string = ""): number {
  """
  Get midnight on Sunday of the week containing `instant`, in a timezone, as
  epoch milliseconds. Weeks begin on Sunday. Defaults to now().

  @param instant - An instant within the week (defaults to now())
  @param timezone - IANA timezone name (defaults to local)
  """
  const at = instant ?? now()
  return _startOfWeek(at, timezone)
}

/** Get the last millisecond of Saturday of the week containing an instant. */
export def endOfWeek(instant?: number, timezone: string = ""): number {
  """
  Get the last millisecond (23:59:59.999) of Saturday of the week containing
  `instant`, in a timezone, as epoch milliseconds. Weeks end on Saturday.
  Defaults to now().

  @param instant - An instant within the week (defaults to now())
  @param timezone - IANA timezone name (defaults to local)
  """
  const at = instant ?? now()
  return _endOfWeek(at, timezone)
}

/** Get midnight on the 1st of the month containing an instant. */
export def startOfMonth(instant?: number, timezone: string = ""): number {
  """
  Get midnight on the 1st of the month containing `instant`, in a timezone, as
  epoch milliseconds. Defaults to now().

  @param instant - An instant within the month (defaults to now())
  @param timezone - IANA timezone name (defaults to local)
  """
  const at = instant ?? now()
  return _startOfMonth(at, timezone)
}

/** Get the last millisecond of the last day of the month containing an instant. */
export def endOfMonth(instant?: number, timezone: string = ""): number {
  """
  Get the last millisecond (23:59:59.999) of the last day of the month
  containing `instant`, in a timezone, as epoch milliseconds. Defaults to now().

  @param instant - An instant within the month (defaults to now())
  @param timezone - IANA timezone name (defaults to local)
  """
  const at = instant ?? now()
  return _endOfMonth(at, timezone)
}

/** Format an instant as an ISO 8601 string with milliseconds and offset. */
export def format(ms: number, timezone: string = ""): string {
  """
  Format an instant (epoch milliseconds) as an ISO 8601 string with
  milliseconds and offset, e.g. "2026-05-05T10:30:00.123-07:00".

  @param ms - The instant to format
  @param timezone - IANA timezone name (defaults to local)
  """
  return _format(ms, timezone)
}

/** Format an instant as the "YYYY-MM-DD" calendar date it falls on. */
export def formatDate(ms: number, timezone: string = ""): string {
  """
  Format an instant as the "YYYY-MM-DD" calendar date it falls on in a timezone.

  @param ms - The instant to format
  @param timezone - IANA timezone name (defaults to local)
  """
  return _formatDate(ms, timezone)
}

/** Parse an ISO 8601 datetime string into an instant (epoch milliseconds). */
export def parse(iso: string): number {
  """
  Parse an ISO 8601 datetime string into an instant (epoch milliseconds). Throws
  if the string cannot be parsed, so bad input fails loudly instead of becoming
  a silent NaN. Strictness matches JavaScript's Date, not RFC 3339: loosely
  valid strings like "2026" or "2026-05" are accepted.

  @param iso - The ISO 8601 datetime string
  """
  return _parse(iso)
}

/** Render a millisecond duration as a readable string like "5m 32s". */
export def formatDuration(ms: number): string {
  """
  Render a duration in milliseconds as a readable string like "5m 32s" or
  "2w 3d". Whole-second granularity; largest unit is weeks.

  @param ms - The duration in milliseconds
  """
  return _formatDuration(ms)
}

/** How much time has elapsed since an instant, as a readable duration string. */
export def elapsedTime(since: number): string {
  """
  How much time has elapsed since the given instant, as a readable duration
  like "5m 32s". Capture the start with now().

  @param since - The starting instant, from now()
  """
  return formatDuration(now() - since)
}
