import { loadDevicePrivateKey } from '@chat-app/shared/auth';
import { type OwnDeviceCtx, shareConvKeyToDevice } from '@chat-app/shared/chat';
import { pgHexToBytes } from '@chat-app/shared/supabase';

import { devLocalSecretStore } from './secretStore';
import { supabase } from './supabase';

// Watches the `devices` table for new entries AND, on mount, scans every
// conversation we participate in for missing key bundles. Fills gaps by
// re-wrapping our active conv-key for the missing recipient devices.
//
// This fixes the "cannot decrypt" cliff for devices that registered while
// no other participant device was online to share the key with them.

export interface SyncCtx {
  myUserId: string;
  myDeviceId: string;
  priv: Uint8Array;
}

// db-types is stale for `conversation_keys`/`active_key_version`; bypass.
function rawFrom(table: string) {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return (supabase as unknown as { from: (t: string) => any }).from(table);
}

// Module-level flag — gap-fill runs once per (user, device) combo per
// process lifetime. Page reloads / route changes don't re-trigger it.
const backfilledKey = new Set<string>();

export function startConversationKeySync(
  ownUserId: string,
  ownDeviceId: string,
): () => void {
  // DISABLED: auto-share of conversation keys to newly-registered devices
  // is gone. Without it, account takeover (stolen password / new device
  // registered by attacker) no longer automatically grants history access
  // — an attacker would have a working device-key but no conv-key wraps.
  //
  // History access paths still supported:
  //   1. Backup-Restore — restores the OLD device-id + privkey, so the
  //      server-side wraps for that device-id are accessible as before.
  //   2. (Planned) Approval flow — existing device or conversation peer
  //      explicitly approves a new device, then conv-keys are wrapped
  //      for it. Until that ships, fresh-login-without-backup means old
  //      conversations stay encrypted.
  //
  // For NEW conversations: the key is generated at conv-creation time
  // and includes all current devices of all members, so a freshly-logged-
  // in device CAN still participate in newly-created conversations. It
  // just can't read the back-history of conversations it wasn't a member
  // of when those messages were sealed.
  //
  // We deliberately keep the helper functions below (syncAllExistingGaps,
  // wrapForOneDevice, …) intact so the upcoming approval flow can wire
  // them to user-driven triggers without rebuilding from scratch.
  void ownUserId;
  void ownDeviceId;
  void backfilledKey;
  void loadDevicePrivateKey;
  void devLocalSecretStore;
  void supabase;
  return () => {};
}

// Keep helpers alive across the auto-sync hibernation window so the
// upcoming approval flow can re-wire them. Without this no-op reference
// `tsc --noEmit` flags them as unused (TS6133).
//
// `wrapForOneDevice` and `syncOneConversationGaps` are exported below for
// the device-approval module — once the user explicitly approves a new
// device the approval flow re-uses these helpers to wrap conv-keys for
// that specific deviceId.
void (() => {
  void listMyConversationIds;
  void listConversationDevices;
  void listExistingKeyRecipients;
  void getActiveKeyVersion;
  void syncAllExistingGaps;
  void isExpectedShareFailure;
  void rawFrom;
});

async function listMyConversationIds(myUserId: string): Promise<string[]> {
  const { data, error } = await supabase
    .from('conversation_members')
    .select('conversation_id')
    .eq('user_id', myUserId)
    .eq('accepted', true);
  if (error) {
    console.warn('keySync: own-member lookup failed', error);
    return [];
  }
  return (data ?? []).map((r) => r.conversation_id as string);
}

async function listConversationDevices(
  conversationId: string,
): Promise<{ id: string; user_id: string; public_key: string }[]> {
  const { data: members, error: mErr } = await supabase
    .from('conversation_members')
    .select('user_id')
    .eq('conversation_id', conversationId)
    .eq('accepted', true);
  if (mErr) {
    console.warn('keySync: members lookup failed', mErr);
    return [];
  }
  const userIds = (members ?? []).map((m) => m.user_id as string);
  if (userIds.length === 0) return [];

  const { data: devices, error: dErr } = await supabase
    .from('devices')
    .select('id, user_id, public_key')
    .in('user_id', userIds);
  if (dErr) {
    console.warn('keySync: devices lookup failed', dErr);
    return [];
  }
  return (devices ?? []) as { id: string; user_id: string; public_key: string }[];
}

async function listExistingKeyRecipients(
  conversationId: string,
  keyVersion: number,
): Promise<Set<string>> {
  const { data, error } = await rawFrom('conversation_keys')
    .select('recipient_device_id')
    .eq('conversation_id', conversationId)
    .eq('key_version', keyVersion);
  if (error) {
    console.warn('keySync: existing keys lookup failed', error);
    return new Set();
  }
  return new Set((data ?? []).map((r: { recipient_device_id: string }) => r.recipient_device_id));
}

async function getActiveKeyVersion(conversationId: string): Promise<number> {
  const { data, error } = await rawFrom('conversations')
    .select('active_key_version')
    .eq('id', conversationId)
    .single();
  if (error) {
    console.warn('keySync: active key version lookup failed', error);
    return 1;
  }
  return (data as { active_key_version: number }).active_key_version;
}

async function syncAllExistingGaps(ctx: SyncCtx): Promise<void> {
  const convs = await listMyConversationIds(ctx.myUserId);
  for (const convId of convs) {
    try {
      await syncOneConversationGaps(ctx, convId);
    } catch (err: unknown) {
      console.warn('keySync: conv gap sync failed', { convId, err });
    }
  }
}

export async function syncOneConversationGaps(ctx: SyncCtx, convId: string): Promise<void> {
  const version = await getActiveKeyVersion(convId);
  const devices = await listConversationDevices(convId);
  if (devices.length === 0) return;

  const recipients = await listExistingKeyRecipients(convId, version);
  const ownCtx: OwnDeviceCtx = {
    userId: ctx.myUserId,
    deviceId: ctx.myDeviceId,
    privateKey: ctx.priv,
  };

  for (const dev of devices) {
    if (recipients.has(dev.id)) continue;
    // Skip our own device — we already have the bundle if we're capable of
    // sharing (or don't need it if we ourselves haven't been wrapped yet).
    if (dev.id === ctx.myDeviceId) continue;
    try {
      await shareConvKeyToDevice(supabase, convId, dev.id, pgHexToBytes(dev.public_key), ownCtx);
    } catch (err: unknown) {
      // Backfill is best-effort. Most common silent failures:
      //   - tryGetConvKey couldn't unwrap (another peer will fill the gap).
      //   - RLS rejects because the recipient's owner is a pending (not-yet
      //     accepted) DM member, or was removed from the conv.
      // Both are recoverable / expected, swallow without spam.
      if (isExpectedShareFailure(err)) continue;
      console.warn('keySync: shareConvKeyToDevice gap-fill failed', {
        convId,
        recipient: dev.id,
        err,
      });
    }
  }
}

function isExpectedShareFailure(err: unknown): boolean {
  if (!err || typeof err !== 'object') return false;
  const e = err as { message?: string; code?: string; details?: string; status?: number };
  const code = (e.code ?? '').toString();
  const status = e.status;
  const haystack = (e.message ?? '') + ' ' + (e.details ?? '');
  return (
    status === 403 ||
    code === '42501' || // postgres: insufficient_privilege (RLS)
    code === '23505' || // unique_violation
    haystack.includes('row-level security') ||
    haystack.includes('does not have it yet') ||
    haystack.includes('Forbidden')
  );
}

export async function wrapForOneDevice(
  ctx: SyncCtx,
  newDeviceId: string,
  newDeviceUserId: string,
  newDevicePubHex: string,
): Promise<void> {
  const myConvs = new Set(await listMyConversationIds(ctx.myUserId));
  const { data: peerMember, error: pErr } = await supabase
    .from('conversation_members')
    .select('conversation_id')
    .eq('user_id', newDeviceUserId);
  if (pErr) {
    console.warn('keySync: peer-member lookup failed', pErr);
    return;
  }
  const sharedConvs = (peerMember ?? [])
    .map((r) => r.conversation_id as string)
    .filter((id) => myConvs.has(id));
  if (sharedConvs.length === 0) return;

  const newPub = pgHexToBytes(newDevicePubHex);
  const ownCtx: OwnDeviceCtx = {
    userId: ctx.myUserId,
    deviceId: ctx.myDeviceId,
    privateKey: ctx.priv,
  };
  for (const convId of sharedConvs) {
    try {
      await shareConvKeyToDevice(supabase, convId, newDeviceId, newPub, ownCtx);
    } catch (err: unknown) {
      console.warn('keySync: shareConvKeyToDevice failed', { convId, err });
    }
  }
}
