const crypto = require('crypto');

const SESSION_KEY_BYTES = 32;

function publicKeyDer(publicKeyPem) {
  return crypto.createPublicKey(publicKeyPem).export({
    type: 'spki',
    format: 'der',
  });
}

function publicKeyFingerprint(publicKeyPem) {
  return crypto.createHash('sha256').update(publicKeyDer(publicKeyPem)).digest('hex');
}

function createIdentity() {
  const pair = crypto.generateKeyPairSync('ed25519');
  const publicKeyPem = pair.publicKey.export({
    type: 'spki',
    format: 'pem',
  }).toString();
  const privateKeyPem = pair.privateKey.export({
    type: 'pkcs8',
    format: 'pem',
  }).toString();
  return {
    publicKeyPem,
    privateKeyPem,
    fingerprint: publicKeyFingerprint(publicKeyPem),
  };
}

function validIdentity(identity) {
  if (!identity || !identity.publicKeyPem || !identity.privateKeyPem) {
    return false;
  }
  try {
    const derivedPublic = crypto.createPublicKey(identity.privateKeyPem).export({
      type: 'spki',
      format: 'der',
    });
    return crypto.timingSafeEqual(derivedPublic, publicKeyDer(identity.publicKeyPem));
  } catch (error) {
    return false;
  }
}

function ensureIdentity(targetStore) {
  if (!validIdentity(targetStore.identity)) {
    targetStore.identity = createIdentity();
    return true;
  }
  const fingerprint = publicKeyFingerprint(targetStore.identity.publicKeyPem);
  if (targetStore.identity.fingerprint !== fingerprint) {
    targetStore.identity.fingerprint = fingerprint;
    return true;
  }
  return false;
}

function shortFingerprint(fingerprint) {
  const value = String(fingerprint || '').toUpperCase().replace(/[^A-F0-9]/g, '');
  return value.slice(0, 16).match(/.{1,4}/g)?.join(' ') || 'Unknown';
}

function createEphemeralKeyPair() {
  const pair = crypto.generateKeyPairSync('x25519');
  return {
    publicKeyPem: pair.publicKey.export({
      type: 'spki',
      format: 'pem',
    }).toString(),
    privateKeyPem: pair.privateKey.export({
      type: 'pkcs8',
      format: 'pem',
    }).toString(),
  };
}

function randomToken(bytes) {
  return crypto.randomBytes(bytes || 24).toString('base64');
}

function canonicalMembers(members) {
  return (members || []).map(member => ({
    hostId: String(member.hostId || ''),
    publicKeyPem: String(member.publicKeyPem || ''),
    challenge: String(member.challenge || ''),
    ephemeralPublicKeyPem: String(member.ephemeralPublicKeyPem || ''),
    pairingNonce: String(member.pairingNonce || ''),
    connectRequested: !!member.connectRequested,
  })).sort((a, b) => a.hostId.localeCompare(b.hostId));
}

function socketTranscript(members) {
  return JSON.stringify({
    purpose: 'openmouse-secure-session-v1',
    members: canonicalMembers(members).map(member => ({
      hostId: member.hostId,
      publicKeyPem: member.publicKeyPem,
      challenge: member.challenge,
      ephemeralPublicKeyPem: member.ephemeralPublicKeyPem,
      connectRequested: member.connectRequested,
    })),
  });
}

function pairingTranscript(pairingId, members) {
  return JSON.stringify({
    purpose: 'openmouse-device-pairing-v1',
    pairingId: String(pairingId || ''),
    members: canonicalMembers(members),
  });
}

function pairingCode(transcript) {
  const digest = crypto.createHash('sha256').update(String(transcript)).digest();
  return String(digest.readUInt32BE(0) % 1000000).padStart(6, '0');
}

function signTranscript(privateKeyPem, transcript) {
  return crypto.sign(null, Buffer.from(String(transcript)), privateKeyPem).toString('base64');
}

function verifyTranscript(publicKeyPem, transcript, signature) {
  try {
    return crypto.verify(
      null,
      Buffer.from(String(transcript)),
      publicKeyPem,
      Buffer.from(String(signature || ''), 'base64')
    );
  } catch (error) {
    return false;
  }
}

function hkdfSha256(secret, salt, info, length) {
  const outputLength = Number(length || SESSION_KEY_BYTES);
  const actualSalt = salt && salt.length ? salt : Buffer.alloc(32);
  const prk = crypto.createHmac('sha256', actualSalt).update(secret).digest();
  let previous = Buffer.alloc(0);
  let output = Buffer.alloc(0);
  let counter = 1;
  while (output.length < outputLength) {
    previous = crypto.createHmac('sha256', prk)
      .update(previous)
      .update(info || Buffer.alloc(0))
      .update(Buffer.from([counter]))
      .digest();
    output = Buffer.concat([output, previous]);
    counter += 1;
  }
  return output.slice(0, outputLength);
}

function deriveSessionKey(localPrivateKeyPem, remotePublicKeyPem, transcript) {
  const sharedSecret = crypto.diffieHellman({
    privateKey: crypto.createPrivateKey(localPrivateKeyPem),
    publicKey: crypto.createPublicKey(remotePublicKeyPem),
  });
  const salt = crypto.createHash('sha256').update(String(transcript)).digest();
  return hkdfSha256(
    sharedSecret,
    salt,
    Buffer.from('openmouse-session-key-v1'),
    SESSION_KEY_BYTES
  );
}

function frameAad(context, sequence) {
  const source = context || {};
  return Buffer.from([
    'openmouse-frame-v1',
    source.transport || 'tcp',
    source.senderHostId || '',
    source.receiverHostId || '',
    Number(sequence),
  ].join('|'));
}

function sealFrame(key, frame, context, sequence) {
  const seq = Number(sequence);
  if (!Number.isSafeInteger(seq) || seq < 1) {
    throw new Error('Invalid secure-frame sequence');
  }
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  cipher.setAAD(frameAad(context, seq));
  const encrypted = Buffer.concat([
    cipher.update(JSON.stringify(frame), 'utf8'),
    cipher.final(),
  ]);
  return {
    type: 'sealed',
    seq,
    iv: iv.toString('base64'),
    tag: cipher.getAuthTag().toString('base64'),
    data: encrypted.toString('base64'),
  };
}

function openFrame(key, wrapper, context) {
  const seq = Number(wrapper && wrapper.seq);
  if (!Number.isSafeInteger(seq) || seq < 1) {
    throw new Error('Invalid secure-frame sequence');
  }
  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    key,
    Buffer.from(wrapper.iv || '', 'base64')
  );
  decipher.setAAD(frameAad(context, seq));
  decipher.setAuthTag(Buffer.from(wrapper.tag || '', 'base64'));
  const clear = Buffer.concat([
    decipher.update(Buffer.from(wrapper.data || '', 'base64')),
    decipher.final(),
  ]);
  return JSON.parse(clear.toString('utf8'));
}

module.exports = {
  createEphemeralKeyPair,
  createIdentity,
  deriveSessionKey,
  ensureIdentity,
  openFrame,
  pairingCode,
  pairingTranscript,
  publicKeyFingerprint,
  randomToken,
  sealFrame,
  shortFingerprint,
  signTranscript,
  socketTranscript,
  validIdentity,
  verifyTranscript,
};
