#!/usr/bin/env node
// build-figur.mjs — render en kodet figur (SVG/HTML) til PNG via headless Chrome.
// Bruk: node build-figur.mjs <figur.svg|figur.html> [--target article|carousel|single]
//                            [--out <fil.png>] [--width N] [--height N]
// Tre render-mål: article (~1200px bred, innholdsstyrt høyde) · carousel (1080×1350)
// · single (1200×1200). Design-tokens leses fra brukerens data-dir
// (${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/brand-tokens.json)
// og injiseres som CSS-variabler (--figur-*); uten fil brukes nøytrale defaults —
// brukerens merkevare er DATA, aldri hardkodet her. Designregler håndheves som
// warn-validering (aldri hard fail). Ingen nettverkstilgang under render (lokal
// fil-input; Chrome kjøres med nettverksdempende flagg). CLI + importerbar modul
// (samme dobbeltrolle som hooks/scripts/ical-generator.mjs). Ingen npm-avhengigheter.

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync, spawn } from "node:child_process";

// ---------------------------------------------------------------------------
// Render-mål. height:null = innholdsstyrt (avledes av kildens aspekt, ellers
// fallback). Overstyrbare via --width/--height.
// ---------------------------------------------------------------------------
export const TARGETS = {
  article: { width: 1200, height: null },
  carousel: { width: 1080, height: 1350 },
  single: { width: 1200, height: 1200 },
};

const ARTICLE_FALLBACK_ASPECT = 16 / 9;

export function deriveDimensions(target, aspect, overrides = {}) {
  const t = TARGETS[target];
  if (!t) {
    throw new Error(`Ukjent mål «${target}». Gyldige mål: ${Object.keys(TARGETS).join(", ")}`);
  }
  const warnings = [];
  const width = overrides.width ?? t.width;
  let height = overrides.height ?? t.height;
  if (height == null) {
    if (aspect) {
      height = Math.round(width / aspect);
    } else {
      height = Math.round(width / ARTICLE_FALLBACK_ASPECT);
      warnings.push(
        `Kilden oppgir ingen dimensjoner — bruker fallback-høyde ${height}px (overstyr med --height).`
      );
    }
  }
  return { width, height, warnings };
}

// ---------------------------------------------------------------------------
// Design-tokens: BRUKERDATA fra data-dir, nøytrale defaults ellers.
// ---------------------------------------------------------------------------
export const DEFAULT_TOKENS = {
  background: "#FFFFFF",
  ink: "#1A1A1A",
  muted: "#5B5B5B",
  accent: "#2F6F9F",
  rule: "#D9D9D9",
  fontFamily: "system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif",
};

function defaultDataDir() {
  return process.env.LINKEDIN_STUDIO_DATA || path.join(os.homedir(), ".claude", "linkedin-studio");
}

export function loadBrandTokens(dataDir = defaultDataDir()) {
  const file = path.join(dataDir, "profile", "brand-tokens.json");
  const warnings = [];
  if (!fs.existsSync(file)) {
    return { tokens: { ...DEFAULT_TOKENS }, source: "default", warnings };
  }
  try {
    const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
      throw new Error("forventet et JSON-objekt");
    }
    return { tokens: { ...DEFAULT_TOKENS, ...parsed }, source: "user", warnings };
  } catch (err) {
    warnings.push(`Kunne ikke lese brand-tokens.json (${err.message}) — bruker nøytrale defaults.`);
    return { tokens: { ...DEFAULT_TOKENS }, source: "default", warnings };
  }
}

export function tokensToCss(tokens) {
  const vars = Object.entries(tokens)
    .map(([key, value]) => `--figur-${key}:${value};`)
    .join("");
  return `:root{${vars}}`;
}

// ---------------------------------------------------------------------------
// Kilde → render-klar HTML. SVG pakkes i en side med eksakt mål-flate;
// HTML får token-variablene injisert (etter <head>, ellers prependet).
// ---------------------------------------------------------------------------
export function wrapSvg(svgContent, { width, height }, tokens) {
  return `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><style>
${tokensToCss(tokens)}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{width: ${width}px;height: ${height}px;background:var(--figur-background);overflow:hidden;}
svg{display:block;width:100%;height:100%;}
</style></head>
<body>
${svgContent}
</body>
</html>
`;
}

export function injectTokens(html, tokens) {
  const style = `<style>${tokensToCss(tokens)}</style>`;
  if (/<head[^>]*>/i.test(html)) {
    return html.replace(/<head[^>]*>/i, (m) => `${m}${style}`);
  }
  return `${style}\n${html}`;
}

// Aspekt (bredde/høyde) fra SVG-kilden: viewBox først, ellers width/height-attributter.
export function parseSvgMeta(svgContent) {
  const vb = svgContent.match(/viewBox\s*=\s*["']\s*[\d.+-]+[\s,]+[\d.+-]+[\s,]+([\d.]+)[\s,]+([\d.]+)\s*["']/);
  if (vb) {
    const w = parseFloat(vb[1]);
    const h = parseFloat(vb[2]);
    if (w > 0 && h > 0) return w / h;
  }
  const wm = svgContent.match(/<svg[^>]*\bwidth\s*=\s*["']([\d.]+)(?:px)?["']/);
  const hm = svgContent.match(/<svg[^>]*\bheight\s*=\s*["']([\d.]+)(?:px)?["']/);
  if (wm && hm) {
    const w = parseFloat(wm[1]);
    const h = parseFloat(hm[1]);
    if (w > 0 && h > 0) return w / h;
  }
  return null;
}

// ---------------------------------------------------------------------------
// Warn-validering av designregler (aldri hard fail):
//  - minst 24pt/px effektiv tekststørrelse
//  - maks 1 primær + 1 sekundærfarge utover nøytraler (gråtoner/hvit/sort)
// (≥40 % whitespace er redaksjonell regel — ikke maskinelt målbar her.)
// ---------------------------------------------------------------------------
const MIN_FONT_SIZE = 24;
const MAX_NON_NEUTRAL_COLORS = 2;

function isNeutralHex(hex6) {
  const r = parseInt(hex6.slice(0, 2), 16);
  const g = parseInt(hex6.slice(2, 4), 16);
  const b = parseInt(hex6.slice(4, 6), 16);
  const spread = Math.max(r, g, b) - Math.min(r, g, b);
  return spread <= 18;
}

export function validateFigure(source) {
  const warnings = [];

  const smallSizes = [];
  const sizeRe = /font-size\s*[:=]\s*["']?\s*([\d.]+)\s*(px|pt)?/gi;
  let m;
  while ((m = sizeRe.exec(source)) !== null) {
    const size = parseFloat(m[1]);
    if (size < MIN_FONT_SIZE) smallSizes.push(size);
  }
  if (smallSizes.length) {
    warnings.push(
      `Tekststørrelse under ${MIN_FONT_SIZE}pt funnet (${smallSizes.join(", ")}) — hev for lesbarhet på mobil.`
    );
  }

  const nonNeutral = new Set();
  const colorRe = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
  while ((m = colorRe.exec(source)) !== null) {
    let hex = m[1].toLowerCase();
    if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
    if (!isNeutralHex(hex)) nonNeutral.add(hex);
  }
  if (nonNeutral.size > MAX_NON_NEUTRAL_COLORS) {
    warnings.push(
      `Fargebudsjett: ${nonNeutral.size} ikke-nøytrale farger (maks 1 primær + 1 sekundær utover nøytraler).`
    );
  }

  return warnings;
}

// ---------------------------------------------------------------------------
// Robust Chrome-oppdagelse: app-bundle-stier → PATH → hint. Prober er
// injiserbare for test.
// ---------------------------------------------------------------------------
const CHROME_APP_CANDIDATES = [
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
  path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
];
const CHROME_PATH_NAMES = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome"];

const CHROME_HINT =
  "Fant ingen Chrome/Chromium-binær — headless-render krever en.\n" +
  `  Lette etter: ${CHROME_APP_CANDIDATES.join(" · ")} + PATH (${CHROME_PATH_NAMES.join(", ")})\n` +
  "  Installer Google Chrome (https://www.google.com/chrome/) eller `brew install chromium`.";

function defaultWhich(cmd) {
  try {
    const out = execFileSync("/usr/bin/which", [cmd], { encoding: "utf8" }).trim();
    return out || null;
  } catch {
    return null;
  }
}

export function resolveChrome(probes = {}) {
  const exists = probes.exists || fs.existsSync;
  const which = probes.which || defaultWhich;
  for (const candidate of CHROME_APP_CANDIDATES) {
    if (exists(candidate)) return { available: true, path: candidate };
  }
  for (const name of CHROME_PATH_NAMES) {
    const found = which(name);
    if (found) return { available: true, path: found };
  }
  return { available: false, hint: CHROME_HINT };
}

// ---------------------------------------------------------------------------
// Render: kilde → wrapper-HTML i tmp → headless Chrome-screenshot → PNG.
// Kjent macOS-quirk (verifisert lokalt mot Chrome 150): headless skriver PNG-en
// og melder «N bytes written to file …» på stderr, men prosessen avslutter
// aldri. Watchdog: vi lytter på stderr-kvitteringen (evt. ren exit på andre
// plattformer), dreper prosessen og verifiserer at PNG-filen faktisk finnes.
// ---------------------------------------------------------------------------
const RENDER_TIMEOUT_MS = 30000;

function runChromeScreenshot(chromePath, args, timeoutMs = RENDER_TIMEOUT_MS) {
  return new Promise((resolve) => {
    const child = spawn(chromePath, args, { stdio: ["ignore", "ignore", "pipe"] });
    let stderr = "";
    let settled = false;
    const finish = (reason) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      child.kill("SIGKILL");
      resolve({ reason, stderr });
    };
    const timer = setTimeout(() => finish("timeout"), timeoutMs);
    child.stderr.on("data", (chunk) => {
      stderr += chunk;
      if (stderr.includes("written to file")) finish("written");
    });
    child.on("exit", () => finish("exit"));
    child.on("error", () => finish("spawn-error"));
  });
}

export async function renderFigure(opts) {
  const { input, target = "article", out = null, dataDir, chrome = null } = opts;
  const inPath = path.isAbsolute(input) ? input : path.join(process.cwd(), input);
  if (!fs.existsSync(inPath)) throw new Error(`Fant ikke input-fil: ${inPath}`);

  const ext = path.extname(inPath).toLowerCase();
  if (![".svg", ".html", ".htm"].includes(ext)) {
    throw new Error(`Ustøttet input-type «${ext}» — bruk .svg eller .html.`);
  }
  const source = fs.readFileSync(inPath, "utf8");

  const { tokens, source: tokensSource, warnings } = loadBrandTokens(dataDir);
  warnings.push(...validateFigure(source));

  const aspect = ext === ".svg" ? parseSvgMeta(source) : null;
  const dims = deriveDimensions(target, aspect, { width: opts.width ?? null, height: opts.height ?? null });
  warnings.push(...dims.warnings);

  const pageHtml = ext === ".svg" ? wrapSvg(source, dims, tokens) : injectTokens(source, tokens);

  const resolved = chrome || resolveChrome();
  if (!resolved.available) throw new Error(resolved.hint);

  const outPath = out
    ? (path.isAbsolute(out) ? out : path.join(process.cwd(), out))
    : path.join(path.dirname(inPath), `${path.basename(inPath, ext)}-${target}.png`);

  const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "build-figur-"));
  try {
    const htmlPath = path.join(workDir, "figur.html");
    fs.writeFileSync(htmlPath, pageHtml, "utf8");
    const run = await runChromeScreenshot(resolved.path, [
      "--headless",
      "--disable-gpu",
      "--hide-scrollbars",
      "--force-device-scale-factor=1",
      "--no-first-run",
      "--no-default-browser-check",
      "--disable-extensions",
      "--disable-sync",
      "--disable-background-networking",
      "--disable-component-update",
      "--use-mock-keychain",
      "--password-store=basic",
      `--user-data-dir=${path.join(workDir, "profile")}`,
      `--window-size=${dims.width},${dims.height}`,
      `--screenshot=${outPath}`,
      `file://${htmlPath}`,
    ]);
    if (!fs.existsSync(outPath) || fs.statSync(outPath).size === 0) {
      const tail = run.stderr.split("\n").filter(Boolean).slice(-5).join("\n");
      throw new Error(`Chrome skrev ikke ${outPath} (${run.reason}).\n${tail}`);
    }
  } finally {
    fs.rmSync(workDir, { recursive: true, force: true });
  }

  return { out: outPath, width: dims.width, height: dims.height, warnings, tokensSource };
}

// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
const USAGE =
  "Bruk: node build-figur.mjs <figur.svg|figur.html> [--target article|carousel|single] [--out <fil.png>] [--width N] [--height N]";

export function parseArgs(argv) {
  const cfg = { input: null, target: "article", out: null, width: null, height: null };
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i];
    if (arg === "--target") cfg.target = argv[++i];
    else if (arg === "--out") cfg.out = argv[++i];
    else if (arg === "--width") cfg.width = parseInt(argv[++i], 10);
    else if (arg === "--height") cfg.height = parseInt(argv[++i], 10);
    else if (arg.startsWith("--")) return { error: `Ukjent flagg «${arg}». ${USAGE}` };
    else if (!cfg.input) cfg.input = arg;
    else return { error: `Uventet argument «${arg}». ${USAGE}` };
  }
  if (!cfg.input) return { error: USAGE };
  if (!TARGETS[cfg.target]) {
    return { error: `Ukjent mål «${cfg.target}». Gyldige mål: ${Object.keys(TARGETS).join(", ")}` };
  }
  if ((cfg.width !== null && !Number.isFinite(cfg.width)) || (cfg.height !== null && !Number.isFinite(cfg.height))) {
    return { error: `--width/--height må være heltall. ${USAGE}` };
  }
  return cfg;
}

async function main() {
  const cfg = parseArgs(process.argv.slice(2));
  if (cfg.error) {
    console.error(cfg.error);
    process.exit(1);
  }
  try {
    const result = await renderFigure(cfg);
    for (const w of result.warnings) console.warn(`⚠ ${w}`);
    console.log(
      `Figur: ${result.out} (${result.width}×${result.height}, mål: ${cfg.target}, tokens: ${result.tokensSource})`
    );
  } catch (err) {
    console.error(err.message);
    process.exit(1);
  }
}

if (import.meta.url === `file://${process.argv[1]}`) {
  main();
}
