import { getCryptoBackend } from '@chat-app/shared/crypto';
import sodium from 'libsodium-wrappers-sumo';

import { pwhashArgon2id } from './nativeCryptoOps';

// Encrypts/decrypts the device private key with a user-provided passphrase
// so the backup string can be safely written down or stored in a password
// manager. Uses Argon2id (libsodium crypto_pwhash) for the KDF and
// XSalsa20-Poly1305 (crypto_secretbox) for the AEAD.
//
// Backup format (base64url-encoded blob, prefixed with a magic string so we
// can version it):
//
//   chatapp-backup-v1.<base64url(salt(16) | nonce(24) | ciphertext)>

const MAGIC = 'chatapp-backup-v1.';
const SALT_LEN = 16; // crypto_pwhash_SALTBYTES
const NONCE_LEN = 24; // crypto_secretbox_NONCEBYTES
const KEY_LEN = 32; // crypto_secretbox_KEYBYTES

async function ensureSodium(): Promise<typeof sodium> {
  await sodium.ready;
  return sodium;
}

function b64url(bytes: Uint8Array): string {
  let s = '';
  for (const b of bytes) s += String.fromCharCode(b);
  return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function unb64url(s: string): Uint8Array {
  let str = s.replace(/-/g, '+').replace(/_/g, '/');
  while (str.length % 4) str += '=';
  const bin = atob(str);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

async function deriveKey(passphrase: string, salt: Uint8Array, _sodiumLib: typeof sodium): Promise<Uint8Array> {
  return pwhashArgon2id({
    password: passphrase,
    salt,
    outLen: KEY_LEN,
    preset: 'moderate',
  });
}

export async function exportDeviceKey(
  privateKey: Uint8Array,
  passphrase: string,
): Promise<string> {
  if (passphrase.length < 8) throw new Error('Passphrase must be at least 8 characters.');
  const s = await ensureSodium();
  const salt = s.randombytes_buf(SALT_LEN);
  const nonce = s.randombytes_buf(NONCE_LEN);
  const key = await deriveKey(passphrase, salt, s);
  const backend = getCryptoBackend();
  const ciphertext = backend.secretbox(privateKey, nonce, key);
  s.memzero(key);
  const blob = new Uint8Array(SALT_LEN + NONCE_LEN + ciphertext.length);
  blob.set(salt, 0);
  blob.set(nonce, SALT_LEN);
  blob.set(ciphertext, SALT_LEN + NONCE_LEN);
  return MAGIC + b64url(blob);
}

export async function importDeviceKey(
  backup: string,
  passphrase: string,
): Promise<Uint8Array> {
  if (!backup.startsWith(MAGIC)) {
    throw new Error('Invalid backup format');
  }
  const blob = unb64url(backup.slice(MAGIC.length));
  if (blob.length < SALT_LEN + NONCE_LEN + 1) {
    throw new Error('Backup too short');
  }
  const salt = blob.slice(0, SALT_LEN);
  const nonce = blob.slice(SALT_LEN, SALT_LEN + NONCE_LEN);
  const ciphertext = blob.slice(SALT_LEN + NONCE_LEN);
  const s = await ensureSodium();
  const key = await deriveKey(passphrase, salt, s);
  const backend = getCryptoBackend();
  try {
    return backend.secretboxOpen(ciphertext, nonce, key);
  } catch {
    throw new Error('Wrong passphrase or corrupt backup');
  } finally {
    s.memzero(key);
  }
}

// Full-device backup. Wraps userId + deviceId + privateKey in a JSON payload
// before encrypting, so a restore flow can re-seed localStorage + vault + server
// device row without requiring the user to remember IDs.
export interface DeviceBackupPayload {
  v: 2;
  userId: string;
  deviceId: string;
  privateKeyB64: string;
}

export async function exportDeviceBackup(
  params: {
    userId: string;
    deviceId: string;
    privateKey: Uint8Array;
    passphrase: string;
  },
): Promise<string> {
  const payload: DeviceBackupPayload = {
    v: 2,
    userId: params.userId,
    deviceId: params.deviceId,
    privateKeyB64: b64url(params.privateKey),
  };
  const bytes = new TextEncoder().encode(JSON.stringify(payload));
  return exportDeviceKey(bytes, params.passphrase);
}

export async function importDeviceBackup(
  backup: string,
  passphrase: string,
): Promise<DeviceBackupPayload> {
  const plain = await importDeviceKey(backup, passphrase);
  const text = new TextDecoder().decode(plain);
  try {
    const obj = JSON.parse(text) as DeviceBackupPayload;
    if (obj && obj.v === 2 && obj.userId && obj.deviceId && obj.privateKeyB64) {
      return obj;
    }
  } catch {
    /* fall through */
  }
  throw new Error('Backup format not supported — v2 expected');
}

export function decodePrivateKeyFromBackup(payload: DeviceBackupPayload): Uint8Array {
  return unb64url(payload.privateKeyB64);
}

// ---------------------------------------------------------------------------
// Recovery code
// ---------------------------------------------------------------------------
//
// Generates a high-entropy code shown to the user once at backup time. The
// same payload is encrypted twice — once with the user's passphrase, once
// with the recovery code — so either string can decrypt the device key.
//
// The recovery code is 24 chars from a 32-symbol alphabet (no ambiguous
// characters), grouped as 4×6. ~120 bits of entropy.

const RECOVERY_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

export interface BackupBundle {
  passphraseBackup: string;
  recoveryBackup: string;
  recoveryCode: string;
}

export async function exportDeviceBackupWithRecovery(params: {
  userId: string;
  deviceId: string;
  privateKey: Uint8Array;
  passphrase: string;
}): Promise<BackupBundle> {
  const recoveryCode = await generateRecoveryCode();
  const [passphraseBackup, recoveryBackup] = await Promise.all([
    exportDeviceBackup(params),
    exportDeviceBackup({ ...params, passphrase: recoveryCode }),
  ]);
  return { passphraseBackup, recoveryBackup, recoveryCode };
}

async function generateRecoveryCode(): Promise<string> {
  const s = await ensureSodium();
  const raw = s.randombytes_buf(24);
  let out = '';
  for (let i = 0; i < raw.length; i++) {
    out += RECOVERY_ALPHABET[raw[i]! % RECOVERY_ALPHABET.length];
    if ((i + 1) % 6 === 0 && i !== raw.length - 1) out += '-';
  }
  return out;
}

// Normalizes a user-typed recovery code: strips dashes/spaces, uppercases,
// maps look-alike characters. Lets users enter the code with imperfect
// spacing without rejecting valid input.
export function normalizeRecoveryCode(input: string): string {
  return input
    .toUpperCase()
    .replace(/[\s-]/g, '')
    .split('')
    .filter((c) => RECOVERY_ALPHABET.includes(c))
    .join('');
}
