import type { ShareData } from "../sharing/contracts";
import type { PublicUser } from "../shared/user";
import { dayNumber } from "../habits/calendar";

export type CardPrivacy = { name: boolean; avatar: boolean; habitNames: boolean; timezone: boolean };
export const cardDate = (date: string) => new Date(`${date}T12:00:00Z`).toLocaleDateString("en", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
export const cardStats = (data: ShareData) => {
  const completed = data.habits.reduce((n, h) => n + h.completed, 0);
  const scheduled = data.habits.reduce((n, h) => n + h.scheduled, 0);
  return { completed, scheduled, percent: scheduled ? Math.round(completed / scheduled * 100) : null };
};

async function loadAvatar(url: string): Promise<HTMLImageElement | null> {
  return new Promise(resolve => {
    const img = new Image();
    const finish = (value: HTMLImageElement | null) => { clearTimeout(timer); img.onload = null; img.onerror = null; resolve(value); };
    const timer = setTimeout(() => finish(null), 5000);
    img.crossOrigin = "anonymous";
    img.onload = () => finish(img);
    img.onerror = () => finish(null);
    img.src = url;
  });
}

/** Render once: this exact PNG is previewed, downloaded, and sent to Discord. */
export async function renderProgressCard(data: ShareData, user: PublicUser, privacy: CardPrivacy): Promise<{ blob: Blob; alt: string; avatarMissing: boolean }> {
  await document.fonts.load('76px "Instrument Serif"');
  const [avatar, botAvatar] = await Promise.all([
    privacy.avatar && user.avatarUrl ? loadAvatar(user.avatarUrl) : null,
    data.botAvatarUrl ? loadAvatar(data.botAvatarUrl) : null,
  ]);
  const canvas = document.createElement("canvas");
  const longRange = data.habits[0]!.days.length > 42;
  const titleGraphGap = 12;
  const rowHeight = (longRange ? 280 : 250) + titleGraphGap;
  canvas.width = 1200; canvas.height = 380 + data.habits.length * rowHeight + 76;
  const ctx = canvas.getContext("2d");
  if (!ctx) throw new Error("Your browser could not create the image. Try another browser.");
  const left = 56;
  const right = 1144;
  const chartLeft = 80;
  const chartWidth = right - chartLeft;
  const legendColumns = [chartLeft, chartLeft + 266, chartLeft + 532, chartLeft + 798] as const;
  const sans = '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
  const text = (value: string, x: number, y: number, size = 22, color = "#666", serif = false, maxWidth = 1088, align: CanvasTextAlign = "left") => {
    ctx.font = `${size}px ${serif ? '"Instrument Serif", Georgia, serif' : sans}`;
    ctx.fillStyle = color;
    ctx.textAlign = align;
    let fitted = value;
    if (ctx.measureText(fitted).width > maxWidth) {
      const chars = Array.from(value);
      while (chars.length && ctx.measureText(`${chars.join("")}…`).width > maxWidth) chars.pop();
      fitted = `${chars.join("")}…`;
    }
    ctx.fillText(fitted, x, y);
  };
  const rect = (x: number, y: number, w: number, h: number, color: string) => { ctx.fillStyle = color; ctx.fillRect(x, y, w, h); };
  rect(0, 0, 1200, canvas.height, "#fff");
  rect(0, 0, 1200, 8, data.habits[0]?.color ?? "#111");
  let profileX = 56;
  if (privacy.avatar) {
    ctx.save(); ctx.beginPath(); ctx.arc(88, 84, 32, 0, Math.PI * 2); ctx.clip();
    rect(56, 52, 64, 64, "#f1f1f1");
    if (avatar) ctx.drawImage(avatar, 56, 52, 64, 64);
    else { ctx.fillStyle = "#888"; ctx.beginPath(); ctx.arc(88, 77, 10, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(88, 109, 22, 0, Math.PI * 2); ctx.fill(); }
    ctx.restore(); profileX = 138;
  }
  text(privacy.name ? user.displayName || user.username : "A little, every day.", profileX, 94, 26, "#111", false, 700);
  if (botAvatar) {
    ctx.font = '34px "Instrument Serif", Georgia, serif';
    const avatarX = right - ctx.measureText("minabot.").width - 12 - 40;
    ctx.save();
    ctx.beginPath();
    ctx.roundRect(avatarX, 64, 40, 40, 4);
    ctx.clip();
    ctx.drawImage(botAvatar, avatarX, 64, 40, 40);
    ctx.restore();
  }
  text("minabot.", right, 94, 34, "#111", true, 112, "right");
  text("Small steps, adding up.", left, 213, 76, "#111", true);
  text(`${cardDate(data.from)} — ${cardDate(data.to)}`, left, 260, 23);
  const stats = cardStats(data);
  text(`${stats.completed} / ${stats.scheduled} scheduled check-ins complete`, left, 317, 24, "#111", false, 800);
  text(stats.percent === null ? "No days scheduled" : `${stats.percent}% complete`, right, 317, 24, "#111", false, 240, "right");
  data.habits.forEach((habit, index) => {
    const top = 358 + index * rowHeight;
    rect(left, top, right - left, 1, "#dedede");
    rect(left, top + 27, 6, 24, habit.color);
    text(privacy.habitNames ? habit.name : `Habit ${index + 1}`, chartLeft, top + 48, 24, "#111", false, 760);
    text(`${habit.completed} / ${habit.scheduled} days`, right, top + 48, 21, "#666", false, 185, "right");
    ctx.save();
    ctx.translate(0, titleGraphGap);
    if (habit.days.length <= 42) {
      const step = (chartWidth + 5) / habit.days.length;
      habit.days.forEach((day, i) => {
        const x = chartLeft + i * step;
        if (day.due) rect(x, top + 78, Math.max(8, step - 5), 62, day.color);
        else { ctx.fillStyle = "#aaa"; ctx.beginPath(); ctx.arc(x + (step - 5) / 2, top + 109, 3, 0, Math.PI * 2); ctx.fill(); }
        if (habit.days.length <= 7 || i % 7 === 0) text(day.date.slice(8), x + (step - 5) / 2, top + 169, 17, "#666", false, step - 5, "center");
      });
    } else {
      const offset = (new Date(`${data.from}T12:00:00Z`).getUTCDay() + 6) % 7;
      const columns = Math.ceil((offset + habit.days.length) / 7);
      const step = (chartWidth + 4) / columns;
      const startX = chartLeft;
      text("M", left, top + 95, 14); text("W", left, top + 135, 14); text("F", left, top + 175, 14);
      let previousMonthX = -100;
      habit.days.forEach((day, i) => {
        const n = offset + i, x = startX + Math.floor(n / 7) * step, y = top + 82 + n % 7 * 20;
        if ((i === 0 || day.date.endsWith("-01")) && x - previousMonthX > 45 && x < 1108) {
          text(new Date(`${day.date}T12:00:00Z`).toLocaleDateString("en", { month: "short", timeZone: "UTC" }), x, top + 70, 14);
          previousMonthX = x;
        }
        if (day.due) rect(x, y, step - 4, 16, day.color);
        else { ctx.fillStyle = "#aaa"; ctx.beginPath(); ctx.arc(x + (step - 4) / 2, y + 8, 2, 0, Math.PI * 2); ctx.fill(); }
      });
    }
    const legendY = top + (longRange ? 241 : 209);
    rect(legendColumns[0], legendY, 16, 16, habit.legend.empty);
    text("No progress", legendColumns[0] + 26, legendY + 15, 18);
    if (habit.legend.partial.length) {
      habit.legend.partial.forEach((color, i) => rect(legendColumns[1] + i * 19, legendY, 16, 16, color));
      text("Partial", legendColumns[1] + habit.legend.partial.length * 19 + 10, legendY + 15, 18);
    }
    rect(legendColumns[2], legendY, 16, 16, habit.legend.complete);
    text("Complete", legendColumns[2] + 26, legendY + 15, 18);
    ctx.fillStyle = "#aaa"; ctx.beginPath(); ctx.arc(legendColumns[3] + 8, legendY + 8, 3, 0, Math.PI * 2); ctx.fill();
    text("Not scheduled", legendColumns[3] + 26, legendY + 15, 18);
    ctx.restore();
  });
  const footerY = canvas.height - 46;
  text("Days off don’t count against you.", left, footerY, 19);
  text(privacy.timezone ? data.timezone : `${dayNumber(data.to) - dayNumber(data.from) + 1} days of small steps`, right, footerY, 19, "#666", false, 350, "right");
  const blob = await new Promise<Blob>((resolve, reject) => canvas.toBlob(value => value ? resolve(value) : reject(new Error("Could not export the card.")), "image/png"));
  const names = privacy.habitNames ? data.habits.map(h => h.name).join(", ") : `${data.habits.length} habits`;
  return { blob, avatarMissing: privacy.avatar && !!user.avatarUrl && !avatar,
    alt: `${privacy.name ? `${user.displayName || user.username}: ` : ""}${names}. ${cardDate(data.from)} to ${cardDate(data.to)}. ${stats.completed} of ${stats.scheduled} scheduled check-ins complete. Legend: empty squares mean no progress, intermediate shades mean partial progress, full color means complete, and dots mean not scheduled.${privacy.timezone ? ` ${data.timezone}.` : ""}` };
}
