// KueSync Agent — per-machine cryptographic identity (Ed25519). © 2026 Krometis Analytics.
//
// FOUNDATION for KueSync Link (secure channel + publish authorization) AND the licensing tightening
// (seat binding, signed ephemeral grants, server-side revoke). It is the thing the codebase does NOT
// have today: real per-machine crypto identity (current fleet trust is a shared password/token, and
// candidateId is a spoofable string — see docs/kwiksync/phase0-map.md §1).
//
// STRICTLY ADDITIVE: this module is NOT imported by any 0.8.2 runtime path. It is pure + injectable so
// it unit-tests on its own, mirroring license.js. Private key bytes live in an injected KeyStore (OS
// keychain adapter in production; a 0600 file fallback otherwise) — NEVER in leader.json. The public
// key binds to the existing stable candidateId so it slots into discovery/election rows unchanged.

import {
  generateKeyPairSync, sign as edSign, verify as edVerify,
  createPublicKey, createPrivateKey, createHash,
} from 'node:crypto'
import nodeFs from 'node:fs'

// ── pure crypto primitives ───────────────────────────────────────────────────────────────────────

// A fresh Ed25519 identity. Returns PEM strings (pkcs8 private, spki public).
export function generateIdentityKeys() {
  const { publicKey, privateKey } = generateKeyPairSync('ed25519')
  return {
    privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
    publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
  }
}

// Whitespace-insensitive PEM comparison helper (so re-exported keys compare equal).
export function normalizePem(pem) {
  return String(pem || '').replace(/\s+/g, '')
}

// Derive the SPKI public key PEM from a private key PEM.
export function publicFromPrivate(privateKeyPem) {
  return createPublicKey(createPrivateKey(privateKeyPem)).export({ type: 'spki', format: 'pem' }).toString()
}

// Stable short fingerprint of a public key (sha256 of SPKI DER, first 16 hex) — for logs + dedup.
export function publicKeyId(publicKeyPem) {
  const der = createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' })
  return createHash('sha256').update(der).digest('hex').slice(0, 16)
}

// Only string/Buffer are accepted. Callers that sign structured data MUST canonicalize to a string
// first (see roster.js / grants.js canonical()) — signing a raw object via JSON is key-order dependent
// and would yield non-verifiable signatures, so it is disallowed.
function toBuf(data) {
  if (Buffer.isBuffer(data)) return data
  if (typeof data === 'string') return Buffer.from(data, 'utf8')
  throw new TypeError('signData/verifyData accept a string or Buffer (canonicalize objects first)')
}

// Detached Ed25519 signature over data; returns base64url (matches license.js token style).
export function signData(privateKeyPem, data) {
  return edSign(null, toBuf(data), createPrivateKey(privateKeyPem)).toString('base64url')
}

// Verify a base64url detached signature. Never throws — bad input returns false (fail-closed).
export function verifyData(publicKeyPem, data, sigB64url) {
  try {
    return edVerify(null, toBuf(data), createPublicKey(publicKeyPem), Buffer.from(String(sigB64url || ''), 'base64url'))
  } catch {
    return false
  }
}

// ── KeyStore: where the private key bytes are persisted ──────────────────────────────────────────
// Interface: { get(): string|null, set(pem: string): void }. Production adapters (added later):
//   - macOS:   `security` CLI (login keychain)
//   - Windows: DPAPI / Credential Manager
// Default fallback below: a 0600 file OUTSIDE leader.json (never in the JSON config, per spec §4.1).

export class FileKeyStore {
  constructor(filePath, fs = nodeFs) { this.path = filePath; this.fs = fs }
  get() {
    try { return this.fs.existsSync(this.path) ? this.fs.readFileSync(this.path, 'utf8') : null } catch { return null }
  }
  set(pem) {
    // Atomic: write a 0600 temp then rename, so a crash can't leave a half-written private key.
    const tmp = this.path + '.tmp'
    this.fs.writeFileSync(tmp, pem, { mode: 0o600 })
    try { this.fs.chmodSync(tmp, 0o600) } catch {}
    this.fs.renameSync(tmp, this.path)
  }
}

// In-memory keystore (tests / ephemeral).
export class MemoryKeyStore {
  constructor(seed = null) { this._v = seed }
  get() { return this._v }
  set(pem) { this._v = pem }
}

// ── load-or-create the machine identity ──────────────────────────────────────────────────────────
// Idempotent: first call generates + persists; later calls return the same key. Binds to candidateId.
// Returns a small handle: { candidateId, publicKeyPem, keyId, created, sign(data) }.
export function loadOrCreateIdentity({ keystore, candidateId }) {
  if (!keystore || typeof keystore.get !== 'function' || typeof keystore.set !== 'function') {
    throw new Error('loadOrCreateIdentity: a KeyStore with get()/set() is required')
  }
  let priv = keystore.get()
  let created = false
  // Validate the stored key; if missing OR unparseable (corrupt keystore), generate a fresh one rather
  // than crashing. (A keystore.get()/set() that itself throws propagates to the caller to handle.)
  let publicKeyPem
  try {
    if (!priv) throw new Error('no key')
    publicKeyPem = publicFromPrivate(priv)
  } catch {
    priv = generateIdentityKeys().privateKeyPem
    keystore.set(priv)
    created = true
    publicKeyPem = publicFromPrivate(priv)
  }
  return {
    candidateId: String(candidateId || ''),
    publicKeyPem,
    keyId: publicKeyId(publicKeyPem),
    created,
    sign: (data) => signData(priv, data),
  }
}
