/**
 * POST /api/share-card — returns a PNG (or SVG fallback) for social sharing.
 *
 * Inputs are validated against allowlists. Any caller-supplied text is
 * XML-escaped in the renderer. Per-IP rate limit (in-memory) prevents
 * abuse. Hashed inputs back a tiny disk cache in /tmp/share-cards so
 * repeated requests serve from disk.
 *
 * SECURITY:
 *   - Sport / grade / type / direction / format ∈ allowlist
 *   - Player & stat & summary length-clamped
 *   - No HTML, no SVG injection (renderer escapes everything)
 *   - Rate limit: 30 cards / minute / IP
 *   - Sharp is invoked through a memory-capped sharp() chain
 */

const express = require('express');
const path = require('node:path');
const fs = require('node:fs/promises');
const crypto = require('node:crypto');

const renderer = require('../services/shareCards/renderer');

const router = express.Router();

const VALID_FORMATS = new Set(['twitter', 'story', 'square']);
const VALID_SPORTS = new Set(['nba', 'wnba', 'mlb', 'nfl', 'nhl', 'tennis', 'mma', 'boxing', 'golf']);
const VALID_GRADES = new Set([
  'A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D', 'F',
]);
const VALID_DIRECTIONS = new Set(['over', 'under']);
const VALID_RESULTS = new Set(['hit', 'miss', 'push', 'pending']);
const MAX_PLAYER_LEN = 64;
const MAX_STAT_LEN = 32;
const MAX_SUMMARY_LEN = 160;
const MAX_RECAP_ENTRIES = 8;
const MAX_CHEATSHEET_ENTRIES = 8;

const CACHE_DIR = path.join('/tmp', 'vyndr-share-cards');
fs.mkdir(CACHE_DIR, { recursive: true }).catch(() => {});

// ── tiny in-memory rate limiter (per IP, 30/min sliding window) ───────────
const RATE_WINDOW_MS = 60_000;
const RATE_MAX = 30;
const ipBuckets = new Map();

function checkRate(ip) {
  const now = Date.now();
  const arr = ipBuckets.get(ip) || [];
  const fresh = arr.filter((t) => now - t < RATE_WINDOW_MS);
  if (fresh.length >= RATE_MAX) return false;
  fresh.push(now);
  ipBuckets.set(ip, fresh);
  return true;
}

// Periodic prune so the map doesn't grow unbounded.
setInterval(() => {
  const now = Date.now();
  for (const [ip, arr] of ipBuckets.entries()) {
    const fresh = arr.filter((t) => now - t < RATE_WINDOW_MS);
    if (fresh.length === 0) ipBuckets.delete(ip);
    else ipBuckets.set(ip, fresh);
  }
}, RATE_WINDOW_MS).unref?.();

// ── input shaping & validation ────────────────────────────────────────────

function pickStr(v, max) {
  if (typeof v !== 'string') return null;
  const trimmed = v.trim();
  if (!trimmed) return null;
  return trimmed.slice(0, max);
}

function pickNum(v) {
  const n = typeof v === 'number' ? v : Number(v);
  return Number.isFinite(n) ? n : null;
}

function validateBase(body) {
  const errors = [];
  const type = pickStr(body.type, 16);
  if (!type || !renderer.VALID_TYPES.has(type)) errors.push('type must be one of: grade, victory, recap, cheatsheet, gotd');
  const format = pickStr(body.format, 16) || 'twitter';
  if (!VALID_FORMATS.has(format)) errors.push(`format must be one of: ${[...VALID_FORMATS].join(', ')}`);
  return { type, format, errors };
}

function shapeSinglePropPayload(b) {
  return {
    player: pickStr(b.player, MAX_PLAYER_LEN),
    sport: (b.sport && VALID_SPORTS.has(String(b.sport).toLowerCase())) ? String(b.sport).toLowerCase() : null,
    stat: pickStr(b.stat, MAX_STAT_LEN),
    line: pickNum(b.line),
    direction: VALID_DIRECTIONS.has(String(b.direction || '').toLowerCase()) ? String(b.direction).toLowerCase() : 'over',
    grade: VALID_GRADES.has(String(b.grade || '').toUpperCase()) ? String(b.grade).toUpperCase() : null,
    projection: pickNum(b.projection),
    summary: pickStr(b.summary, MAX_SUMMARY_LEN),
  };
}

function shapeRecapPayload(b) {
  const entries = Array.isArray(b.entries) ? b.entries.slice(0, MAX_RECAP_ENTRIES) : [];
  return {
    date: pickStr(b.date, 32),
    accuracy: pickNum(b.accuracy),
    entries: entries.map((e) => ({
      player: pickStr(e.player, MAX_PLAYER_LEN),
      stat: pickStr(e.stat, MAX_STAT_LEN),
      direction: VALID_DIRECTIONS.has(String(e.direction || '').toLowerCase()) ? String(e.direction).toLowerCase() : 'over',
      line: pickNum(e.line),
      grade: VALID_GRADES.has(String(e.grade || '').toUpperCase()) ? String(e.grade).toUpperCase() : null,
      result: VALID_RESULTS.has(String(e.result || '').toLowerCase()) ? String(e.result).toLowerCase() : 'pending',
    })).filter((e) => e.player && e.grade),
  };
}

function shapeCheatsheetPayload(b) {
  const grades = Array.isArray(b.grades) ? b.grades.slice(0, MAX_CHEATSHEET_ENTRIES) : [];
  return {
    date: pickStr(b.date, 32),
    gameCount: pickNum(b.gameCount),
    grades: grades.map((g) => ({
      player: pickStr(g.player, MAX_PLAYER_LEN),
      stat: pickStr(g.stat, MAX_STAT_LEN),
      direction: VALID_DIRECTIONS.has(String(g.direction || '').toLowerCase()) ? String(g.direction).toLowerCase() : 'over',
      line: pickNum(g.line),
      grade: VALID_GRADES.has(String(g.grade || '').toUpperCase()) ? String(g.grade).toUpperCase() : null,
    })).filter((g) => g.player && g.grade),
  };
}

function shapeVictoryPayload(b) {
  return {
    ...shapeSinglePropPayload(b),
    result_actual: pickStr(b.result_actual || b.actual || '', 64) || 'HIT',
  };
}

function shapePayload(type, body) {
  switch (type) {
    case 'grade':      return shapeSinglePropPayload(body);
    case 'gotd':       return shapeSinglePropPayload(body);
    case 'victory':    return shapeVictoryPayload(body);
    case 'recap':      return shapeRecapPayload(body);
    case 'cheatsheet': return shapeCheatsheetPayload(body);
    default:           return {};
  }
}

function hashKey(type, format, payload) {
  const json = JSON.stringify({ type, format, payload });
  return crypto.createHash('sha256').update(json).digest('hex').slice(0, 24);
}

// ── route ─────────────────────────────────────────────────────────────────

router.post('/', async (req, res) => {
  const ip = (req.headers['x-forwarded-for'] || req.ip || 'unknown').toString().split(',')[0].trim();
  if (!checkRate(ip)) {
    return res.status(429).json({ error: 'rate limit exceeded — 30 cards/min' });
  }

  const { type, format, errors } = validateBase(req.body || {});
  if (errors.length) return res.status(400).json({ error: 'invalid input', detail: errors });

  const payload = shapePayload(type, req.body || {});
  const key = hashKey(type, format, payload);
  const cachePath = path.join(CACHE_DIR, `${key}.png`);

  // Cache check
  try {
    const cached = await fs.readFile(cachePath);
    res.set('Content-Type', 'image/png');
    res.set('X-Cache', 'HIT');
    res.set('Cache-Control', 'public, max-age=900');
    return res.send(cached);
  } catch { /* miss */ }

  let svg;
  try {
    svg = renderer.buildSvg(type, format, payload);
  } catch (err) {
    // SEC-2 (Session 7d): don't echo err.message to public callers — the
    // SVG renderer may surface file paths or upstream library detail.
    // Log to stderr for ops, return a generic 400 to the caller.
    console.error('[VYNDR] shareCard render failed:', err?.message);
    return res.status(400).json({ error: 'render failed' });
  }

  // Optional SVG-only mode (no rasterization)
  if (req.query.svg === '1') {
    res.set('Content-Type', 'image/svg+xml');
    res.set('X-Cache', 'MISS');
    return res.send(svg);
  }

  let png;
  try {
    png = await renderer.rasterize(svg);
  } catch (err) {
    if (err && err.code === 'SHARP_UNAVAILABLE') {
      // Degrade: hand back SVG so the channel-side renderer can still embed.
      res.set('Content-Type', 'image/svg+xml');
      res.set('X-Cache', 'MISS');
      res.set('X-Degraded', 'svg-fallback');
      return res.send(svg);
    }
    console.error('[VYNDR] shareCard rasterize failed:', err?.message);
    return res.status(500).json({ error: 'rasterize failed' });
  }

  // Write cache (best-effort; ignore failures so the response still flies)
  fs.writeFile(cachePath, png).catch(() => {});

  res.set('Content-Type', 'image/png');
  res.set('X-Cache', 'MISS');
  res.set('Cache-Control', 'public, max-age=900');
  return res.send(png);
});

module.exports = router;
