/**
 * Timezone-aware ISO-week helpers for the analytics module.
 *
 * Weeks are anchored to Monday 00:00 in the configured IANA timezone.
 * We avoid adding a new dep (date-fns-tz, luxon, …) by walking dates with
 * `Intl.DateTimeFormat`. That's enough for bucketing — we don't do any
 * duration arithmetic, only "get the Monday of the ISO week containing X".
 */

const WALL_PARTS = ["year", "month", "day", "weekday"] as const;

interface WallParts {
  year: number;
  month: number;
  day: number;
  weekday: number; // 1 = Monday … 7 = Sunday
}

function getWallParts(date: Date, tz: string): WallParts {
  const fmt = new Intl.DateTimeFormat("en-US", {
    timeZone: tz,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    weekday: "short",
  });
  const parts = fmt.formatToParts(date);
  const lookup = Object.fromEntries(parts.map((p) => [p.type, p.value])) as Record<string, string>;
  const weekdayMap: Record<string, number> = {
    Mon: 1,
    Tue: 2,
    Wed: 3,
    Thu: 4,
    Fri: 5,
    Sat: 6,
    Sun: 7,
  };
  const weekdayLabel = lookup.weekday ?? "Mon";
  const result: WallParts = {
    year: Number(lookup.year),
    month: Number(lookup.month),
    day: Number(lookup.day),
    weekday: weekdayMap[weekdayLabel] ?? 1,
  };
  for (const key of WALL_PARTS) {
    if (key === "weekday") continue;
    if (!Number.isFinite(result[key as "year" | "month" | "day"])) {
      throw new Error(`week-buckets: could not read ${key} from ${date.toISOString()} in ${tz}`);
    }
  }
  return result;
}

/** Format a YYYY-MM-DD ISO date in UTC from a wall-time (Y, M, D). */
export function formatISODate(year: number, month: number, day: number): string {
  const mm = String(month).padStart(2, "0");
  const dd = String(day).padStart(2, "0");
  return `${year}-${mm}-${dd}`;
}

/**
 * Returns the "YYYY-MM-DD" of the Monday of the ISO week that contains `date`,
 * interpreted in the given IANA timezone.
 */
export function mondayOf(date: Date, tz: string): string {
  const parts = getWallParts(date, tz);
  // Walk back (weekday - 1) calendar days from (year, month, day).
  const daysBack = parts.weekday - 1;
  // Convert to a UTC anchor we can subtract from reliably. We treat wall date
  // as if UTC (no tz offset) — fine because we only need calendar arithmetic.
  const anchor = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
  anchor.setUTCDate(anchor.getUTCDate() - daysBack);
  return formatISODate(anchor.getUTCFullYear(), anchor.getUTCMonth() + 1, anchor.getUTCDate());
}

/** Returns YYYY-MM-DD N days after the given ISO date (UTC calendar math). */
export function addDaysISO(isoDate: string, days: number): string {
  const parts = isoDate.split("-").map((s) => Number(s));
  const year = parts[0] ?? NaN;
  const month = parts[1] ?? NaN;
  const day = parts[2] ?? NaN;
  if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
    throw new Error(`addDaysISO: bad input "${isoDate}"`);
  }
  const anchor = new Date(Date.UTC(year, month - 1, day));
  anchor.setUTCDate(anchor.getUTCDate() + days);
  return formatISODate(anchor.getUTCFullYear(), anchor.getUTCMonth() + 1, anchor.getUTCDate());
}

/**
 * Builds a list of `{ weekStart, weekEnd }` ISO-date pairs covering the span
 * from the Monday-of-`start` up to (but not including) the Monday-of-or-after-`end`.
 *
 * Each pair represents a half-open interval `[weekStart, weekEnd)`.
 */
export function weekBucketsBetween(
  start: Date,
  end: Date,
  tz: string,
): Array<{ weekStart: string; weekEnd: string }> {
  if (end.getTime() < start.getTime()) return [];
  const firstMonday = mondayOf(start, tz);
  const endMonday = mondayOf(end, tz);
  const buckets: Array<{ weekStart: string; weekEnd: string }> = [];
  let cursor = firstMonday;
  // Include the bucket that contains `end`.
  while (cursor <= endMonday) {
    const weekEnd = addDaysISO(cursor, 7);
    buckets.push({ weekStart: cursor, weekEnd });
    cursor = weekEnd;
  }
  return buckets;
}
