import {
  decryptWithConvKey,
  encryptWithConvKey,
  generateConvKey,
  unwrapConvKey,
  wrapConvKeyForRecipient,
} from '../crypto/sessionKeys.js';
import { bytesToPgHex, pgHexToBytes } from '../supabase/bytea.js';
import type { AppSupabaseClient } from '../supabase/client.js';

// db-types in this monorepo is a static snapshot generated against the older
// schema. The new `conversation_keys` table + `active_key_version` column on
// `conversations` aren't in there yet. Until the codegen catches up we bypass
// the typed builder for those calls.
function rawFrom(client: AppSupabaseClient, table: string) {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return (client as unknown as { from: (t: string) => any }).from(table);
}

// Per-conversation symmetric key management. Replaces per-device envelopes
// with a single conv-key (32-byte XSalsa20-Poly1305) wrapped to each device's
// X25519 pubkey via crypto_box.

interface DeviceKey {
  deviceId: string;
  userId: string;
  publicKey: Uint8Array;
}

export interface OwnDeviceCtx {
  userId: string;
  deviceId: string;
  privateKey: Uint8Array;
}

export interface ConvKeyHandle {
  conversationId: string;
  keyVersion: number;
  key: Uint8Array;
}

// In-process cache to avoid re-fetching + re-unwrapping every send/decrypt.
const cache = new Map<string, ConvKeyHandle>();
const cacheKey = (convId: string, version: number) => convId + '@' + version;

export function clearConvKeyCache(): void {
  cache.clear();
}

async function listDeviceKeys(
  client: AppSupabaseClient,
  conversationId: string,
): Promise<DeviceKey[]> {
  const { data: members, error: mErr } = await client
    .from('conversation_members')
    .select('user_id, accepted')
    .eq('conversation_id', conversationId);
  if (mErr) throw mErr;
  const memberIds = (members ?? []).filter((m) => m.accepted).map((m) => m.user_id);
  if (memberIds.length === 0) return [];

  const { data: devices, error: dErr } = await client
    .from('devices')
    .select('id, user_id, public_key')
    .in('user_id', memberIds);
  if (dErr) throw dErr;

  return (devices ?? []).map((d) => ({
    deviceId: d.id,
    userId: d.user_id,
    publicKey: pgHexToBytes(d.public_key),
  }));
}

async function fetchActiveKeyVersion(
  client: AppSupabaseClient,
  conversationId: string,
): Promise<number> {
  const { data, error } = await rawFrom(client, 'conversations')
    .select('active_key_version')
    .eq('id', conversationId)
    .single();
  if (error) throw error;
  return (data as { active_key_version: number }).active_key_version;
}

interface SenderInfo {
  senderDeviceId: string;
  senderPublicKey: Uint8Array;
}

async function fetchKeyBundle(
  client: AppSupabaseClient,
  conversationId: string,
  ownDeviceId: string,
  keyVersion: number,
): Promise<{ encryptedKey: Uint8Array; nonce: Uint8Array; sender: SenderInfo } | null> {
  const { data, error } = await rawFrom(client, 'conversation_keys')
    .select('encrypted_key, nonce, sender_device_id')
    .eq('conversation_id', conversationId)
    .eq('recipient_device_id', ownDeviceId)
    .eq('key_version', keyVersion)
    .maybeSingle();
  if (error) throw error;
  if (!data) return null;

  const row = data as {
    encrypted_key: string;
    nonce: string;
    sender_device_id: string;
  };
  const { data: dev, error: dErr } = await client
    .from('devices')
    .select('id, public_key')
    .eq('id', row.sender_device_id)
    .single();
  if (dErr) throw dErr;

  return {
    encryptedKey: pgHexToBytes(row.encrypted_key),
    nonce: pgHexToBytes(row.nonce),
    sender: {
      senderDeviceId: dev.id,
      senderPublicKey: pgHexToBytes(dev.public_key),
    },
  };
}

// Strips the leading `\x` postgres bytea hex prefix so the RPC's
// `decode(text, 'hex')` accepts it.
function hexNoPrefix(bytes: Uint8Array): string {
  return bytesToPgHex(bytes).slice(2);
}

// Bootstraps a brand-new conv-key, wrapping it for every member device that
// currently exists (including the caller's own devices). Used the first time
// a conversation needs a key, or when rotation is requested. All inserts go
// through `share_conv_keys` (SECURITY DEFINER) — silently skips invalid
// recipients, no per-row 403 console spam.
export async function bootstrapConvKey(
  client: AppSupabaseClient,
  conversationId: string,
  own: OwnDeviceCtx,
  keyVersion: number,
): Promise<ConvKeyHandle> {
  const convKey = generateConvKey();
  const recipients = await listDeviceKeys(client, conversationId);
  if (recipients.length === 0) {
    throw new Error('cannot bootstrap conv key — no recipient devices');
  }

  const bundles: Array<{ recipient_device_id: string; encrypted_key: string; nonce: string }> = [];
  for (const r of recipients) {
    const wrapped = await wrapConvKeyForRecipient(convKey, r.publicKey, own.privateKey);
    bundles.push({
      recipient_device_id: r.deviceId,
      encrypted_key: hexNoPrefix(wrapped.ciphertext),
      nonce: hexNoPrefix(wrapped.nonce),
    });
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const rpc = (client as unknown as { rpc: (n: string, p: object) => Promise<{ error: any }> }).rpc;
  const { error } = await rpc.call(client, 'share_conv_keys', {
    p_conv_id: conversationId,
    p_sender_device_id: own.deviceId,
    p_key_version: keyVersion,
    p_bundles: bundles,
  });
  if (error) throw error;

  const handle = { conversationId, keyVersion, key: convKey };
  cache.set(cacheKey(conversationId, keyVersion), handle);
  return handle;
}

// Resolves the current conv-key for `conversationId`. Order:
//   1) cache hit
//   2) DB row for own device → unwrap
//   3) bootstrap a brand-new key (only valid path if NO existing keys exist
//      for any device — i.e. this is the conversation's very first message)
export async function getOrCreateConvKey(
  client: AppSupabaseClient,
  conversationId: string,
  own: OwnDeviceCtx,
): Promise<ConvKeyHandle> {
  const version = await fetchActiveKeyVersion(client, conversationId);
  const cached = cache.get(cacheKey(conversationId, version));
  if (cached) return cached;

  const bundle = await fetchKeyBundle(client, conversationId, own.deviceId, version);
  if (bundle) {
    const key = await unwrapConvKey(
      bundle.encryptedKey,
      bundle.nonce,
      bundle.sender.senderPublicKey,
      own.privateKey,
    );
    const handle = { conversationId, keyVersion: version, key };
    cache.set(cacheKey(conversationId, version), handle);
    return handle;
  }

  // No bundle yet for THIS device. Two cases:
  //   - I'm the first ever sender → bootstrap.
  //   - Conversation already has keys but my device wasn't included yet → I
  //     have to wait until an existing device wraps the key for me.
  const { count, error: cntErr } = await rawFrom(client, 'conversation_keys')
    .select('recipient_device_id', { count: 'exact', head: true })
    .eq('conversation_id', conversationId)
    .eq('key_version', version);
  if (cntErr) throw cntErr;

  if ((count ?? 0) > 0) {
    throw new Error(
      'Awaiting conversation key — another device must share it with this device.',
    );
  }
  return bootstrapConvKey(client, conversationId, own, version);
}

// Read-only variant: never bootstraps. Returns null if no key bundle exists
// for this device yet.
export async function tryGetConvKey(
  client: AppSupabaseClient,
  conversationId: string,
  ownDeviceId: string,
  ownPrivateKey: Uint8Array,
  keyVersion: number,
): Promise<ConvKeyHandle | null> {
  const cached = cache.get(cacheKey(conversationId, keyVersion));
  if (cached) return cached;

  const bundle = await fetchKeyBundle(client, conversationId, ownDeviceId, keyVersion);
  if (!bundle) return null;
  const key = await unwrapConvKey(
    bundle.encryptedKey,
    bundle.nonce,
    bundle.sender.senderPublicKey,
    ownPrivateKey,
  );
  const handle = { conversationId, keyVersion, key };
  cache.set(cacheKey(conversationId, keyVersion), handle);
  return handle;
}

// Wraps the active conv-key for a single new device (e.g. when a peer
// registers a new device). The caller's device must have an unwrapped copy
// of the conv-key in cache (or be able to fetch it).
export async function shareConvKeyToDevice(
  client: AppSupabaseClient,
  conversationId: string,
  recipientDeviceId: string,
  recipientPublicKey: Uint8Array,
  own: OwnDeviceCtx,
): Promise<void> {
  const version = await fetchActiveKeyVersion(client, conversationId);
  const handle =
    cache.get(cacheKey(conversationId, version)) ??
    (await tryGetConvKey(client, conversationId, own.deviceId, own.privateKey, version));
  if (!handle) {
    throw new Error('cannot share conv key — own device does not have it yet');
  }

  const wrapped = await wrapConvKeyForRecipient(
    handle.key,
    recipientPublicKey,
    own.privateKey,
  );
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const rpc = (client as unknown as { rpc: (n: string, p: object) => Promise<{ error: any }> }).rpc;
  const { error } = await rpc.call(client, 'share_conv_keys', {
    p_conv_id: conversationId,
    p_sender_device_id: own.deviceId,
    p_key_version: version,
    p_bundles: [
      {
        recipient_device_id: recipientDeviceId,
        encrypted_key: hexNoPrefix(wrapped.ciphertext),
        nonce: hexNoPrefix(wrapped.nonce),
      },
    ],
  });
  if (error) throw error;
}

// Re-exports for convenience.
export { decryptWithConvKey, encryptWithConvKey };
