#!/usr/bin/env node
// =============================================================================
// /opt/timmy-tower/webhook.js
// Lightweight webhook receiver: validates Gitea HMAC signature, runs deploy.sh
// Runs as systemd service: timmy-deploy-hook
//
// Security: fails closed — if WEBHOOK_SECRET is not set, the server refuses
// to start rather than accepting unsigned requests.
// =============================================================================
'use strict';

const http = require('http');
const crypto = require('crypto');
const { execFile } = require('child_process');
const fs = require('fs');
const path = require('path');

const PORT = 9000;
const HOST = '127.0.0.1';
const SECRET = process.env.WEBHOOK_SECRET || '';
const DEPLOY_SCRIPT = path.join(__dirname, 'deploy.sh');
const LOG_FILE = path.join(__dirname, 'deploy.log');

// Fail closed — do not start without a secret
if (!SECRET) {
  process.stderr.write(
    '[webhook] FATAL: WEBHOOK_SECRET environment variable is not set.\n' +
    '  Set it in /opt/timmy-tower/.env and restart the service.\n'
  );
  process.exit(1);
}

// Single-slot deploy queue: tracks current deploy + whether another is pending
let deploying = false;
let pendingDeploy = null; // { pusher, commit } of the queued request, or null

function log(msg) {
  const line = `[${new Date().toISOString()}] [webhook] ${msg}\n`;
  process.stdout.write(line);
  try { fs.appendFileSync(LOG_FILE, line); } catch (_) {}
}

function verifySignature(payload, signature) {
  const hmac = crypto.createHmac('sha256', SECRET);
  hmac.update(payload);
  const hexDigest = hmac.digest('hex');
  // Gitea sends raw hex in X-Gitea-Signature; GitHub/others send sha256=<hex>
  // Normalise both sides to raw hex before comparing
  const incomingHex = signature.startsWith('sha256=')
    ? signature.slice(7)
    : signature;
  if (!incomingHex || Buffer.byteLength(hexDigest) !== Buffer.byteLength(incomingHex)) return false;
  try {
    return crypto.timingSafeEqual(
      Buffer.from(hexDigest, 'utf8'),
      Buffer.from(incomingHex, 'utf8')
    );
  } catch (_) {
    return false;
  }
}

function runDeploy(pusher, commit) {
  deploying = true;
  log(`Running deploy — pusher: ${pusher}, commit: ${commit}`);
  execFile('bash', [DEPLOY_SCRIPT], { timeout: 600_000 }, (err) => {
    deploying = false;
    if (err) {
      log(`Deploy FAILED: ${err.message}`);
    } else {
      log(`Deploy succeeded (commit ${commit})`);
    }
    // If another push arrived while we were deploying, run it now
    if (pendingDeploy) {
      const next = pendingDeploy;
      pendingDeploy = null;
      log(`Running queued deploy — pusher: ${next.pusher}, commit: ${next.commit}`);
      runDeploy(next.pusher, next.commit);
    }
  });
}

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true, deploying, queued: !!pendingDeploy }));
    return;
  }

  if (req.method !== 'POST' || req.url !== '/deploy') {
    res.writeHead(404).end('Not found');
    return;
  }

  const chunks = [];
  req.on('data', chunk => chunks.push(chunk));
  req.on('end', () => {
    const body = Buffer.concat(chunks);
    const sig = req.headers['x-gitea-signature']
      || req.headers['x-hub-signature-256']
      || '';

    if (!verifySignature(body, sig)) {
      log(`Rejected: invalid HMAC from ${req.socket.remoteAddress}`);
      res.writeHead(401).end('Unauthorized');
      return;
    }

    let payload = {};
    try { payload = JSON.parse(body.toString()); } catch (_) {}

    const branch = (payload.ref || '').replace('refs/heads/', '');
    if (branch && branch !== 'main') {
      log(`Skipping push to non-main branch: ${branch}`);
      res.writeHead(200).end('Skipped non-main branch');
      return;
    }

    const pusher = payload.pusher
      ? (payload.pusher.login || payload.pusher.name || 'unknown')
      : 'unknown';
    const commit = payload.after ? payload.after.slice(0, 8) : 'unknown';

    if (deploying) {
      // Queue the latest push — overwrite any previous queued one
      pendingDeploy = { pusher, commit };
      log(`Deploy in progress — queued commit ${commit} (will run after current deploy)`);
      res.writeHead(202).end('Queued — will deploy after current deploy completes');
      return;
    }

    res.writeHead(202).end('Deploy started');
    runDeploy(pusher, commit);
  });
});

server.listen(PORT, HOST, () => {
  log(`Webhook receiver listening on ${HOST}:${PORT}`);
  log(`Deploy script: ${DEPLOY_SCRIPT}`);
  log('HMAC secret: configured');
});

process.on('SIGTERM', () => { log('SIGTERM — shutting down'); process.exit(0); });
process.on('SIGINT',  () => { log('SIGINT — shutting down');  process.exit(0); });
