/**
 * Share-card SVG renderer.
 *
 * Five templates: standard (grade), victory ("I told you"), recap (multi),
 * cheatsheet (grid), gotd (Grade of the Day).
 *
 * Three sizes:
 *   twitter — 1200x675   (X cards, OG)
 *   story   — 1080x1920  (IG Story, TikTok)
 *   square  — 1080x1080  (IG feed, Discord embed)
 *
 * The renderer composes a single SVG string (no DOM, no user-controlled
 * markup — every dynamic value passes through escapeXml first) and pipes
 * it through sharp for PNG. If sharp's native binding is unavailable in
 * a degraded environment, the route returns the raw SVG with
 * Content-Type: image/svg+xml.
 *
 * SECURITY: all caller-supplied strings are XML-escaped. No <foreignObject>,
 * no <script>, no <use href> external refs. Templates use only plain SVG
 * primitives (rect, text, path, circle, line, g, defs, gradient, filter).
 */

const { COLORS, FONTS, SPORT_LABEL_COLOR, gradeColor } = require('./tokens');

const SIZES = Object.freeze({
  twitter: { w: 1200, h: 675 },
  story:   { w: 1080, h: 1920 },
  square:  { w: 1080, h: 1080 },
});

const VALID_TYPES = new Set(['grade', 'victory', 'recap', 'cheatsheet', 'gotd']);

// ── tiny helpers ─────────────────────────────────────────────────────────

function escapeXml(input) {
  if (input == null) return '';
  return String(input)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

function clampLen(s, max) {
  if (s == null) return '';
  const str = String(s);
  return str.length > max ? str.slice(0, max - 1) + '…' : str;
}

function gradeGlowFilter(id, color) {
  return `
    <filter id="${id}" x="-50%" y="-50%" width="200%" height="200%">
      <feGaussianBlur stdDeviation="6" result="b1"/>
      <feFlood flood-color="${color}" flood-opacity="0.75"/>
      <feComposite in2="b1" operator="in" result="glow"/>
      <feMerge><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>
    </filter>`;
}

function scanLinesPattern(id) {
  // Subtle CRT scan lines, drawn at 4% opacity, repeating every 4px.
  return `
    <pattern id="${id}" width="4" height="4" patternUnits="userSpaceOnUse">
      <line x1="0" y1="0" x2="2000" y2="0" stroke="rgba(255,255,255,0.04)" stroke-width="1"/>
    </pattern>`;
}

function diagonalSlash() {
  // The signature -2deg diagonal cut, drawn faintly.
  return `
    <linearGradient id="slash" x1="0" y1="0" x2="1" y2="0.07">
      <stop offset="0" stop-color="rgba(0,212,160,0)"/>
      <stop offset="0.5" stop-color="rgba(0,212,160,0.08)"/>
      <stop offset="1" stop-color="rgba(0,212,160,0)"/>
    </linearGradient>`;
}

function commonDefs(width, height) {
  return `<defs>
    ${diagonalSlash()}
    ${scanLinesPattern('scan')}
    ${gradeGlowFilter('glow-aplus', COLORS.gradeAplus)}
    ${gradeGlowFilter('glow-a',     COLORS.gradeA)}
    ${gradeGlowFilter('glow-b',     COLORS.gradeB)}
    ${gradeGlowFilter('glow-c',     COLORS.gradeC)}
    ${gradeGlowFilter('glow-d',     COLORS.gradeD)}
    <linearGradient id="bgFade" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="${COLORS.bg1}"/>
      <stop offset="1" stop-color="${COLORS.bg0}"/>
    </linearGradient>
    <radialGradient id="glow" cx="0.5" cy="0.35" r="0.6">
      <stop offset="0" stop-color="rgba(0,212,160,0.16)"/>
      <stop offset="1" stop-color="rgba(0,212,160,0)"/>
    </radialGradient>
  </defs>`;
}

function baseBackground(width, height) {
  // Background stack: gradient + radial glow + scan lines + slash band.
  return `
    <rect width="${width}" height="${height}" fill="url(#bgFade)"/>
    <rect width="${width}" height="${height}" fill="url(#glow)"/>
    <rect width="${width}" height="${height}" fill="url(#scan)"/>
    <rect width="${width}" height="${height}" fill="url(#slash)"/>`;
}

function wordmark(x, y, fontSize) {
  // VYNDR with green R + drop-shadow on the R. No external font load —
  // SVG falls back through the font-family stack at rasterize time.
  const rOffset = fontSize * 0.62; // approx width of "VYND" at this size
  return `
    <g font-family="${FONTS.mono}" font-weight="800" letter-spacing="${fontSize * 0.10}">
      <text x="${x}" y="${y}" font-size="${fontSize}" fill="${COLORS.text0}">VYND</text>
      <text x="${x + rOffset * 3.4}" y="${y}" font-size="${fontSize}" fill="${COLORS.gradeA}"
            style="filter:drop-shadow(0 0 ${fontSize * 0.25}px rgba(0,212,160,0.7))">R</text>
    </g>`;
}

function watermarkFooter(width, height, extra = '') {
  return `
    <text x="${width / 2}" y="${height - 32}" text-anchor="middle"
          font-family="${FONTS.mono}" font-size="14" letter-spacing="5" fill="${COLORS.text2}">
      VYNDR.XYZ · @GETVYNDR${extra ? ' · ' + escapeXml(extra) : ''}
    </text>`;
}

function sportPill(x, y, sport) {
  const key = String(sport || '').toLowerCase();
  const color = SPORT_LABEL_COLOR[key] || COLORS.text1;
  const label = (key || 'sport').toUpperCase();
  // Simple pill: rounded rect + centered text.
  const w = 90;
  const h = 28;
  return `
    <g>
      <rect x="${x}" y="${y}" rx="14" ry="14" width="${w}" height="${h}"
            fill="${color}" fill-opacity="0.16" stroke="${color}" stroke-opacity="0.4"/>
      <text x="${x + w / 2}" y="${y + h / 2 + 5}" text-anchor="middle"
            font-family="${FONTS.mono}" font-size="13" font-weight="700"
            letter-spacing="2" fill="${color}">${escapeXml(label)}</text>
    </g>`;
}

function gradeBadge(x, y, grade, scale = 1) {
  const safeGrade = clampLen(grade, 4) || '—';
  const color = gradeColor(grade);
  const filterId = ({
    'A+': 'glow-aplus', 'A': 'glow-a', 'A-': 'glow-a',
    'B+': 'glow-b', 'B': 'glow-b', 'B-': 'glow-b',
    'C+': 'glow-c', 'C': 'glow-c', 'C-': 'glow-c',
    'D':  'glow-d', 'F': 'glow-d',
  })[safeGrade] || 'glow-b';
  const size = 200 * scale;
  return `
    <text x="${x}" y="${y}" text-anchor="middle"
          font-family="${FONTS.mono}" font-weight="800" font-size="${size}"
          letter-spacing="-4" fill="${color}" filter="url(#${filterId})"
          style="filter:drop-shadow(0 0 ${24 * scale}px ${color})">${escapeXml(safeGrade)}</text>`;
}

// ── templates ────────────────────────────────────────────────────────────

function renderStandardGrade({ width, height, player, sport, stat, line, direction, grade, projection, summary }) {
  const titleY = height < 700 ? 200 : 280;
  const gradeY = height < 700 ? 460 : 660;
  return `
    ${baseBackground(width, height)}
    ${wordmark(60, 80, 44)}
    ${sportPill(width - 150, 56, sport)}

    <text x="60" y="${titleY}" font-family="${FONTS.sans}" font-size="${width < 1100 ? 56 : 64}"
          font-weight="700" letter-spacing="-2" fill="${COLORS.text0}">
      ${escapeXml(clampLen(player, 28))}
    </text>
    <text x="60" y="${titleY + 60}" font-family="${FONTS.mono}" font-size="32"
          font-weight="500" fill="${COLORS.text1}">
      ${escapeXml(clampLen(stat, 14))} ${escapeXml(String(direction || 'over').toUpperCase())} ${escapeXml(String(line ?? '—'))}
    </text>

    ${gradeBadge(width / 2, gradeY, grade, height < 700 ? 0.9 : 1.1)}

    ${projection != null ? `
    <text x="60" y="${gradeY + 80}" font-family="${FONTS.mono}" font-size="22"
          font-weight="700" fill="${COLORS.text1}" letter-spacing="3">
      PROJECTION ${escapeXml(String(projection))}
    </text>` : ''}
    ${summary ? `
    <text x="60" y="${gradeY + 120}" font-family="${FONTS.sans}" font-size="20"
          fill="${COLORS.text1}">
      ${escapeXml(clampLen(summary, 80))}
    </text>` : ''}

    ${watermarkFooter(width, height)}`;
}

function renderVictoryCard(opts) {
  const { width, height, result_actual, grade, line } = opts;
  // Standard grade card body + a result banner across the top.
  const safeActual = escapeXml(clampLen(result_actual || 'HIT', 32));
  return `
    ${baseBackground(width, height)}
    <rect x="0" y="0" width="${width}" height="80" fill="${COLORS.gradeA}" fill-opacity="0.18"/>
    <text x="${width / 2}" y="52" text-anchor="middle"
          font-family="${FONTS.mono}" font-weight="800" font-size="28"
          letter-spacing="6" fill="${COLORS.gradeA}"
          style="filter:drop-shadow(0 0 8px rgba(0,212,160,0.7))">
      ✓ HIT — ${safeActual}
    </text>

    ${wordmark(60, 140, 40)}
    ${sportPill(width - 150, 110, opts.sport)}

    <text x="60" y="${height < 700 ? 260 : 340}" font-family="${FONTS.sans}" font-size="${width < 1100 ? 56 : 64}"
          font-weight="700" letter-spacing="-2" fill="${COLORS.text0}">
      ${escapeXml(clampLen(opts.player, 28))}
    </text>
    <text x="60" y="${(height < 700 ? 260 : 340) + 60}" font-family="${FONTS.mono}" font-size="32"
          font-weight="500" fill="${COLORS.text1}">
      ${escapeXml(clampLen(opts.stat, 14))} ${escapeXml(String(opts.direction || 'over').toUpperCase())} ${escapeXml(String(line ?? '—'))}
    </text>

    ${gradeBadge(width / 2, height - (height < 700 ? 180 : 260), grade, height < 700 ? 0.85 : 1.0)}

    <text x="${width / 2}" y="${height - 70}" text-anchor="middle"
          font-family="${FONTS.sans}" font-size="22" font-weight="600" fill="${COLORS.text1}">
      VYNDR called it.
    </text>
    ${watermarkFooter(width, height)}`;
}

function renderRecap({ width, height, date, entries = [], accuracy }) {
  const safeEntries = entries.slice(0, 6);
  const rowH = 64;
  const startY = 240;
  const rows = safeEntries.map((e, i) => {
    const y = startY + i * rowH;
    const ok = e.result === 'hit';
    const tint = ok ? 'rgba(0,212,160,0.10)' : 'rgba(255,82,82,0.10)';
    const mark = ok ? '✓' : '✗';
    const markColor = ok ? COLORS.gradeA : COLORS.gradeD;
    return `
      <rect x="40" y="${y - 36}" width="${width - 80}" height="${rowH - 8}" rx="10" fill="${tint}"/>
      <text x="64" y="${y}" font-family="${FONTS.mono}" font-size="28" font-weight="800" fill="${markColor}">${mark}</text>
      <text x="100" y="${y}" font-family="${FONTS.sans}" font-size="24" font-weight="700" fill="${COLORS.text0}">
        ${escapeXml(clampLen(e.player, 22))}
      </text>
      <text x="${width / 2}" y="${y}" font-family="${FONTS.mono}" font-size="22" fill="${COLORS.text1}">
        ${escapeXml(clampLen(`${e.stat || ''} ${String(e.direction || '').toUpperCase()} ${e.line ?? ''}`, 28))}
      </text>
      <text x="${width - 220}" y="${y}" font-family="${FONTS.mono}" font-size="22" font-weight="800"
            fill="${gradeColor(e.grade)}">${escapeXml(clampLen(e.grade, 4))}</text>
      <text x="${width - 64}" y="${y}" text-anchor="end" font-family="${FONTS.mono}" font-size="22"
            font-weight="700" fill="${ok ? COLORS.gradeA : COLORS.gradeD}">
        ${ok ? 'HIT' : 'MISS'}
      </text>`;
  }).join('');
  const hits = safeEntries.filter((e) => e.result === 'hit').length;
  const acc = accuracy != null ? accuracy : (safeEntries.length ? Math.round((hits / safeEntries.length) * 100) : 0);
  return `
    ${baseBackground(width, height)}
    ${wordmark(60, 80, 36)}
    <text x="60" y="140" font-family="${FONTS.mono}" font-size="22" font-weight="700"
          letter-spacing="4" fill="${COLORS.text2}">LAST NIGHT'S RESULTS</text>
    <text x="60" y="180" font-family="${FONTS.sans}" font-size="28" fill="${COLORS.text0}">
      ${escapeXml(clampLen(date || '', 32))}
    </text>
    ${rows}
    <text x="${width / 2}" y="${height - 90}" text-anchor="middle"
          font-family="${FONTS.mono}" font-size="36" font-weight="800"
          fill="${COLORS.gradeA}" letter-spacing="4"
          style="filter:drop-shadow(0 0 10px rgba(0,212,160,0.6))">
      ACCURACY ${acc}% (${hits}/${safeEntries.length})
    </text>
    ${watermarkFooter(width, height)}`;
}

function renderCheatsheet({ width, height, date, gameCount, grades = [] }) {
  const safe = grades.slice(0, 8);
  const rowH = 60;
  const startY = 240;
  const rows = safe.map((g, i) => {
    const y = startY + i * rowH;
    return `
      <text x="60" y="${y}" font-family="${FONTS.mono}" font-size="32" font-weight="800"
            fill="${gradeColor(g.grade)}"
            style="filter:drop-shadow(0 0 6px ${gradeColor(g.grade)})">${escapeXml(clampLen(g.grade, 4))}</text>
      <text x="160" y="${y}" font-family="${FONTS.sans}" font-size="24" font-weight="600" fill="${COLORS.text0}">
        ${escapeXml(clampLen(g.player, 22))}
      </text>
      <text x="${width - 80}" y="${y}" text-anchor="end" font-family="${FONTS.mono}" font-size="22"
            fill="${COLORS.text1}">
        ${escapeXml(clampLen(`${g.stat || ''} ${String(g.direction || '').toUpperCase()} ${g.line ?? ''}`, 28))}
      </text>`;
  }).join('');
  return `
    ${baseBackground(width, height)}
    ${wordmark(60, 80, 36)}
    <text x="60" y="140" font-family="${FONTS.mono}" font-size="22" font-weight="700"
          letter-spacing="4" fill="${COLORS.gradeA}">TONIGHT'S CHEATSHEET</text>
    <text x="60" y="180" font-family="${FONTS.sans}" font-size="22" fill="${COLORS.text1}">
      ${escapeXml(clampLen(date || '', 24))} · ${escapeXml(String(gameCount ?? 0))} games
    </text>
    ${rows}
    ${watermarkFooter(width, height)}`;
}

function renderGOTD(opts) {
  // Same shape as standard grade but with the "GRADE OF THE DAY" slug.
  const { width, height } = opts;
  return `
    ${baseBackground(width, height)}
    <rect x="0" y="0" width="${width}" height="64" fill="${COLORS.gradeA}" fill-opacity="0.14"/>
    <text x="${width / 2}" y="42" text-anchor="middle"
          font-family="${FONTS.mono}" font-weight="800" font-size="22"
          letter-spacing="8" fill="${COLORS.gradeA}">◆ GRADE OF THE DAY</text>
    ${wordmark(60, 130, 40)}
    ${sportPill(width - 150, 100, opts.sport)}

    <text x="60" y="${height < 700 ? 250 : 320}" font-family="${FONTS.sans}" font-size="${width < 1100 ? 56 : 64}"
          font-weight="700" letter-spacing="-2" fill="${COLORS.text0}">
      ${escapeXml(clampLen(opts.player, 28))}
    </text>
    <text x="60" y="${(height < 700 ? 250 : 320) + 56}" font-family="${FONTS.mono}" font-size="30"
          fill="${COLORS.text1}">
      ${escapeXml(clampLen(opts.stat, 14))} ${escapeXml(String(opts.direction || 'over').toUpperCase())} ${escapeXml(String(opts.line ?? '—'))}
    </text>

    ${gradeBadge(width / 2, height - (height < 700 ? 180 : 260), opts.grade, height < 700 ? 0.95 : 1.15)}

    ${opts.summary ? `
    <text x="${width / 2}" y="${height - 110}" text-anchor="middle"
          font-family="${FONTS.sans}" font-size="22" fill="${COLORS.text0}">
      ${escapeXml(clampLen(opts.summary, 96))}
    </text>` : ''}
    ${watermarkFooter(width, height)}`;
}

// ── public API ───────────────────────────────────────────────────────────

function buildSvg(type, format, payload) {
  if (!VALID_TYPES.has(type)) throw new Error(`unknown card type: ${type}`);
  const sz = SIZES[format] || SIZES.twitter;
  const body = (() => {
    switch (type) {
      case 'grade':      return renderStandardGrade({ ...payload, width: sz.w, height: sz.h });
      case 'victory':    return renderVictoryCard({ ...payload, width: sz.w, height: sz.h });
      case 'recap':      return renderRecap({ ...payload, width: sz.w, height: sz.h });
      case 'cheatsheet': return renderCheatsheet({ ...payload, width: sz.w, height: sz.h });
      case 'gotd':       return renderGOTD({ ...payload, width: sz.w, height: sz.h });
      default:           throw new Error(`unhandled card type: ${type}`);
    }
  })();
  return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sz.w} ${sz.h}" width="${sz.w}" height="${sz.h}">
  ${commonDefs(sz.w, sz.h)}
  ${body}
</svg>`;
}

async function rasterize(svg) {
  // Lazy require so a missing sharp binding doesn't sink the whole module.
  let sharp;
  try { sharp = require('sharp'); }
  catch (err) {
    const e = new Error('sharp unavailable');
    e.code = 'SHARP_UNAVAILABLE';
    throw e;
  }
  return sharp(Buffer.from(svg)).png({ compressionLevel: 9 }).toBuffer();
}

module.exports = {
  SIZES,
  VALID_TYPES,
  buildSvg,
  rasterize,
  // exported for tests
  escapeXml,
  clampLen,
};
