#!/usr/bin/env node
/**
 * DashCaddy License Code Generator
 *
 * Admin-only CLI tool for generating license codes.
 * NOT shipped with the product — runs only on the developer's machine.
 *
 * Usage:
 *   node license-keygen.js --duration 365 --count 10
 *   node license-keygen.js --duration 30 --count 1 --output codes.txt
 *   node license-keygen.js --verify DC-XXXXX-XXXXX-XXXXX-XXXXX
 *   node license-keygen.js --init-secret
 */

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

// Master secret file — lives only on admin machine, NEVER shipped
const SECRET_FILE = path.join(__dirname, '.license-secret');

// License code format: DC-AAAAA-BBBBB-CCCCC-DDDDD
// Encodes: version(4bit) + duration_days(12bit) + code_id(32bit) + created_ts(32bit) + hmac(48bit)
// Total: 128 bits = 16 bytes, base32-encoded into 4 groups of 5 chars

const VALID_DURATIONS = [30, 90, 180, 365];
const LIFETIME_DURATION = 0; // Admin-only, not publicly available
const VERSION = 1;

// Base32 alphabet (Crockford variant — no I/L/O/U to avoid confusion)
const BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

function base32Encode(buffer) {
  let bits = '';
  for (const byte of buffer) {
    bits += byte.toString(2).padStart(8, '0');
  }
  // Pad to multiple of 5
  while (bits.length % 5 !== 0) bits += '0';
  let result = '';
  for (let i = 0; i < bits.length; i += 5) {
    const index = parseInt(bits.substring(i, i + 5), 2);
    result += BASE32[index];
  }
  return result;
}

function base32Decode(str) {
  let bits = '';
  for (const char of str.toUpperCase()) {
    const index = BASE32.indexOf(char);
    if (index === -1) throw new Error(`Invalid base32 character: ${char}`);
    bits += index.toString(2).padStart(5, '0');
  }
  const bytes = [];
  for (let i = 0; i + 8 <= bits.length; i += 8) {
    bytes.push(parseInt(bits.substring(i, i + 8), 2));
  }
  return Buffer.from(bytes);
}

function getSecret() {
  if (!fs.existsSync(SECRET_FILE)) {
    console.error('No master secret found. Run with --init-secret first.');
    process.exit(1);
  }
  return fs.readFileSync(SECRET_FILE, 'utf8').trim();
}

function initSecret() {
  if (fs.existsSync(SECRET_FILE)) {
    console.error('Master secret already exists at', SECRET_FILE);
    console.error('Delete it first if you want to regenerate (WARNING: invalidates all existing codes).');
    process.exit(1);
  }
  const secret = crypto.randomBytes(32).toString('hex');
  fs.writeFileSync(SECRET_FILE, secret, { mode: 0o600 });
  console.log('Master secret generated and saved to', SECRET_FILE);
  console.log('KEEP THIS FILE SAFE. It is needed to generate and validate all license codes.');
  console.log('DO NOT ship this file with the product.');
}

function generateCode(secret, durationDays, codeId) {
  // Pack payload: version(4b) + duration_days(12b) + code_id(32b) + created_ts(32b) = 80 bits = 10 bytes
  const payload = Buffer.alloc(10);

  // Byte 0-1: version (4 bits) + duration (12 bits) = 16 bits
  const versionAndDuration = ((VERSION & 0x0F) << 12) | (durationDays & 0x0FFF);
  payload.writeUInt16BE(versionAndDuration, 0);

  // Byte 2-5: code_id (32 bits)
  payload.writeUInt32BE(codeId, 2);

  // Byte 6-9: created timestamp (32 bits, seconds since epoch)
  const createdTs = Math.floor(Date.now() / 1000);
  payload.writeUInt32BE(createdTs, 6);

  // HMAC the payload to get signature
  const hmac = crypto.createHmac('sha256', secret).update(payload).digest();
  // Take first 5 bytes of HMAC (40 bits) — fits exactly in 25 base32 chars with 10-byte payload
  const signature = hmac.subarray(0, 5);

  // Combine: payload (10 bytes) + signature (5 bytes) = 15 bytes = 120 bits
  // 25 base32 chars = 125 bits, comfortably fits 120 bits
  const combined = Buffer.concat([payload, signature]);

  let encoded = base32Encode(combined);
  while (encoded.length < 25) encoded += '0';
  encoded = encoded.substring(0, 25);
  const groups = [];
  for (let i = 0; i < 25; i += 5) {
    groups.push(encoded.substring(i, i + 5));
  }

  return `DC-${groups.join('-')}`;
}

function parseCode(code) {
  // Strip prefix and dashes
  const cleaned = code.replace(/^DC-/, '').replace(/-/g, '');
  if (cleaned.length !== 25) {
    throw new Error(`Invalid code length: expected 25 base32 chars, got ${cleaned.length}`);
  }

  // Decode base32 — 25 chars = 125 bits = 15 full bytes
  const decoded = base32Decode(cleaned);
  if (decoded.length < 15) {
    const padded = Buffer.alloc(15);
    decoded.copy(padded);
    return parsePayload(padded);
  }
  return parsePayload(decoded.subarray(0, 15));
}

function parsePayload(buffer) {
  const payload = buffer.subarray(0, 10);
  const signature = buffer.subarray(10, 15);

  const versionAndDuration = payload.readUInt16BE(0);
  const version = (versionAndDuration >> 12) & 0x0F;
  const durationDays = versionAndDuration & 0x0FFF;
  const codeId = payload.readUInt32BE(2);
  const createdTs = payload.readUInt32BE(6);

  return { version, durationDays, codeId, createdTs, payload, signature };
}

function verifyCode(secret, code) {
  try {
    const { version, durationDays, codeId, createdTs, payload, signature } = parseCode(code);

    // Verify HMAC (5-byte signature)
    const expectedHmac = crypto.createHmac('sha256', secret).update(payload).digest();
    const expectedSig = expectedHmac.subarray(0, 5);

    if (!crypto.timingSafeEqual(signature, expectedSig)) {
      return { valid: false, reason: 'Invalid signature — code is forged or corrupted' };
    }

    if (version !== VERSION) {
      return { valid: false, reason: `Unsupported version: ${version}` };
    }

    // Accept lifetime (0) and standard durations
    if (durationDays !== LIFETIME_DURATION && !VALID_DURATIONS.includes(durationDays)) {
      return { valid: false, reason: `Invalid duration: ${durationDays} days` };
    }

    const createdDate = new Date(createdTs * 1000);
    const isLifetime = durationDays === LIFETIME_DURATION;
    const expiresDate = isLifetime ? null : new Date(createdTs * 1000 + durationDays * 86400000);

    return {
      valid: true,
      version,
      durationDays,
      codeId,
      createdAt: createdDate.toISOString(),
      expiresAt: isLifetime ? null : expiresDate.toISOString(),
      expired: isLifetime ? false : Date.now() > expiresDate.getTime()
    };
  } catch (error) {
    return { valid: false, reason: error.message };
  }
}

// CLI
function main() {
  const args = process.argv.slice(2);

  if (args.includes('--help') || args.length === 0) {
    console.log(`
DashCaddy License Code Generator

Usage:
  node license-keygen.js --init-secret              Initialize master secret (first time only)
  node license-keygen.js --duration <days> [options] Generate license codes
  node license-keygen.js --verify <code>             Verify a license code
  node license-keygen.js --decode <code>             Decode and display code details

Options:
  --duration <days>    Code validity: 30, 90, 180, or 365 days (required for generation)
  --count <n>          Number of codes to generate (default: 1)
  --start-id <n>       Starting code ID (default: auto from counter file)
  --output <file>      Write codes to file instead of stdout
  --json               Output as JSON

Valid durations: ${VALID_DURATIONS.join(', ')} days
    `);
    process.exit(0);
  }

  if (args.includes('--init-secret')) {
    initSecret();
    return;
  }

  if (args.includes('--verify') || args.includes('--decode')) {
    const codeIndex = args.indexOf('--verify') !== -1 ? args.indexOf('--verify') : args.indexOf('--decode');
    const code = args[codeIndex + 1];
    if (!code) {
      console.error('Please provide a code to verify.');
      process.exit(1);
    }
    const secret = getSecret();
    const result = verifyCode(secret, code);
    if (args.includes('--json')) {
      console.log(JSON.stringify(result, null, 2));
    } else if (result.valid) {
      const isLifetime = result.durationDays === 0;
      console.log('Code is VALID');
      console.log(`  Version:    ${result.version}`);
      console.log(`  Duration:   ${isLifetime ? 'LIFETIME' : result.durationDays + ' days'}`);
      console.log(`  Code ID:    ${result.codeId}`);
      console.log(`  Created:    ${result.createdAt}`);
      console.log(`  Expires:    ${isLifetime ? 'NEVER' : result.expiresAt}`);
      console.log(`  Status:     ${isLifetime ? 'LIFETIME' : (result.expired ? 'EXPIRED' : 'ACTIVE')}`);
    } else {
      console.log('Code is INVALID');
      console.log(`  Reason: ${result.reason}`);
    }
    return;
  }

  // Generate codes
  const isLifetime = args.includes('--lifetime');
  const durationIndex = args.indexOf('--duration');
  if (!isLifetime && durationIndex === -1) {
    console.error('--duration is required. Use --help for usage.');
    process.exit(1);
  }
  const duration = isLifetime ? LIFETIME_DURATION : parseInt(args[durationIndex + 1]);
  if (!isLifetime && !VALID_DURATIONS.includes(duration)) {
    console.error(`Invalid duration: ${duration}. Valid: ${VALID_DURATIONS.join(', ')}`);
    process.exit(1);
  }

  const countIndex = args.indexOf('--count');
  const count = countIndex !== -1 ? parseInt(args[countIndex + 1]) : 1;

  // Load or create counter file for auto-incrementing code IDs
  const counterFile = path.join(__dirname, '.license-counter');
  let startId;
  const startIdIndex = args.indexOf('--start-id');
  if (startIdIndex !== -1) {
    startId = parseInt(args[startIdIndex + 1]);
  } else if (fs.existsSync(counterFile)) {
    startId = parseInt(fs.readFileSync(counterFile, 'utf8').trim()) + 1;
  } else {
    startId = 1;
  }

  const secret = getSecret();
  const codes = [];

  for (let i = 0; i < count; i++) {
    const codeId = startId + i;
    const code = generateCode(secret, duration, codeId);
    codes.push({ code, codeId, durationDays: duration });
  }

  // Save counter
  fs.writeFileSync(counterFile, String(startId + count - 1));

  // Output
  const outputIndex = args.indexOf('--output');
  if (args.includes('--json')) {
    const output = JSON.stringify(codes, null, 2);
    if (outputIndex !== -1) {
      fs.writeFileSync(args[outputIndex + 1], output);
      console.log(`${count} code(s) written to ${args[outputIndex + 1]}`);
    } else {
      console.log(output);
    }
  } else {
    const lines = codes.map(c => `${c.code}  (${c.durationDays === 0 ? 'LIFETIME' : c.durationDays + ' days'}, ID: ${c.codeId})`);
    if (outputIndex !== -1) {
      fs.writeFileSync(args[outputIndex + 1], codes.map(c => c.code).join('\n') + '\n');
      console.log(`${count} code(s) written to ${args[outputIndex + 1]}`);
    } else {
      lines.forEach(l => console.log(l));
    }
  }

  console.log(`\nGenerated ${count} code(s) for ${duration === 0 ? 'LIFETIME' : duration + ' days'}. Next ID: ${startId + count}`);
}

// Also export for use by license-manager.js
module.exports = { verifyCode, parseCode, VALID_DURATIONS, VERSION };

if (require.main === module) {
  main();
}
