// Hero Studio server: state, SSE live channel, console relay, toast hook, generate API, static UI.
const http = require('http');
const fs = require('fs');
const path = require('path');
const { spawn, execFile } = require('child_process');

const PORT = 8846;
const ROOT = path.join(__dirname, 'public');
const PAGES = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'pages.json'), 'utf8'));
const COMPONENTS = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'components.json'), 'utf8'));

let state = {
  mode: 'app', // app | component
  page: 'adom/adom-tts',
  comp: { id: 'sm02b-srss-tb', imgSrc: 'photo', layout: 'photo-right', mpn: 'under-title' },
  layout: 'bleed-bottom',   // bleed-bottom | bleed-right | bleed-top | bleed-corner-br | bleed-corner-tr | contained | mirror
  theme: 'midnight',        // midnight | pcb | onion | pill | gradient
  show: { logo: true, title: true, subtitle: true, description: false },
  titleMode: 'overlay-only', // overlay-only | image-only | both
  overlay: { pill: true, scores: true, author: true, slug: true },
  gradAngle: 155,
  guides: true,
};

const consoleBuf = [];
const clients = new Set();
// seq counters seed from the clock so a server restart never replays or masks events
let toastSeq = Date.now();
let lastToast = null;
let genSeq = Date.now();
let lastGen = null;
let generating = false;

function broadcast(event, data) {
  const msg = 'event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n';
  for (const res of clients) res.write(msg);
}

function logLine(level, text) {
  consoleBuf.push({ t: new Date().toISOString(), level, text });
  if (consoleBuf.length > 500) consoleBuf.shift();
}

function merge(dst, src) {
  for (const k of Object.keys(src)) {
    if (src[k] && typeof src[k] === 'object' && !Array.isArray(src[k]) && dst[k] && typeof dst[k] === 'object') {
      merge(dst[k], src[k]);
    } else {
      dst[k] = src[k];
    }
  }
}

function readBody(req, cb) {
  let b = '';
  req.on('data', (c) => { b += c; });
  req.on('end', () => {
    try { cb(null, b ? JSON.parse(b) : {}); } catch (e) { cb(e); }
  });
}

const MIME = {
  '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
  '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png',
  '.woff2': 'font/woff2',
};

const server = http.createServer((req, res) => {
  const url = new URL(req.url, 'http://x');
  const p = url.pathname;

  if (p === '/state' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify(Object.assign({}, state, { _toast: lastToast, _gen: lastGen })));
  }
  if (p === '/state' && req.method === 'POST') {
    return readBody(req, (err, body) => {
      if (err) { res.writeHead(200); return res.end(JSON.stringify({ ok: false, error: 'bad json' })); }
      merge(state, body);
      logLine('info', 'state <- ' + JSON.stringify(body));
      broadcast('state', state);
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: true, state }));
    });
  }
  if (p === '/events') {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive',
      'X-Accel-Buffering': 'no',
    });
    res.write(':' + ' '.repeat(2048) + '\n\n'); // flush proxy buffers
    res.write('event: state\ndata: ' + JSON.stringify(state) + '\n\n');
    clients.add(res);
    const hb = setInterval(() => res.write(': hb\n\n'), 15000);
    req.on('close', () => { clearInterval(hb); clients.delete(res); });
    return;
  }
  if (p === '/ui/toast' && req.method === 'POST') {
    return readBody(req, (err, body) => {
      toastSeq += 1;
      const t = { text: (body && body.text) || '...', kind: (body && body.kind) || 'info', seq: toastSeq };
      lastToast = t;
      logLine('toast', t.text);
      broadcast('toast', t);
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: true }));
    });
  }
  if (p === '/console' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify(consoleBuf));
  }
  if (p === '/console' && req.method === 'POST') {
    return readBody(req, (err, body) => {
      if (!err && body) logLine(body.level || 'log', String(body.text || ''));
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: true }));
    });
  }
  if (p === '/api/pages') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify(PAGES));
  }
  if (p === '/api/components') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify(COMPONENTS));
  }

  if (p === '/generate' && req.method === 'POST') {
    if (generating) {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      return res.end(JSON.stringify({ ok: false, error: 'a generate is already running', hint: 'wait for it to finish, then retry' }));
    }
    generating = true;
    const slug = state.mode === 'component'
      ? ((COMPONENTS.find(c => c.id === state.comp.id) || COMPONENTS[0]).slug)
      : (state.page.split('/')[1] || 'hero');
    const ts = Date.now();
    const file = 'hero-' + slug + '-' + ts + '.png';
    const outDir = path.join(__dirname, 'generated');
    fs.mkdirSync(outDir, { recursive: true });
    const outFile = path.join(outDir, file);
    logLine('info', 'generate -> ' + file);
    const child = spawn(process.execPath, [path.join(__dirname, 'render-hero.mjs'), 'http://localhost:' + PORT + '/?render=1', outFile], {
      env: process.env, stdio: ['ignore', 'pipe', 'pipe'],
    });
    let errBuf = '';
    child.stderr.on('data', (c) => { errBuf += c; });
    const finish = (payload) => {
      generating = false;
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(payload));
    };
    const timer = setTimeout(() => { child.kill(); }, 60000);
    child.on('close', (code) => {
      clearTimeout(timer);
      if (code !== 0 || !fs.existsSync(outFile)) {
        logLine('error', 'generate failed: ' + errBuf.slice(-400));
        return finish({ ok: false, error: errBuf.trim().split('\n').pop() || 'render exited ' + code, hint: 'set CHROME and PUPPETEER_CORE env vars for the server process' });
      }
      // downscale to the hero standard 2000x1250 and stamp provenance metadata
      const meta = JSON.stringify({ generator: 'hero-studio', version: '0.3.0', ts, state: { mode: state.mode, page: state.page, comp: state.comp, theme: state.theme, layout: state.layout, gradAngle: state.gradAngle } });
      const py = 'import sys\n' +
        'from PIL import Image\n' +
        'from PIL.PngImagePlugin import PngInfo\n' +
        'f, meta = sys.argv[1], sys.argv[2]\n' +
        'im = Image.open(f)\n' +
        'im = im.resize((2000, round(2000 * im.size[1] / im.size[0])), Image.LANCZOS)\n' +
        'info = PngInfo(); info.add_text("comment", meta)\n' +
        'im.save(f, pnginfo=info)\n';
      execFile('python3', ['-c', py, outFile, meta], (err) => {
        if (err) return execFile('convert', [outFile, '-resize', '2000x', '-set', 'comment', meta, outFile], (err2) => {
          if (err2) logLine('warn', 'no python3/PIL or convert; keeping full-size PNG without metadata');
          afterFinalize();
        });
        afterFinalize();
      });
      function afterFinalize() {
        try { fs.copyFileSync(outFile, path.join(outDir, 'latest.png')); } catch {}
        genSeq += 1;
        lastGen = { seq: genSeq, ok: true, file, url: 'generated/' + file, latest: 'generated/latest.png', ts, slug };
        logLine('info', 'generated ' + file);
        broadcast('generated', lastGen);
        broadcast('toast', { text: 'Hero generated: ' + file, kind: 'success', seq: ++toastSeq });
        lastToast = { text: 'Hero generated: ' + file, kind: 'success', seq: toastSeq };
        finish(lastGen);
      }
    });
    return;
  }

  if (p.startsWith('/generated/')) {
    const gfile = path.normalize(p.slice('/generated/'.length)).replace(/^(\.\.[/\\])+/, '');
    const gfull = path.join(__dirname, 'generated', gfile);
    if (!gfull.startsWith(path.join(__dirname, 'generated'))) { res.writeHead(404); return res.end('not found'); }
    return fs.readFile(gfull, (err, buf) => {
      if (err) { res.writeHead(404); return res.end('not found'); }
      res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-cache' });
      res.end(buf);
    });
  }

  // static
  let file = p === '/' ? '/index.html' : p;
  file = path.normalize(file).replace(/^(\.\.[/\\])+/, '');
  const full = path.join(ROOT, file);
  if (!full.startsWith(ROOT)) { res.writeHead(404); return res.end('not found'); }
  fs.readFile(full, (err, buf) => {
    if (err) { res.writeHead(404); return res.end('not found'); }
    res.writeHead(200, { 'Content-Type': MIME[path.extname(full)] || 'application/octet-stream' });
    res.end(buf);
  });
});

server.listen(PORT, () => console.log('hero-studio on ' + PORT));