/**
 * OpenGraph image generation for posts without photos.
 * Uses Satori (layout → SVG) + @resvg/resvg-js (SVG → PNG).
 * Generated images are cached in .cache/og/ and passthrough-copied to output.
 *
 * Card design inspired by GitHub's OG images: light background, clean
 * typography hierarchy, avatar, metadata row, and accent color bar.
 */

import satori from "satori";
import { Resvg } from "@resvg/resvg-js";
import {
  readFileSync,
  writeFileSync,
  mkdirSync,
  existsSync,
  readdirSync,
} from "node:fs";
import { resolve, join, basename, dirname } from "node:path";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
import matter from "gray-matter";

const __dirname = dirname(fileURLToPath(import.meta.url));
const WIDTH = 1200;
const HEIGHT = 630;

// Card design version — bump to force full regeneration
const DESIGN_VERSION = 3;

const COLORS = {
  bg: "#ffffff",
  title: "#24292f",
  description: "#57606a",
  meta: "#57606a",
  accent: "#3b82f6",
  badge: "#ddf4ff",
  badgeText: "#0969da",
  border: "#d8dee4",
  bar: "#3b82f6",
};

const POST_TYPE_MAP = {
  articles: "Article",
  notes: "Note",
  bookmarks: "Bookmark",
  photos: "Photo",
  likes: "Like",
  replies: "Reply",
  reposts: "Repost",
  pages: "Page",
  videos: "Video",
  audio: "Audio",
  jams: "Jam",
  rsvps: "RSVP",
  events: "Event",
};

let avatarDataUri = null;

function loadAvatar() {
  if (avatarDataUri) return avatarDataUri;
  const avatarPath = resolve(__dirname, "..", "images", "svemagie.jpg");
  if (existsSync(avatarPath)) {
    const buf = readFileSync(avatarPath);
    avatarDataUri = `data:image/jpeg;base64,${buf.toString("base64")}`;
  }
  return avatarDataUri;
}

function loadFonts() {
  const fontsDir = resolve(
    __dirname,
    "..",
    "node_modules",
    "@fontsource",
    "inter",
    "files",
  );
  return [
    {
      name: "Inter",
      data: readFileSync(join(fontsDir, "inter-latin-400-normal.woff")),
      weight: 400,
      style: "normal",
    },
    {
      name: "Inter",
      data: readFileSync(join(fontsDir, "inter-latin-700-normal.woff")),
      weight: 700,
      style: "normal",
    },
  ];
}

function computeHash(title, description, date, postType, siteName) {
  return createHash("md5")
    .update(`v${DESIGN_VERSION}|${title}|${description}|${date}|${postType}|${siteName}`)
    .digest("hex")
    .slice(0, 12);
}

function detectPostType(filePath) {
  const parts = filePath.split("/");
  const contentIdx = parts.indexOf("content");
  if (contentIdx >= 0 && contentIdx + 1 < parts.length) {
    const typeDir = parts[contentIdx + 1];
    if (POST_TYPE_MAP[typeDir]) return POST_TYPE_MAP[typeDir];
  }
  return "Post";
}

function formatDate(dateStr) {
  if (!dateStr) return "";
  try {
    const d = new Date(dateStr);
    if (Number.isNaN(d.getTime())) return "";
    return d.toLocaleDateString("en-US", {
      year: "numeric",
      month: "short",
      day: "numeric",
    });
  } catch {
    return "";
  }
}

/**
 * Use the pure slug (e.g., my-post) as the OG image slug.
 * Matches the plain URL structure.
 */
function toOgSlug(filePath, fm) {
  // filePath: content/{type}/{slug}.md
  const filename = basename(filePath);
  const match = filename.match(/^([\w-]+)\.md$/);
  if (match) {
    return match[1];
  }
  // fallback: use filename only (without extension)
  return filename.replace(/\.md$/, "");
}

/**
 * Sanitize text for Satori rendering — strip characters that cause NO GLYPH.
 */
function sanitize(text) {
  if (!text) return "";
  return text.replace(/[^\x20-\x7E\u00A0-\u024F\u2010-\u2027\u2030-\u205E]/g, "").trim();
}

/**
 * Strip markdown formatting from raw content, returning plain text lines.
 */
function stripMarkdown(raw) {
  return raw
    // Strip frontmatter
    .replace(/^---[\s\S]*?---\s*/, "")
    // Strip images
    .replace(/!\[[^\]]*\]\([^)]+\)/g, "")
    // Strip markdown tables (lines with pipes)
    .replace(/^\|.*\|$/gm, "")
    // Strip table separator rows
    .replace(/^\s*[-|: ]+$/gm, "")
    // Strip heading anchors {#id}
    .replace(/\{#[^}]+\}/g, "")
    // Strip HTML tags
    .replace(/<[^>]+>/g, "")
    // Strip markdown links, keep text
    .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
    // Strip heading markers
    .replace(/^#{1,6}\s+/gm, "")
    // Strip bold, italic, strikethrough, code, blockquote markers
    .replace(/[*_~`>]/g, "")
    // Strip list bullets and numbered lists
    .replace(/^\s*[-*+]\s+/gm, "")
    .replace(/^\s*\d+\.\s+/gm, "")
    // Strip horizontal rules
    .replace(/^-{3,}$/gm, "");
}

/**
 * Extract the first paragraph from raw markdown content.
 * Returns only the first meaningful block of text, ignoring headings,
 * tables, lists, and other structural elements.
 */
function extractFirstParagraph(raw) {
  const stripped = stripMarkdown(raw);
  // Split into lines, find first non-empty line(s) that form a paragraph
  const lines = stripped.split("\n");
  const paragraphLines = [];
  let started = false;

  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) {
      // Empty line: if we've started collecting, the paragraph is done
      if (started) break;
      continue;
    }
    started = true;
    paragraphLines.push(trimmed);
  }

  const text = paragraphLines.join(" ").replace(/\s+/g, " ").trim();
  if (!text) return "";
  const safe = sanitize(text);
  return safe || text;
}

function truncate(text, max) {
  if (!text || text.length <= max) return text || "";
  return text.slice(0, max).trim() + "\u2026";
}

function buildCard(title, description, dateStr, postType, siteName) {
  const avatar = loadAvatar();
  const formattedDate = formatDate(dateStr);

  return {
    type: "div",
    props: {
      style: {
        display: "flex",
        flexDirection: "column",
        width: `${WIDTH}px`,
        height: `${HEIGHT}px`,
        backgroundColor: COLORS.bg,
      },
      children: [
        // Top accent bar
        {
          type: "div",
          props: {
            style: {
              width: "100%",
              height: "6px",
              backgroundColor: COLORS.bar,
              flexShrink: 0,
            },
          },
        },
        // Main content — vertically centered
        {
          type: "div",
          props: {
            style: {
              display: "flex",
              flex: 1,
              padding: "0 64px",
              alignItems: "center",
            },
            children: [
              // Left: text content
              {
                type: "div",
                props: {
                  style: {
                    display: "flex",
                    flexDirection: "column",
                    flex: 1,
                    gap: "16px",
                    overflow: "hidden",
                    paddingRight: avatar ? "48px" : "0",
                  },
                  children: [
                    // Post type badge + date inline
                    {
                      type: "div",
                      props: {
                        style: {
                          display: "flex",
                          alignItems: "center",
                          gap: "12px",
                          color: COLORS.meta,
                          fontSize: "18px",
                          fontWeight: 400,
                          fontFamily: "Inter",
                        },
                        children: [
                          {
                            type: "span",
                            props: {
                              style: {
                                backgroundColor: COLORS.badge,
                                color: COLORS.badgeText,
                                fontSize: "14px",
                                fontWeight: 700,
                                fontFamily: "Inter",
                                padding: "4px 12px",
                                borderRadius: "999px",
                                textTransform: "uppercase",
                                letterSpacing: "0.05em",
                              },
                              children: postType,
                            },
                          },
                          formattedDate
                            ? { type: "span", props: { children: formattedDate } }
                            : null,
                        ].filter(Boolean),
                      },
                    },
                    // Title
                    {
                      type: "div",
                      props: {
                        style: {
                          color: COLORS.title,
                          fontSize: "48px",
                          fontWeight: 700,
                          fontFamily: "Inter",
                          lineHeight: 1.2,
                          overflow: "hidden",
                        },
                        children: truncate(title, 120),
                      },
                    },
                    // Description (if available)
                    description
                      ? {
                          type: "div",
                          props: {
                            style: {
                              color: COLORS.description,
                              fontSize: "22px",
                              fontWeight: 400,
                              fontFamily: "Inter",
                              lineHeight: 1.4,
                              overflow: "hidden",
                            },
                            children: truncate(description, 160),
                          },
                        }
                      : null,
                  ].filter(Boolean),
                },
              },
              // Right: avatar
              avatar
                ? {
                    type: "div",
                    props: {
                      style: {
                        display: "flex",
                        flexShrink: 0,
                      },
                      children: [
                        {
                          type: "img",
                          props: {
                            src: avatar,
                            width: 128,
                            height: 128,
                            style: {
                              borderRadius: "16px",
                              border: `2px solid ${COLORS.border}`,
                            },
                          },
                        },
                      ],
                    },
                  }
                : null,
            ].filter(Boolean),
          },
        },
        // Footer: site name
        {
          type: "div",
          props: {
            style: {
              display: "flex",
              justifyContent: "flex-end",
              alignItems: "center",
              padding: "0 64px 32px 64px",
            },
            children: [
              {
                type: "div",
                props: {
                  style: {
                    color: "#8b949e",
                    fontSize: "18px",
                    fontWeight: 400,
                    fontFamily: "Inter",
                  },
                  children: siteName,
                },
              },
            ],
          },
        },
      ],
    },
  };
}

function scanContentFiles(contentDir) {
  const files = [];
  function walk(dir) {
    let entries;
    try {
      entries = readdirSync(dir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      if (entry.name === ".indiekit") continue;
      const fullPath = join(dir, entry.name);
      if (entry.isDirectory()) {
        walk(fullPath);
      } else if (entry.isFile() && entry.name.endsWith(".md")) {
        files.push(fullPath);
      }
    }
  }
  walk(contentDir);
  return files;
}

/**
 * Generate OG images for all content posts without photos.
 * @param {string} contentDir - Path to content/ directory
 * @param {string} cacheDir - Path to .cache/ directory
 * @param {string} siteName - Site name for the card
 * @param {number} batchSize - Max images to generate (0 = unlimited)
 * @returns {{ hasMore: boolean }} Whether more images need generation
 */
export async function generateOgImages(contentDir, cacheDir, siteName, batchSize = 0) {
  const ogDir = join(cacheDir, "og");
  mkdirSync(ogDir, { recursive: true });

  const manifestPath = join(ogDir, "manifest.json");
  let manifest = {};
  try {
    manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
  } catch {
    // First run
  }

  const fonts = loadFonts();
  const mdFiles = scanContentFiles(contentDir);

  let generated = 0;
  let skipped = 0;
  // Seed with existing manifest so unscanned entries survive batch writes
  const newManifest = { ...manifest };
  const SAVE_INTERVAL = 10;
  // GC every 5 images to keep WASM native memory bounded.
  const GC_INTERVAL = 5;
  const hasGC = typeof global.gc === "function";
  let peakRss = 0;

  for (const filePath of mdFiles) {
    const raw = readFileSync(filePath, "utf8");
    const { data: fm } = matter(raw);

    if (fm.photo || fm.image) {
      skipped++;
      continue;
    }

    const slug = toOgSlug(filePath, fm);
    const postType = detectPostType(filePath);
    const date = fm.published || fm.date || "";

    // Title: use frontmatter title/name, or first paragraph of body
    const fmTitle = fm.title || fm.name || "";
    const bodyText = extractFirstParagraph(raw);
    const title = fmTitle || bodyText || "Untitled";

    // Description: only show if we have a frontmatter title (so body adds context)
    const description = fmTitle ? bodyText : "";

    const hash = computeHash(title, description, date, postType, siteName);

    if (manifest[slug]?.hash === hash && existsSync(join(ogDir, `${slug}.png`))) {
      newManifest[slug] = manifest[slug];
      skipped++;
      continue;
    }

    const card = buildCard(title, description, date, postType, siteName);
    const svg = await satori(card, { width: WIDTH, height: HEIGHT, fonts });
    const resvg = new Resvg(svg, {
      fitTo: { mode: "width", value: WIDTH },
    });
    const pngBuffer = resvg.render().asPng();

    writeFileSync(join(ogDir, `${slug}.png`), pngBuffer);
    newManifest[slug] = { title: slug, hash };
    generated++;

    // Save manifest periodically to preserve progress on OOM kill
    if (generated % SAVE_INTERVAL === 0) {
      writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2));
    }

    // Force GC to reclaim Satori/Resvg WASM native memory.
    if (hasGC && generated % GC_INTERVAL === 0) {
      global.gc();
      const rss = process.memoryUsage().rss;
      if (rss > peakRss) peakRss = rss;
    }

    // Batch limit: stop after N images so the caller can re-spawn
    if (batchSize > 0 && generated >= batchSize) {
      break;
    }
  }

  const hasMore = batchSize > 0 && generated >= batchSize;

  if (hasGC) global.gc();
  writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2));
  const mem = process.memoryUsage();
  if (mem.rss > peakRss) peakRss = mem.rss;
  console.log(
    `[og] Generated ${generated} images, skipped ${skipped} (cached or have photos)` +
    (hasMore ? ` [batch, more remain]` : ``) +
    ` | RSS: ${(mem.rss / 1024 / 1024).toFixed(0)} MB, peak: ${(peakRss / 1024 / 1024).toFixed(0)} MB, heap: ${(mem.heapUsed / 1024 / 1024).toFixed(0)} MB`,
  );

  return { hasMore };
}
