#!/usr/bin/env node
 
const http = require('http');
const { execFile } = require('child_process');
const fs = require('fs');
const path = require('path');

const PORT = process.env.CONTROL_PORT || 8080;
const TOKEN = process.env.CONTROL_TOKEN || '';
const FTP_ROOT = process.env.PHOTOBOOTH_ROOT || '/photobooth';
const FTP_SYSTEM_USER = process.env.FTP_SYSTEM_USER || 'ftpuser';
const FTP_SYSTEM_GROUP = process.env.FTP_SYSTEM_GROUP || 'ftpgroup';
const FTP_SYSTEM_UID = process.env.FTP_SYSTEM_UID || '2100';
const FTP_SYSTEM_GID = process.env.FTP_SYSTEM_GID || '2100';
const FTP_PW_CMD = process.env.PURE_PW_CMD || '/usr/sbin/pure-pw';

const jsonResponse = (res, status, payload) => {
  res.writeHead(status, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(payload));
};

const unauthorized = (res) => jsonResponse(res, 401, { error: { code: 'unauthorized', message: 'Missing or invalid token' } });

const parseBody = (req) => new Promise((resolve, reject) => {
  let data = '';
  req.on('data', (chunk) => {
    data += chunk;
    if (data.length > 1_000_000) {
      reject(new Error('Payload too large'));
      req.connection.destroy();
    }
  });
  req.on('end', () => {
    if (!data) return resolve({});
    try {
      resolve(JSON.parse(data));
    } catch {
      reject(new Error('Invalid JSON'));
    }
  });
});

const run = (cmd, args) => new Promise((resolve, reject) => {
  execFile(cmd, args, (error, stdout, stderr) => {
    if (error) {
      error.stdout = stdout;
      error.stderr = stderr;
      return reject(error);
    }
    resolve({ stdout, stderr });
  });
});

const sanitizePath = (input) => {
  if (typeof input !== 'string' || !input.trim()) {
    throw new Error('path required');
  }
  if (input.includes('..')) {
    throw new Error('invalid path');
  }
  const cleaned = input.trim().replace(/^\/+/, '');
  if (!cleaned) {
    throw new Error('invalid path');
  }
  return cleaned;
};

const sanitizeUsername = (input) => {
  if (typeof input !== 'string' || !input.trim()) {
    throw new Error('username required');
  }
  const cleaned = input.trim();
  if (!/^[a-zA-Z0-9_-]{3,64}$/.test(cleaned)) {
    throw new Error('invalid username');
  }
  return cleaned;
};

const requirePassword = (input) => {
  if (typeof input !== 'string' || input.length < 4) {
    throw new Error('password required');
  }
  return input;
};

const ensureDir = (absolutePath) => {
  fs.mkdirSync(absolutePath, { recursive: true });
};

const chownDir = (absolutePath) => new Promise((resolve, reject) => {
  fs.chown(absolutePath, Number(process.env.FTP_SYSTEM_UID || 2100), Number(process.env.FTP_SYSTEM_GID || 2100), (err) => {
    if (err) return reject(err);
    resolve();
  });
});

const addUser = async ({ username, password, relPath }) => {
  const target = path.join(FTP_ROOT, relPath);
  ensureDir(target);
  await chownDir(target);

  const args = [
    'useradd',
    username,
    '-m',
    '-u', String(FTP_SYSTEM_UID),
    '-g', String(FTP_SYSTEM_GID),
    '-d', target,
    '-p', password,
  ];

  await run(FTP_PW_CMD, args);
};

const rotateUser = async ({ username, password }) => {
  const args = ['passwd', username, '-m', '-p', password];
  await run(FTP_PW_CMD, args);
};

const deleteUser = async ({ username }) => {
  const args = ['userdel', username, '-m'];
  await run(FTP_PW_CMD, args);
};

const notFound = (res) => jsonResponse(res, 404, { error: { code: 'not_found', message: 'Route not found' } });

const server = http.createServer(async (req, res) => {
  if (TOKEN && req.headers.authorization !== `Bearer ${TOKEN}`) {
    return unauthorized(res);
  }

  try {
    if (req.method === 'GET' && req.url === '/health') {
      return jsonResponse(res, 200, { ok: true });
    }

    if (req.method === 'POST' && req.url === '/users') {
      const body = await parseBody(req);
      const username = sanitizeUsername(body.username);
      const password = requirePassword(body.password);
      const relPath = sanitizePath(body.path);

      await addUser({ username, password, relPath });
      return jsonResponse(res, 201, { ok: true });
    }

    if (req.method === 'POST' && req.url.startsWith('/users/') && req.url.endsWith('/rotate')) {
      const username = sanitizeUsername(decodeURIComponent(req.url.split('/')[2] || ''));
      const body = await parseBody(req);
      const password = requirePassword(body.password);

      await rotateUser({ username, password });
      return jsonResponse(res, 200, { ok: true });
    }

    if (req.method === 'DELETE' && req.url.startsWith('/users/')) {
      const username = sanitizeUsername(decodeURIComponent(req.url.split('/')[2] || ''));
      try {
        await deleteUser({ username });
      } catch (error) {
        if (error.stderr && /No such user/.test(error.stderr)) {
          return jsonResponse(res, 200, { ok: true, note: 'user_absent' });
        }
        throw error;
      }
      return jsonResponse(res, 200, { ok: true });
    }

    if (req.method === 'POST' && req.url === '/config') {
      return jsonResponse(res, 200, { ok: true });
    }

    return notFound(res);
  } catch (error) {
    console.error('[control] error', error.message);
    return jsonResponse(res, 400, {
      error: {
        code: 'bad_request',
        message: error.message,
      },
    });
  }
});

server.listen(PORT, '0.0.0.0', () => {
  console.log(`[control] listening on ${PORT}`);
});
