import { mkdir, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";

const OWNER = "jasonsuhari";
const REPO = "gridbash";
const MILESTONE = 130;
const OUTPUT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "outputs");

const githubHeaders = {
  Accept: "application/vnd.github+json",
  "X-GitHub-Api-Version": "2022-11-28",
  "User-Agent": "gridbash-launch-history-artifact",
};

if (process.env.GITHUB_TOKEN) {
  githubHeaders.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}

async function fetchJson(url, headers = {}) {
  const response = await fetch(url, { headers: { ...githubHeaders, ...headers } });
  if (!response.ok) {
    const body = await response.text();
    throw new Error(`${response.status} ${response.statusText}: ${body.slice(0, 300)}`);
  }
  return response.json();
}

async function fetchAll(endpoint, headers = {}) {
  const rows = [];
  for (let page = 1; ; page += 1) {
    const joiner = endpoint.includes("?") ? "&" : "?";
    const batch = await fetchJson(
      `https://api.github.com${endpoint}${joiner}per_page=100&page=${page}`,
      headers,
    );
    rows.push(...batch);
    if (batch.length < 100) break;
  }
  return rows;
}

function utcDay(value) {
  return new Date(value).toISOString().slice(0, 10);
}

function durationParts(start, end) {
  let remaining = new Date(end).getTime() - new Date(start).getTime();
  const days = Math.floor(remaining / 86_400_000);
  remaining -= days * 86_400_000;
  const hours = Math.floor(remaining / 3_600_000);
  remaining -= hours * 3_600_000;
  const minutes = Math.floor(remaining / 60_000);
  const seconds = Math.floor((remaining - minutes * 60_000) / 1_000);
  return { days, hours, minutes, seconds };
}

function dailyStarHistory(stars, throughIndex = stars.length) {
  const counts = new Map();
  for (const star of stars.slice(0, throughIndex)) {
    const day = utcDay(star.starred_at);
    counts.set(day, (counts.get(day) ?? 0) + 1);
  }

  let cumulative = 0;
  return [...counts.entries()].map(([date, starsOnDay]) => {
    cumulative += starsOnDay;
    return { date, starsOnDay, cumulative };
  });
}

function csv(rows) {
  const header = "date,stars_on_day,cumulative";
  const body = rows.map((row) => `${row.date},${row.starsOnDay},${row.cumulative}`);
  return `${[header, ...body].join("\n")}\n`;
}

function svgChart(rows, milestoneAt) {
  const width = 1400;
  const height = 820;
  const plot = { left: 115, top: 120, right: 1310, bottom: 670 };
  const x = (index) => plot.left + (index / Math.max(1, rows.length - 1)) * (plot.right - plot.left);
  const y = (value) => plot.bottom - (value / MILESTONE) * (plot.bottom - plot.top);
  const points = rows.map((row, index) => `${x(index).toFixed(1)},${y(row.cumulative).toFixed(1)}`).join(" ");
  const gridLines = [0, 25, 50, 75, 100, 130]
    .map((value, index) => {
      const yy = y(value).toFixed(1);
      const wobble = index % 2 === 0 ? 5 : -4;
      return `<path d="M ${plot.left + wobble} ${yy} L ${plot.right - wobble} ${Number(yy) + wobble / 3}" stroke="#b7b7b7" stroke-width="3" stroke-dasharray="10 14" fill="none"/><text x="${plot.left - 28}" y="${Number(yy) + 9}" text-anchor="end">${value}</text>`;
    })
    .join("\n");
  const labels = rows
    .map((row, index) => `<text x="${x(index).toFixed(1)}" y="${plot.bottom + 48 + (index % 2) * 25}" text-anchor="middle">${row.date.slice(5)}</text>`)
    .join("\n");

  return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="title desc">
  <title id="title">GridBash cumulative GitHub stars through star 130</title>
  <desc id="desc">An exact cumulative chart generated from timestamped GitHub stargazer data. GridBash reached 130 stars on ${milestoneAt}.</desc>
  <rect width="100%" height="100%" fill="#ffffff"/>
  <g font-family="Comic Sans MS, Segoe Print, sans-serif" font-size="27" fill="#111111">
    <text x="80" y="62" font-size="42" font-weight="700">GridBash stars, drawn with severe mouse damage</text>
    <text x="82" y="98" font-size="22">Exact data; intentionally suspicious visual technique.</text>
    ${gridLines}
    <path d="M ${plot.left - 6} ${plot.top - 16} L ${plot.left + 3} ${plot.bottom + 10} L ${plot.right + 10} ${plot.bottom - 5}" stroke="#111111" stroke-width="8" fill="none" stroke-linecap="round"/>
    <polyline points="${points}" stroke="#2457ff" stroke-width="13" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
    ${rows.map((row, index) => `<circle cx="${x(index).toFixed(1)}" cy="${y(row.cumulative).toFixed(1)}" r="10" fill="#ffdf00" stroke="#111111" stroke-width="4"><title>${row.date}: ${row.starsOnDay} new, ${row.cumulative} cumulative</title></circle>`).join("\n")}
    ${labels}
    <path d="M ${plot.right - 30} ${plot.top + 6} l 27 -35 l 22 40 l 43 -6 l -30 31 l 21 42 l -46 -18 l -35 30 l 3 -47 l -39 -23 z" fill="#ffdf00" stroke="#111111" stroke-width="5"/>
    <text x="${plot.right - 285}" y="${plot.top + 35}" font-size="30" font-weight="700">star #130</text>
    <text x="${plot.right - 285}" y="${plot.top + 70}" font-size="22">${milestoneAt}</text>
    <text x="700" y="790" text-anchor="middle" font-size="22">Source: GitHub timestamped stargazer API; generated by collect-public-history.mjs</text>
  </g>
</svg>`;
}

async function main() {
  await mkdir(OUTPUT_DIR, { recursive: true });

  const repository = await fetchJson(`https://api.github.com/repos/${OWNER}/${REPO}`);
  const stars = await fetchAll(`/repos/${OWNER}/${REPO}/stargazers`, {
    Accept: "application/vnd.github.star+json",
  });
  stars.sort((a, b) => new Date(a.starred_at) - new Date(b.starred_at));

  if (stars.length < MILESTONE) {
    throw new Error(`Expected at least ${MILESTONE} current stargazers, received ${stars.length}.`);
  }

  const milestoneAt = stars[MILESTONE - 1].starred_at;
  const firstStarAt = stars[0].starred_at;
  const commits = await fetchAll(
    `/repos/${OWNER}/${REPO}/commits?sha=${encodeURIComponent(repository.default_branch)}&until=${encodeURIComponent(milestoneAt)}`,
  );
  const pulls = await fetchAll(`/repos/${OWNER}/${REPO}/pulls?state=all`);
  const issuesAndPulls = await fetchAll(`/repos/${OWNER}/${REPO}/issues?state=all`);
  const releases = await fetchAll(`/repos/${OWNER}/${REPO}/releases`);
  const npm = await fetchJson(
    `https://api.npmjs.org/downloads/range/${utcDay(repository.created_at)}:${utcDay(milestoneAt)}/${REPO}`,
    { Accept: "application/json" },
  );

  const milestoneRows = dailyStarHistory(stars, MILESTONE);
  const snapshot = {
    methodology: {
      collectedAt: new Date().toISOString(),
      milestone: MILESTONE,
      note: "GitHub exposes timestamps for current stargazers, not an immutable star event ledger. The milestone is reconstructed from the ordered current-stargazer list at collection time.",
      publicOnly: true,
    },
    repository: {
      fullName: repository.full_name,
      url: repository.html_url,
      createdAt: repository.created_at,
      defaultBranch: repository.default_branch,
      currentStarsAtCollection: stars.length,
      currentForksAtCollection: repository.forks_count,
      language: repository.language,
    },
    milestone: {
      firstStarAt,
      star100At: stars[99].starred_at,
      star130At: milestoneAt,
      repositoryToFirstStar: durationParts(repository.created_at, firstStarAt),
      repositoryToStar100: durationParts(repository.created_at, stars[99].starred_at),
      repositoryToStar130: durationParts(repository.created_at, milestoneAt),
      firstStarToStar130: durationParts(firstStarAt, milestoneAt),
    },
    stateAtStar130: {
      commitsReachableFromDefaultBranch: commits.length,
      mergedPullRequests: pulls.filter((pull) => pull.merged_at && new Date(pull.merged_at) <= new Date(milestoneAt)).length,
      openedNonPullIssues: issuesAndPulls.filter((issue) => !issue.pull_request && new Date(issue.created_at) <= new Date(milestoneAt)).length,
      publishedGitHubReleases: releases.filter((release) => release.published_at && new Date(release.published_at) <= new Date(milestoneAt)).length,
      npmDownloadsThroughMilestoneDay: npm.downloads.reduce((sum, row) => sum + row.downloads, 0),
      npmCaution: "npm download counts include installs by CI, release checks, mirrors, and other automation; they are not a user count.",
    },
    starsByUtcDayThrough130: milestoneRows,
    releasesThrough130: releases
      .filter((release) => release.published_at && new Date(release.published_at) <= new Date(milestoneAt))
      .sort((a, b) => new Date(a.published_at) - new Date(b.published_at))
      .map((release) => ({ tag: release.tag_name, publishedAt: release.published_at, prerelease: release.prerelease })),
  };

  await writeFile(path.join(OUTPUT_DIR, "public-history.json"), `${JSON.stringify(snapshot, null, 2)}\n`);
  await writeFile(path.join(OUTPUT_DIR, "star-history.csv"), csv(milestoneRows));
  await writeFile(path.join(OUTPUT_DIR, "star-history-ms-paint.svg"), svgChart(milestoneRows, milestoneAt));
  process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
}

await main();
