'use strict';
// ═══════════════════════════════════════════════
// js/identity.js — ECDH identity, keypair, crypto helpers
//
// ARCHITECTURE NOTE: No signaling server is used.
// - Each user has a permanent ECDH P-256 keypair stored in IndexedDB.
// - The identity code is their ECDH public key only (~90 chars base64).
// - Journal data is encrypted with a key derived via ECDH between the
//   two peers' keypairs — no shared secret is ever transmitted.
// - STUN servers (Google's public servers) are used only during WebRTC
//   ICE candidate gathering to discover your public IP/port for NAT
//   traversal. They never see any journal data or content.
// ═══════════════════════════════════════════════

function toUrlSafeB64(b64) {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function fromUrlSafeB64(s) {
  s = s.replace(/-/g, '+').replace(/_/g, '/');
  while (s.length % 4) s += '=';
  return s;
}
function b64FromBytes(bytes) {
  return btoa(String.fromCharCode(...new Uint8Array(bytes)));
}
function bytesFromB64(b64) {
  return Uint8Array.from(atob(fromUrlSafeB64(b64)), c => c.charCodeAt(0));
}

// ─── ECDH keypair in IDB ─────────────────────────────────────────────────────
// Stored as exported raw bytes (public) and pkcs8 (private) under key 'frj_keypair'
// IDB store: frj_blobs (same store, key = 'frj_keypair_pub' / 'frj_keypair_priv')

let _identityCache = null; // { privKey: CryptoKey, pubKey: CryptoKey, pubKeyB64: string, displayName: string }

async function getIdentity() {
  if (_identityCache) return _identityCache;

  const pub = await idbGet('frj_keypair_pub');
  const priv = await idbGet('frj_keypair_priv');

  if (!pub || !priv) return null;

  const pubKey = await crypto.subtle.importKey(
    'raw', pub, { name: 'ECDH', namedCurve: 'P-256' }, true, []
  );
  const privKey = await crypto.subtle.importKey(
    'pkcs8', priv, { name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveKey']
  );

  const pubKeyB64 = toUrlSafeB64(b64FromBytes(pub));
  const displayName = lsGet('frj_displayname', '') || 'Restorer';

  _identityCache = { privKey, pubKey, pubKeyB64, displayName };
  return _identityCache;
}

async function generateIdentity() {
  // Generate as extractable:true so we can export raw bytes for IDB storage
  const kp = await crypto.subtle.generateKey(
    { name: 'ECDH', namedCurve: 'P-256' },
    true,
    ['deriveKey']
  );

  const pubRaw = await crypto.subtle.exportKey('raw', kp.publicKey);
  const privPkcs8 = await crypto.subtle.exportKey('pkcs8', kp.privateKey);

  await idbPut('frj_keypair_pub', pubRaw);
  await idbPut('frj_keypair_priv', privPkcs8);

  // Re-import private key as non-extractable immediately — closes the window
  // where an XSS/extension could call exportKey() on the in-memory CryptoKey
  const pubKey = await crypto.subtle.importKey(
    'raw', pubRaw, { name: 'ECDH', namedCurve: 'P-256' }, true, []
  );
  const privKey = await crypto.subtle.importKey(
    'pkcs8', privPkcs8, { name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveKey']
  );
  const pubKeyB64 = toUrlSafeB64(b64FromBytes(pubRaw));
  const displayName = lsGet('frj_displayname', '') || 'Restorer';

  _identityCache = { privKey, pubKey, pubKeyB64, displayName };
  return _identityCache;
}

async function setDisplayName(name) {
  lsSet('frj_displayname', name);
  if (_identityCache) _identityCache.displayName = name;
}

// Synchronous — safe to call any time after init()
function getDisplayNameSync() {
  return (_identityCache?.displayName) || lsGet('frj_displayname', '') || '';
}

// Whether you allow contacts to persist an offline copy of your journal
function getAllowOfflineCopy() {
  return localStorage.getItem('frj_allow_offline') === '1';
}
function setAllowOfflineCopy(val) {
  localStorage.setItem('frj_allow_offline', val ? '1' : '0');
}

// Identity code = just the ECDH public key
// Format: FR1:<ecdhPubKeyUrlSafeB64>
function makeIdentityCode(pubKeyB64) {
  return `FR1:${pubKeyB64}`;
}

function parseIdentityCode(code) {
  if (!code) return null;
  const m = code.trim().match(/^[Ff][Rr]1:([A-Za-z0-9_\-]+)$/);
  if (!m) return null;
  // Validate P-256 uncompressed public key: must be exactly 65 bytes, starting with 0x04
  try {
    const raw = bytesFromB64(m[1]);
    if (raw.length !== 65 || raw[0] !== 0x04) return null;
  } catch { return null; }
  return { pubKeyB64: m[1] };
}

// ─── ECDH shared key derivation ──────────────────────────────────────────────
// Both peers compute ECDH(my_priv, their_pub) → same AES-GCM key
// This key is unique to each pair. Never transmitted.
async function deriveSharedKey(myPrivKey, theirPubKeyB64) {
  const raw = bytesFromB64(theirPubKeyB64);
  const theirPubKey = await crypto.subtle.importKey(
    'raw', raw, { name: 'ECDH', namedCurve: 'P-256' }, false, []
  );
  return crypto.subtle.deriveKey(
    { name: 'ECDH', public: theirPubKey },
    myPrivKey,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

// ─── HMAC key derivation for message authentication ─────────────────────────
// Same ECDH exchange as deriveSharedKey but outputs an HMAC-SHA-256 key
async function deriveHmacKey(myPrivKey, theirPubKeyB64) {
  const raw = bytesFromB64(theirPubKeyB64);
  const theirPubKey = await crypto.subtle.importKey(
    'raw', raw, { name: 'ECDH', namedCurve: 'P-256' }, false, []
  );
  return crypto.subtle.deriveKey(
    { name: 'ECDH', public: theirPubKey },
    myPrivKey,
    { name: 'HMAC', hash: 'SHA-256', length: 256 },
    false,
    ['sign', 'verify']
  );
}

// HMAC-SHA-256 sign — returns url-safe base64
async function hmacSign(payload, hmacKey) {
  const data = new TextEncoder().encode(JSON.stringify(payload));
  const sig = await crypto.subtle.sign('HMAC', hmacKey, data);
  return toUrlSafeB64(b64FromBytes(sig));
}

// HMAC-SHA-256 verify — returns boolean
async function hmacVerify(payload, sig, hmacKey) {
  const data = new TextEncoder().encode(JSON.stringify(payload));
  const sigBytes = bytesFromB64(sig);
  return crypto.subtle.verify('HMAC', hmacKey, sigBytes, data);
}

// ─── AES-GCM encrypt/decrypt with a derived CryptoKey ────────────────────────
async function encryptWithKey(obj, key) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const enc = new TextEncoder().encode(JSON.stringify(obj));
  const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc);
  return {
    iv: toUrlSafeB64(b64FromBytes(iv)),
    data: toUrlSafeB64(b64FromBytes(ct))
  };
}

async function decryptWithKey(payload, key) {
  const iv = bytesFromB64(payload.iv);
  const ct = bytesFromB64(payload.data);
  const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
  return JSON.parse(new TextDecoder().decode(pt));
}

// ─── ECDSA entry signing ─────────────────────────────────────────────────────
// Same P-256 key bytes work for ECDSA — just import under a different algorithm.
// Signs sync entry metadata so paired devices can verify authenticity.

async function signEntry(payload) {
  const privBytes = await idbGet('frj_keypair_priv');
  if (!privBytes) throw new Error('No identity key for signing');
  const signingKey = await crypto.subtle.importKey(
    'pkcs8', privBytes,
    { name: 'ECDSA', namedCurve: 'P-256' },
    false, ['sign']
  );
  const data = new TextEncoder().encode(JSON.stringify(payload));
  const sig = await crypto.subtle.sign(
    { name: 'ECDSA', hash: 'SHA-256' },
    signingKey, data
  );
  return toUrlSafeB64(b64FromBytes(sig));
}

async function verifyEntrySignature(payload, sig, pubKeyB64) {
  const pubBytes = bytesFromB64(pubKeyB64);
  const verifyKey = await crypto.subtle.importKey(
    'raw', pubBytes,
    { name: 'ECDSA', namedCurve: 'P-256' },
    false, ['verify']
  );
  const data = new TextEncoder().encode(JSON.stringify(payload));
  const sigBytes = bytesFromB64(sig);
  return crypto.subtle.verify(
    { name: 'ECDSA', hash: 'SHA-256' },
    verifyKey, sigBytes, data
  );
}

// ─── Identity backup / restore ────────────────────────────────────────────────
// FRK2 format: FRK2:<salt_b64>:<iv_b64>:<ciphertext_b64>
//   - Keypair JSON encrypted with AES-256-GCM derived from user's at-rest password
//   - Requires password to import — safe to screenshot/store (still not recommended)
// FRK1 format (legacy): FRK1:<urlsafeB64(JSON({priv,pub}))> — plaintext, backward compat

async function exportIdentityBackup(password) {
  const privRaw = await idbGet('frj_keypair_priv');
  const pubRaw = await idbGet('frj_keypair_pub');
  if (!privRaw || !pubRaw) throw new Error('No identity to export');
  const payload = JSON.stringify({
    priv: b64FromBytes(privRaw),
    pub: b64FromBytes(pubRaw)
  });

  // If password provided, produce encrypted FRK2 code
  if (password) {
    const salt = crypto.getRandomValues(new Uint8Array(16));
    const key = await deriveKey(password, salt, 600000);
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const ct = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv }, key, new TextEncoder().encode(payload)
    );
    return 'FRK2:' +
      toUrlSafeB64(b64FromBytes(salt)) + ':' +
      toUrlSafeB64(b64FromBytes(iv)) + ':' +
      toUrlSafeB64(b64FromBytes(ct));
  }

  // Fallback: plaintext FRK1 (for unencrypted devices — should be rare)
  return 'FRK1:' + toUrlSafeB64(b64FromBytes(new TextEncoder().encode(payload)));
}

async function importIdentityBackup(code, password) {
  const trimmed = code.trim();
  let payload;

  // ── FRK2: encrypted backup ────────────────────────────────────────────────
  const m2 = trimmed.match(/^FRK2:([A-Za-z0-9_\-]+):([A-Za-z0-9_\-]+):([A-Za-z0-9_\-]+)$/);
  if (m2) {
    if (!password) throw new Error('This backup code is encrypted. Enter your password to restore.');
    try {
      const salt = bytesFromB64(m2[1]);
      const iv   = bytesFromB64(m2[2]);
      const ct   = bytesFromB64(m2[3]);
      const key  = await deriveKey(password, salt, 600000);
      const pt   = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
      payload = JSON.parse(new TextDecoder().decode(pt));
    } catch(e) {
      if (e.name === 'OperationError') throw new Error('Wrong password for this backup code.');
      throw new Error('Backup code is corrupted or truncated');
    }
  } else {
    // ── FRK1: legacy plaintext backup ─────────────────────────────────────────
    const m1 = trimmed.match(/^FRK1:([A-Za-z0-9_\-]+)$/);
    if (!m1) throw new Error('Invalid backup code. Must start with FRK2: (or legacy FRK1:)');
    try {
      payload = JSON.parse(new TextDecoder().decode(bytesFromB64(m1[1])));
    } catch(e) {
      throw new Error('Backup code is corrupted or truncated');
    }
  }

  if (!payload.priv || !payload.pub) throw new Error('Backup code is missing key data');

  // Validate by importing — throws if data is wrong
  const privBytes = Uint8Array.from(atob(payload.priv), c => c.charCodeAt(0));
  const pubBytes = Uint8Array.from(atob(payload.pub), c => c.charCodeAt(0));
  await crypto.subtle.importKey('pkcs8', privBytes, { name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveKey']);
  await crypto.subtle.importKey('raw', pubBytes, { name: 'ECDH', namedCurve: 'P-256' }, true, []);

  await idbPut('frj_keypair_priv', privBytes.buffer);
  await idbPut('frj_keypair_pub', pubBytes.buffer);
  _identityCache = null;
}
