export const PROGRESS_SHADES = [
  "#eeeeee",
  "#d8d8d8",
  "#bfbfbf",
  "#a5a5a5",
  "#8a8a8a",
  "#707070",
  "#555555",
  "#363636",
  "#111111",
] as const;
export type CalendarDay = {
  color?: string;
  unit?: string;
  date: string;
  value: number;
  target: number;
  state: "due" | "not-due" | "future";
};

export const MONTH_VIEWS = [3, 4, 6, 12] as const;
export type MonthView = (typeof MONTH_VIEWS)[number];

/** Whole calendar months, including the month containing the anchor date. */
export function monthWindow(anchor: string, months: MonthView) {
  const date = new Date(`${anchor}T12:00:00Z`);
  const first = new Date(
    Date.UTC(date.getUTCFullYear(), date.getUTCMonth() - months + 1, 1),
  );
  const last = new Date(
    Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0),
  );
  return {
    from: first.toISOString().slice(0, 10),
    to: last.toISOString().slice(0, 10),
  };
}

export function visibleCalendarDays(
  days: CalendarDay[],
  months: MonthView,
  anchor: string,
) {
  const range = monthWindow(anchor, months);
  return days.filter((day) => day.date >= range.from && day.date <= range.to);
}

/** One uninterrupted Sunday-first timeline, including partial boundary weeks. */
export function calendarTimeline(days: CalendarDay[]) {
  const first = days[0];
  if (!first) return { weeks: 0, entries: [], months: [] };
  const start = new Date(`${first.date}T00:00:00Z`);
  const origin = start.getTime() - start.getUTCDay() * 86_400_000;
  const entries = days.map((day, index) => {
    const offset = Math.round((Date.parse(`${day.date}T00:00:00Z`) - origin) / 86_400_000);
    return { day, index, column: Math.floor(offset / 7) + 1, row: offset % 7 + 1 };
  });
  const weeks = entries.at(-1)!.column;
  const months = entries.filter(({ day }, index) =>
    index === 0 || day.date.slice(0, 7) !== entries[index - 1]!.day.date.slice(0, 7),
  ).map(({ day, column }) => ({ month: day.date.slice(0, 7), column }))
    // A short custom range can cross a month boundary inside a single week.
    // The exact date-range caption names both; avoid overlapping axis labels.
    .filter((month, index, labels) => index === 0 || month.column !== labels[index - 1]!.column);
  return { weeks, entries, months };
}

export const HABIT_COLORS = {
  water: "#426582",
  reading: "#977344",
  tasks: "#79618d",
  movement: "#58765b",
} as const;

export function progressShade(day: CalendarDay, color = "#111111") {
  if (day.color) return day.color;
  if (day.state === "not-due") return "#ffffff";
  if (day.state === "future") return PROGRESS_SHADES[0];
  if (day.target <= 0 || day.value <= 0) return PROGRESS_SHADES[0];
  const step = Math.max(
    1,
    Math.min(8, Math.round((day.value / day.target) * 8)),
  );
  if (color === "#111111" || !/^#[0-9a-f]{6}$/i.test(color))
    return PROGRESS_SHADES[step]!;
  const weight = step / 8;
  return `#${[1, 3, 5]
    .map((offset) =>
      Math.round(
        238 + (parseInt(color.slice(offset, offset + 2), 16) - 238) * weight,
      )
        .toString(16)
        .padStart(2, "0"),
    )
    .join("")}`;
}

export function describeDay(day: CalendarDay, unit: string) {
  if (day.state === "future") return "Upcoming · Logging is not available yet";
  if (day.state === "not-due")
    return "Nothing scheduled · Not included in the score";
  return `${day.value} of ${day.target} ${day.unit ?? unit} · ${day.value >= day.target ? "Complete" : "Incomplete"}`;
}

/** Only show shades that the chart's completion targets can produce. */
export function legendShades(days: CalendarDay[], color = "#111111") {
  const levels = new Set<number>();
  for (const target of new Set(days.map((day) => day.target))) {
    if (!Number.isFinite(target) || target <= 0) continue;
    for (let value = 1; value <= Math.min(target, 8); value++) {
      levels.add(
        target > 8 ? value : Math.max(1, Math.round((value / target) * 8)),
      );
    }
  }
  return [
    PROGRESS_SHADES[0],
    ...[...levels]
      .sort((a, b) => a - b)
      .map((value) =>
        progressShade({ date: "", value, target: 8, state: "due" }, color),
      ),
  ];
}

export function combinedProgress(
  habits: { value: number; target: number; due: boolean }[],
) {
  const due = habits.filter((habit) => habit.due && habit.target > 0);
  return {
    completed: due.filter((habit) => habit.value >= habit.target).length,
    due: due.length,
  };
}

/** A full 12 calendar months of stable demo history, including future dates. */
export function demoCalendar(value: number, target: number): CalendarDay[] {
  return Array.from({ length: 365 }, (_, index) => {
    const timestamp = Date.UTC(2025, 9, 1 + index);
    // Keep the previously displayed dates' fixture values unchanged.
    const fixtureIndex = (timestamp - Date.UTC(2026, 2, 8)) / 86_400_000;
    const rawValue = fixtureIndex * 17 + Math.floor(fixtureIndex / 7) * 3;
    const date = new Date(timestamp).toISOString().slice(0, 10);
    return {
      date,
      target,
      value:
        date === "2026-09-04"
          ? value
          : ((rawValue % (target + 1)) + target + 1) % (target + 1),
      state:
        date > "2026-09-04"
          ? "future"
          : fixtureIndex % 19 === 0
            ? "not-due"
            : "due",
    };
  });
}
