// AGENT 破甲 - Ed25519 license verification (own keypair)
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const os = require('os');

const EMBEDDED_PUBLIC_KEY_HEX = '7b8912dca2f9268d12028865117042617632316fe5a82b930b7448653b440758';
const PRODUCT = 'agent-pojia';
const APP_VERSION = '1.0.0';
const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
const B64URL_RE = /^[A-Za-z0-9_-]*$/;
const LICENSE_TYPES = ['permanent', 'trial'];

function appDataDir() {
  if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Application Support', 'AGENT破甲');
  const local = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
  return path.join(local, 'AGENT破甲');
}
function licenseDir() { return path.join(appDataDir(), 'license'); }
function licenseFile() { return path.join(licenseDir(), 'activation.json'); }

function b64urlDecode(s) {
  if (typeof s !== 'string' || !B64URL_RE.test(s)) throw new Error('bad-base64url');
  s = s.replace(/-/g, '+').replace(/_/g, '/');
  while (s.length % 4) s += '=';
  return Buffer.from(s, 'base64');
}
function canonical(obj) {
  const keys = Object.keys(obj).sort();
  const out = {};
  for (const k of keys) out[k] = obj[k];
  return JSON.stringify(out);
}
function publicKeyObject() {
  const raw = Buffer.from(EMBEDDED_PUBLIC_KEY_HEX, 'hex');
  return crypto.createPublicKey({ key: Buffer.concat([SPKI_PREFIX, raw]), format: 'der', type: 'spki' });
}
function matchVersion(rule, ver) {
  if (!rule || rule === '*') return true;
  try { return new RegExp(rule).test(ver); } catch { return rule === ver; }
}

// nowMs: effective current time (caller may pass max(wall clock, last_seen_at)
// so that rolling the system clock back cannot extend a trial).
function verifyToken(token, nowMs) {
  try {
    const now = typeof nowMs === 'number' ? nowMs : Date.now();
    if (typeof token !== 'string') return { ok: false, reason: 'bad-token' };
    const parts = token.split('.');
    if (parts.length !== 2) return { ok: false, reason: 'token-malformed' };
    const payloadJson = b64urlDecode(parts[0]).toString('utf8');
    const sig = b64urlDecode(parts[1]);
    if (sig.length !== 64) return { ok: false, reason: 'bad-sig-length' };
    const payload = JSON.parse(payloadJson);
    if (typeof payload !== 'object' || payload === null) return { ok: false, reason: 'bad-payload' };
    const canonicalStr = canonical(payload);
    if (!crypto.verify(null, Buffer.from(canonicalStr, 'utf8'), publicKeyObject(), sig)) return { ok: false, reason: 'bad-signature' };
    if (payload.product !== PRODUCT) return { ok: false, reason: 'wrong-product' };
    if (!matchVersion(payload.version_rule || '*', APP_VERSION)) return { ok: false, reason: 'version-mismatch' };
    if (!LICENSE_TYPES.includes(payload.license_type)) return { ok: false, reason: 'bad-license-type' };
    const issued = Date.parse(payload.issued_at || '');
    if (Number.isNaN(issued)) return { ok: false, reason: 'bad-issued_at' };
    if (now < issued) return { ok: false, reason: 'issued-in-future' };
    if (payload.license_type !== 'permanent') {
      const days = Number(payload.duration_days);
      if (!Number.isInteger(days) || days <= 0) return { ok: false, reason: 'invalid-trial-duration' };
      if (now > issued + days * 86400000) return { ok: false, reason: 'expired' };
    }
    return { ok: true, payload };
  } catch {
    return { ok: false, reason: 'exception' };
  }
}

function readCache() {
  try { return JSON.parse(fs.readFileSync(licenseFile(), 'utf8')); } catch { return null; }
}

function status() {
  if (!fs.existsSync(licenseFile())) return { activated: false };
  const data = readCache();
  if (!data || typeof data.token !== 'string') return { activated: false, reason: 'corrupt' };
  const lastSeen = Date.parse(data.last_seen_at || '');
  const now = Date.now();
  if (!Number.isNaN(lastSeen) && now < lastSeen) return { activated: false, reason: 'clock-rollback' };
  const effectiveNow = Number.isNaN(lastSeen) ? now : Math.max(now, lastSeen);
  const v = verifyToken(data.token, effectiveNow);
  if (!v.ok) return { activated: false, reason: v.reason };
  return {
    activated: true,
    license_type: v.payload.license_type,
    serial: v.payload.serial,
    version_rule: v.payload.version_rule,
    duration_days: v.payload.duration_days,
    issued_at: v.payload.issued_at,
    last_seen_at: data.last_seen_at || null
  };
}

function activate(token) {
  const v = verifyToken(token, Date.now());
  if (!v.ok) return { ok: false, reason: v.reason };
  fs.mkdirSync(licenseDir(), { recursive: true });
  fs.writeFileSync(licenseFile(), JSON.stringify({ token, last_seen_at: new Date().toISOString() }, null, 2));
  return { ok: true, payload: v.payload };
}

function touch() {
  const s = status();
  if (s.activated) {
    try {
      const data = readCache();
      if (!data) return s.activated;
      const lastSeen = Date.parse(data.last_seen_at || '');
      const now = Date.now();
      // Never move last_seen_at backwards.
      data.last_seen_at = new Date(Number.isNaN(lastSeen) ? now : Math.max(now, lastSeen)).toISOString();
      fs.writeFileSync(licenseFile(), JSON.stringify(data, null, 2));
    } catch {}
  }
  return s.activated;
}

module.exports = { verifyToken, status, activate, touch, appDataDir, licenseFile, PRODUCT, APP_VERSION };
