import type { SecretStore } from '@chat-app/shared/auth';
import { exists, mkdir, readFile, rename, writeFile } from '@tauri-apps/plugin-fs';
import { appLocalDataDir } from '@tauri-apps/api/path';
import sodium from 'libsodium-wrappers';

// Encrypted single-file SecretStore for Tauri. Replaces the flaky
// tauri-plugin-stronghold implementation.
//
// File layout (binary, little-endian):
//   bytes  0..7   magic: ASCII "CHATVLT1"
//   bytes  8..23  salt for KDF (16 bytes)
//   bytes 24..47  XSalsa20-Poly1305 nonce (24 bytes)
//   bytes 48..    secretbox(plaintext_json, key, nonce)
//
// `plaintext_json` is a UTF-8 JSON object  { [key: string]: base64url(value) }.
//
// Key derivation: Argon2id (libsodium MODERATE ops/mem) over a passphrase
// derived from the authenticated user-id + a constant. Same userId on the
// same machine after re-install ⇒ same key ⇒ vault recovers automatically.
//
// Atomic writes: serialised vault is first written to `<file>.tmp` then
// renamed onto `<file>` so an interrupted write never corrupts the existing
// vault.

// Per-user vault filename so multiple accounts on the same machine each get
// their own file (and Argon2 derives a different key per user, so cross-user
// decrypt is also blocked even if filenames collided).
async function vaultFileName(userId: string): Promise<string> {
  const enc = new TextEncoder();
  const buf = await crypto.subtle.digest('SHA-256', enc.encode('chatapp-vault-name:' + userId));
  const hex = Array.from(new Uint8Array(buf))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
  return 'chatapp-vault-' + hex.slice(0, 16) + '.bin';
}
const MAGIC = new TextEncoder().encode('CHATVLT1'); // 8 bytes
const SALT_LEN = 16;
const NONCE_LEN = 24;
const KEY_LEN = 32;

interface VaultState {
  path: string;
  tmpPath: string;
  key: Uint8Array; // derived encryption key
  data: Map<string, Uint8Array>;
}

let initPromise: Promise<VaultState> | null = null;
let vault: VaultState | null = null;
let initializedFor: string | null = null;

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

function joinPath(dir: string, name: string): string {
  const sep = dir.endsWith('/') || dir.endsWith('\\') ? '' : '/';
  return dir + sep + name;
}

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(userId: string, salt: Uint8Array, s: typeof sodium): Promise<Uint8Array> {
  const passphrase = 'chatapp-vault-v1:' + userId;
  return s.crypto_pwhash(
    KEY_LEN,
    passphrase,
    salt,
    s.crypto_pwhash_OPSLIMIT_MODERATE,
    s.crypto_pwhash_MEMLIMIT_MODERATE,
    s.crypto_pwhash_ALG_ARGON2ID13,
  );
}

async function loadOrCreateVault(userId: string): Promise<VaultState> {
  const s = await ensureSodium();
  const dir = await appLocalDataDir();
  const fileName = await vaultFileName(userId);
  const path = joinPath(dir, fileName);
  const tmpPath = path + '.tmp';

  try {
    await mkdir(dir, { recursive: true });
  } catch {
    /* parent likely already exists */
  }

  const fileExists = await exists(path).catch(() => false);
  if (!fileExists) {
    const salt = s.randombytes_buf(SALT_LEN);
    const key = await deriveKey(userId, salt, s);
    const state: VaultState = { path, tmpPath, key, data: new Map() };
    await persist(state, salt, s);
    return state;
  }

  const raw = await readFile(path);
  if (raw.length < MAGIC.length + SALT_LEN + NONCE_LEN + 1) {
    throw new Error('vault file too short');
  }
  for (let i = 0; i < MAGIC.length; i++) {
    if (raw[i] !== MAGIC[i]) throw new Error('vault magic mismatch');
  }
  const salt = raw.slice(MAGIC.length, MAGIC.length + SALT_LEN);
  const nonce = raw.slice(MAGIC.length + SALT_LEN, MAGIC.length + SALT_LEN + NONCE_LEN);
  const ciphertext = raw.slice(MAGIC.length + SALT_LEN + NONCE_LEN);

  const key = await deriveKey(userId, salt, s);
  let plain: Uint8Array;
  try {
    plain = s.crypto_secretbox_open_easy(ciphertext, nonce, key);
  } catch (err: unknown) {
    throw new Error(
      'vault decrypt failed (wrong user / corrupted file): ' +
        (err instanceof Error ? err.message : String(err)),
    );
  }
  const json = new TextDecoder().decode(plain) || '{}';
  const obj = JSON.parse(json) as Record<string, string>;
  const data = new Map<string, Uint8Array>();
  for (const [k, v] of Object.entries(obj)) {
    try {
      data.set(k, unb64url(v));
    } catch {
      /* skip malformed entries */
    }
  }
  return { path, tmpPath, key, data };
}

async function persist(state: VaultState, salt: Uint8Array, s: typeof sodium): Promise<void> {
  const obj: Record<string, string> = {};
  for (const [k, v] of state.data) obj[k] = b64url(v);
  const plain = new TextEncoder().encode(JSON.stringify(obj));
  const nonce = s.randombytes_buf(NONCE_LEN);
  const ciphertext = s.crypto_secretbox_easy(plain, nonce, state.key);

  const out = new Uint8Array(MAGIC.length + SALT_LEN + NONCE_LEN + ciphertext.length);
  out.set(MAGIC, 0);
  out.set(salt, MAGIC.length);
  out.set(nonce, MAGIC.length + SALT_LEN);
  out.set(ciphertext, MAGIC.length + SALT_LEN + NONCE_LEN);

  // Atomic write: tmp → rename. `rename` on the same filesystem is atomic
  // on macOS, Linux, and Windows (NTFS).
  await writeFile(state.tmpPath, out);
  await rename(state.tmpPath, state.path);
}

// Re-derives the salt by reading the existing file header so persist() can
// keep using the same KDF salt across writes (we don't rotate KDF on every
// save — only on initial vault creation).
async function readSalt(state: VaultState): Promise<Uint8Array> {
  const raw = await readFile(state.path);
  return raw.slice(MAGIC.length, MAGIC.length + SALT_LEN);
}

async function ensureInit(userId: string): Promise<VaultState> {
  if (initializedFor === userId && vault) return vault;
  if (initPromise) return initPromise;
  initPromise = loadOrCreateVault(userId)
    .then((v) => {
      vault = v;
      initializedFor = userId;
      return v;
    })
    .finally(() => {
      initPromise = null;
    });
  return initPromise;
}

export function makeSecureFileStore(userId: string): SecretStore {
  return {
    async getSecret(key: string): Promise<Uint8Array | null> {
      const v = await ensureInit(userId);
      const found = v.data.get(key);
      return found ? new Uint8Array(found) : null;
    },
    async setSecret(key: string, value: Uint8Array): Promise<void> {
      const v = await ensureInit(userId);
      v.data.set(key, new Uint8Array(value));
      const s = await ensureSodium();
      const salt = await readSalt(v);
      await persist(v, salt, s);
    },
    async removeSecret(key: string): Promise<void> {
      const v = await ensureInit(userId);
      v.data.delete(key);
      const s = await ensureSodium();
      const salt = await readSalt(v);
      await persist(v, salt, s);
    },
  };
}

// Migrates legacy localStorage entries (chatapp.secret:*) into the encrypted
// vault on first init. Idempotent — checks for marker key.
export async function migrateLocalStorageToVault(
  userId: string,
  prefix: string,
): Promise<void> {
  const v = await ensureInit(userId);
  if (v.data.has('__migrated_from_localstorage')) return;

  for (let i = 0; i < window.localStorage.length; i++) {
    const fullKey = window.localStorage.key(i);
    if (!fullKey || !fullKey.startsWith(prefix)) continue;
    const raw = window.localStorage.getItem(fullKey);
    if (!raw) continue;
    try {
      const decoded = Uint8Array.from(atob(raw), (c) => c.charCodeAt(0));
      const shortKey = fullKey.slice(prefix.length);
      v.data.set(shortKey, decoded);
    } catch {
      /* skip malformed */
    }
  }
  v.data.set('__migrated_from_localstorage', new Uint8Array([1]));
  const s = await ensureSodium();
  const salt = await readSalt(v);
  await persist(v, salt, s);
}
