/**
 * EncryptID Database Layer — PostgreSQL
 *
 * Replaces in-memory Maps with persistent PostgreSQL storage.
 * Uses the 'postgres' npm package (lightweight, no native deps with Bun).
 */

import postgres from 'postgres';
import { readFileSync } from 'fs';
import { join } from 'path';
import { encryptField, decryptField, hashForLookup } from './server-crypto';

// ============================================================================
// CONNECTION
// ============================================================================

const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  throw new Error('DATABASE_URL environment variable is required');
}

const sql = postgres(DATABASE_URL, {
  max: 10,
  idle_timeout: 20,
  connect_timeout: 10,
});

// ============================================================================
// TYPES
// ============================================================================

export interface StoredCredential {
  credentialId: string;
  publicKey: string;
  userId: string;
  username: string;
  counter: number;
  createdAt: number;
  lastUsed?: number;
  transports?: string[];
  rpId?: string;
  label?: string;
}

export interface StoredChallenge {
  challenge: string;
  userId?: string;
  type: 'registration' | 'authentication' | 'device_registration' | 'wallet_link' | 'legacy_migration';
  createdAt: number;
  expiresAt: number;
}

// ============================================================================
// INITIALIZATION
// ============================================================================

export async function initDatabase(): Promise<void> {
  const schema = readFileSync(join(import.meta.dir, 'schema.sql'), 'utf-8');
  await sql.unsafe(schema);
  console.log('EncryptID: Database initialized');

  // Clean expired challenges on startup
  await cleanExpiredChallenges();
}

// ============================================================================
// USER OPERATIONS
// ============================================================================

export async function createUser(id: string, username: string, displayName?: string, did?: string): Promise<void> {
  await sql`
    INSERT INTO users (id, username, display_name, did)
    VALUES (${id}, ${username}, ${displayName || username}, ${did || null})
    ON CONFLICT (id) DO NOTHING
  `;
}

export async function getUserByUsername(username: string) {
  const [user] = await sql`SELECT * FROM users WHERE username = ${username}`;
  return user || null;
}

// ============================================================================
// CREDENTIAL OPERATIONS
// ============================================================================

export async function storeCredential(cred: StoredCredential, did?: string, label?: string): Promise<void> {
  // Ensure user exists first (with display name + DID so they're never NULL)
  // If a proper DID is provided (e.g. from PRF key derivation), use it; otherwise omit
  await createUser(cred.userId, cred.username, cred.username, did || undefined);

  await sql`
    INSERT INTO credentials (credential_id, user_id, public_key, counter, transports, created_at, rp_id, label)
    VALUES (
      ${cred.credentialId},
      ${cred.userId},
      ${cred.publicKey},
      ${cred.counter},
      ${cred.transports || null},
      ${new Date(cred.createdAt)},
      ${cred.rpId || 'rspace.online'},
      ${label || null}
    )
  `;
}

export async function getCredential(credentialId: string): Promise<StoredCredential | null> {
  const rows = await sql`
    SELECT c.credential_id, c.public_key, c.user_id, c.counter,
           c.transports, c.created_at, c.last_used, u.username
    FROM credentials c
    JOIN users u ON c.user_id = u.id
    WHERE c.credential_id = ${credentialId}
  `;
  if (rows.length === 0) return null;

  const row = rows[0];
  return {
    credentialId: row.credential_id,
    publicKey: row.public_key,
    userId: row.user_id,
    username: row.username,
    counter: row.counter,
    createdAt: new Date(row.created_at).getTime(),
    lastUsed: row.last_used ? new Date(row.last_used).getTime() : undefined,
    transports: row.transports,
  };
}

export async function updateCredentialUsage(credentialId: string, newCounter: number): Promise<void> {
  await sql`
    UPDATE credentials
    SET counter = ${newCounter}, last_used = NOW()
    WHERE credential_id = ${credentialId}
  `;
}

export async function getUserCredentials(userId: string): Promise<StoredCredential[]> {
  const rows = await sql`
    SELECT c.credential_id, c.public_key, c.user_id, c.counter,
           c.transports, c.created_at, c.last_used, c.label, u.username
    FROM credentials c
    JOIN users u ON c.user_id = u.id
    WHERE c.user_id = ${userId}
  `;

  return rows.map(row => ({
    credentialId: row.credential_id,
    publicKey: row.public_key,
    userId: row.user_id,
    username: row.username,
    counter: row.counter,
    createdAt: new Date(row.created_at).getTime(),
    lastUsed: row.last_used ? new Date(row.last_used).getTime() : undefined,
    transports: row.transports,
    label: row.label || undefined,
  }));
}

export async function updateCredentialLabel(credentialId: string, userId: string, label: string): Promise<boolean> {
  const result = await sql`
    UPDATE credentials SET label = ${label}
    WHERE credential_id = ${credentialId} AND user_id = ${userId}
    RETURNING credential_id
  `;
  return result.length > 0;
}

export async function deleteCredential(credentialId: string, userId: string): Promise<boolean> {
  const result = await sql`
    DELETE FROM credentials
    WHERE credential_id = ${credentialId} AND user_id = ${userId}
    RETURNING credential_id
  `;
  return result.length > 0;
}

// ============================================================================
// CHALLENGE OPERATIONS
// ============================================================================

export async function storeChallenge(ch: StoredChallenge): Promise<void> {
  await sql`
    INSERT INTO challenges (challenge, user_id, type, created_at, expires_at)
    VALUES (
      ${ch.challenge},
      ${ch.userId || null},
      ${ch.type},
      ${new Date(ch.createdAt)},
      ${new Date(ch.expiresAt)}
    )
  `;
}

export async function getChallenge(challenge: string): Promise<StoredChallenge | null> {
  const rows = await sql`
    SELECT * FROM challenges WHERE challenge = ${challenge}
  `;
  if (rows.length === 0) return null;

  const row = rows[0];
  return {
    challenge: row.challenge,
    userId: row.user_id || undefined,
    type: row.type as StoredChallenge['type'],
    createdAt: new Date(row.created_at).getTime(),
    expiresAt: new Date(row.expires_at).getTime(),
  };
}

export async function deleteChallenge(challenge: string): Promise<void> {
  await sql`DELETE FROM challenges WHERE challenge = ${challenge}`;
}

export async function consumeChallenge(
  challenge: string,
  userId: string,
  expectedType: StoredChallenge['type'],
): Promise<StoredChallenge | null> {
  const rows = await sql`
    DELETE FROM challenges
    WHERE challenge = ${challenge}
      AND user_id = ${userId}
      AND type = ${expectedType}
      AND expires_at > NOW()
    RETURNING *
  `;
  if (rows.length === 0) return null;
  const row = rows[0];
  return {
    challenge: row.challenge,
    userId: row.user_id || undefined,
    type: row.type as StoredChallenge['type'],
    createdAt: new Date(row.created_at).getTime(),
    expiresAt: new Date(row.expires_at).getTime(),
  };
}

export async function cleanExpiredChallenges(): Promise<number> {
  const result = await sql`DELETE FROM challenges WHERE expires_at < NOW()`;
  return result.count;
}

// ============================================================================
// USER EMAIL OPERATIONS
// ============================================================================

export async function setUserEmail(userId: string, email: string): Promise<void> {
  const [emailEnc, profileEmailEnc, emailHash] = await Promise.all([
    encryptField(email),
    encryptField(email),
    hashForLookup(email),
  ]);
  await sql`UPDATE users SET email = ${email}, profile_email = ${email},
    email_enc = ${emailEnc}, profile_email_enc = ${profileEmailEnc}, email_hash = ${emailHash},
    updated_at = NOW() WHERE id = ${userId}`;
}

export async function getUserByEmail(email: string) {
  const hash = await hashForLookup(email);
  // Try hash lookup first, fall back to plaintext for pre-migration rows
  const [user] = await sql`SELECT * FROM users WHERE email_hash = ${hash}`;
  if (user) return user;
  const [legacy] = await sql`SELECT * FROM users WHERE email = ${email} AND email_hash IS NULL`;
  return legacy || null;
}

export async function getUserById(userId: string) {
  const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`;
  return user || null;
}

/** Record a global logout — all JWTs issued before this timestamp are revoked */
export async function setUserLoggedOutAt(userId: string): Promise<void> {
  await sql`UPDATE users SET logged_out_at = NOW() WHERE id = ${userId}`;
}

/** Get the timestamp of the user's last global logout (null if never) */
export async function getUserLoggedOutAt(userId: string): Promise<Date | null> {
  const [row] = await sql`SELECT logged_out_at FROM users WHERE id = ${userId}`;
  return row?.logged_out_at ? new Date(row.logged_out_at) : null;
}

/** Update a user's DID (e.g. upgrading from truncated to proper did:key:z6Mk...) */
export async function updateUserDid(userId: string, newDid: string): Promise<void> {
  await sql`UPDATE users SET did = ${newDid}, updated_at = NOW() WHERE id = ${userId}`;
}

/** Update all space memberships from one DID to another */
export async function migrateSpaceMemberDid(oldDid: string, newDid: string): Promise<number> {
  const result = await sql`UPDATE space_members SET user_did = ${newDid} WHERE user_did = ${oldDid}`;
  return result.count;
}

// ============================================================================
// PASSPHRASE SALT OPERATIONS
// ============================================================================

/** Store a passphrase salt for a user (non-PRF browsers like Firefox) */
export async function setPassphraseSalt(userId: string, salt: string): Promise<void> {
  await sql`UPDATE users SET passphrase_salt = ${salt}, updated_at = NOW() WHERE id = ${userId}`;
}

/** Get the passphrase salt for a user (null if user has PRF support) */
export async function getPassphraseSalt(userId: string): Promise<string | null> {
  const [row] = await sql`SELECT passphrase_salt FROM users WHERE id = ${userId}`;
  return row?.passphrase_salt || null;
}

// ============================================================================
// RECOVERY TOKEN OPERATIONS
// ============================================================================

export interface StoredRecoveryToken {
  token: string;
  userId: string;
  type: 'email_verify' | 'account_recovery' | 'magic_login';
  createdAt: number;
  expiresAt: number;
  used: boolean;
}

export async function storeRecoveryToken(rt: StoredRecoveryToken): Promise<void> {
  await sql`
    INSERT INTO recovery_tokens (token, user_id, type, created_at, expires_at, used)
    VALUES (
      ${rt.token},
      ${rt.userId},
      ${rt.type},
      ${new Date(rt.createdAt)},
      ${new Date(rt.expiresAt)},
      ${rt.used}
    )
  `;
}

export async function getRecoveryToken(token: string): Promise<StoredRecoveryToken | null> {
  const rows = await sql`SELECT * FROM recovery_tokens WHERE token = ${token}`;
  if (rows.length === 0) return null;
  const row = rows[0];
  return {
    token: row.token,
    userId: row.user_id,
    type: row.type as 'email_verify' | 'account_recovery' | 'magic_login',
    createdAt: new Date(row.created_at).getTime(),
    expiresAt: new Date(row.expires_at).getTime(),
    used: row.used,
  };
}

export async function markRecoveryTokenUsed(token: string): Promise<void> {
  await sql`UPDATE recovery_tokens SET used = TRUE WHERE token = ${token}`;
}

export async function cleanExpiredRecoveryTokens(): Promise<number> {
  const result = await sql`DELETE FROM recovery_tokens WHERE expires_at < NOW()`;
  return result.count;
}

// ============================================================================
// SPACE MEMBERSHIP
// ============================================================================

export interface StoredSpaceMember {
  spaceSlug: string;
  userDID: string;
  role: string;
  joinedAt: number;
  grantedBy?: string;
}

export async function listSpacesForUser(userDID: string): Promise<StoredSpaceMember[]> {
  const rows = await sql`
    SELECT * FROM space_members
    WHERE user_did = ${userDID}
    ORDER BY joined_at DESC
  `;
  return rows.map((row) => ({
    spaceSlug: row.space_slug,
    userDID: row.user_did,
    role: row.role,
    joinedAt: new Date(row.joined_at).getTime(),
    grantedBy: row.granted_by || undefined,
  }));
}

export async function getSpaceMember(
  spaceSlug: string,
  userDID: string,
): Promise<StoredSpaceMember | null> {
  const rows = await sql`
    SELECT * FROM space_members
    WHERE space_slug = ${spaceSlug} AND user_did = ${userDID}
  `;
  if (rows.length === 0) return null;
  const row = rows[0];
  return {
    spaceSlug: row.space_slug,
    userDID: row.user_did,
    role: row.role,
    joinedAt: new Date(row.joined_at).getTime(),
    grantedBy: row.granted_by || undefined,
  };
}

export async function listSpaceMembers(
  spaceSlug: string,
): Promise<StoredSpaceMember[]> {
  const rows = await sql`
    SELECT * FROM space_members
    WHERE space_slug = ${spaceSlug}
    ORDER BY joined_at ASC
  `;
  return rows.map((row) => ({
    spaceSlug: row.space_slug,
    userDID: row.user_did,
    role: row.role,
    joinedAt: new Date(row.joined_at).getTime(),
    grantedBy: row.granted_by || undefined,
  }));
}

export async function upsertSpaceMember(
  spaceSlug: string,
  userDID: string,
  role: string,
  grantedBy?: string,
): Promise<StoredSpaceMember> {
  const rows = await sql`
    INSERT INTO space_members (space_slug, user_did, role, granted_by)
    VALUES (${spaceSlug}, ${userDID}, ${role}, ${grantedBy ?? null})
    ON CONFLICT (space_slug, user_did)
    DO UPDATE SET role = ${role}, granted_by = ${grantedBy ?? null}
    RETURNING *
  `;
  const row = rows[0];
  return {
    spaceSlug: row.space_slug,
    userDID: row.user_did,
    role: row.role,
    joinedAt: new Date(row.joined_at).getTime(),
    grantedBy: row.granted_by || undefined,
  };
}

export async function removeSpaceMember(
  spaceSlug: string,
  userDID: string,
): Promise<boolean> {
  const result = await sql`
    DELETE FROM space_members
    WHERE space_slug = ${spaceSlug} AND user_did = ${userDID}
  `;
  return result.count > 0;
}

// ============================================================================
// GUARDIAN OPERATIONS
// ============================================================================

export interface StoredGuardian {
  id: string;
  userId: string;
  name: string;
  email: string | null;
  guardianUserId: string | null;
  status: 'pending' | 'accepted' | 'revoked';
  inviteToken: string | null;
  inviteExpiresAt: number | null;
  acceptedAt: number | null;
  createdAt: number;
}

async function rowToGuardian(row: any): Promise<StoredGuardian> {
  const [nameDecrypted, emailDecrypted] = await Promise.all([
    decryptField(row.name_enc),
    decryptField(row.email_enc),
  ]);
  return {
    id: row.id,
    userId: row.user_id,
    name: nameDecrypted ?? row.name,
    email: emailDecrypted ?? row.email ?? null,
    guardianUserId: row.guardian_user_id || null,
    status: row.status,
    inviteToken: row.invite_token || null,
    inviteExpiresAt: row.invite_expires_at ? new Date(row.invite_expires_at).getTime() : null,
    acceptedAt: row.accepted_at ? new Date(row.accepted_at).getTime() : null,
    createdAt: new Date(row.created_at).getTime(),
  };
}

export async function addGuardian(
  id: string,
  userId: string,
  name: string,
  email: string | null,
  inviteToken: string,
  inviteExpiresAt: number,
): Promise<StoredGuardian> {
  const [nameEnc, emailEnc] = await Promise.all([encryptField(name), encryptField(email)]);
  const rows = await sql`
    INSERT INTO guardians (id, user_id, name, email, name_enc, email_enc, invite_token, invite_expires_at)
    VALUES (${id}, ${userId}, ${name}, ${email}, ${nameEnc}, ${emailEnc}, ${inviteToken}, ${new Date(inviteExpiresAt)})
    RETURNING *
  `;
  return rowToGuardian(rows[0]);
}

export async function getGuardians(userId: string): Promise<StoredGuardian[]> {
  const rows = await sql`
    SELECT * FROM guardians
    WHERE user_id = ${userId} AND status != 'revoked'
    ORDER BY created_at ASC
  `;
  return Promise.all(rows.map(rowToGuardian));
}

export async function getGuardianByInviteToken(token: string): Promise<StoredGuardian | null> {
  const rows = await sql`SELECT * FROM guardians WHERE invite_token = ${token}`;
  if (rows.length === 0) return null;
  return await rowToGuardian(rows[0]);
}

export async function acceptGuardianInvite(guardianId: string, guardianUserId: string): Promise<void> {
  await sql`
    UPDATE guardians
    SET status = 'accepted', guardian_user_id = ${guardianUserId}, accepted_at = NOW(), invite_token = NULL
    WHERE id = ${guardianId}
  `;
}

export async function removeGuardian(guardianId: string, userId: string): Promise<boolean> {
  const result = await sql`
    UPDATE guardians SET status = 'revoked'
    WHERE id = ${guardianId} AND user_id = ${userId}
  `;
  return result.count > 0;
}

export async function getGuardianById(guardianId: string): Promise<StoredGuardian | null> {
  const rows = await sql`SELECT * FROM guardians WHERE id = ${guardianId}`;
  if (rows.length === 0) return null;
  return await rowToGuardian(rows[0]);
}

export async function getGuardianships(guardianUserId: string): Promise<StoredGuardian[]> {
  const rows = await sql`
    SELECT * FROM guardians
    WHERE guardian_user_id = ${guardianUserId} AND status = 'accepted'
    ORDER BY created_at ASC
  `;
  return Promise.all(rows.map(rowToGuardian));
}

// ============================================================================
// RECOVERY REQUEST OPERATIONS
// ============================================================================

export interface StoredRecoveryRequest {
  id: string;
  userId: string;
  status: string;
  threshold: number;
  approvalCount: number;
  initiatedAt: number;
  expiresAt: number;
  completedAt: number | null;
  isDrill: boolean;
}

function rowToRecoveryRequest(row: any): StoredRecoveryRequest {
  return {
    id: row.id,
    userId: row.user_id,
    status: row.status,
    threshold: row.threshold,
    approvalCount: row.approval_count,
    initiatedAt: new Date(row.initiated_at).getTime(),
    expiresAt: new Date(row.expires_at).getTime(),
    completedAt: row.completed_at ? new Date(row.completed_at).getTime() : null,
    isDrill: row.is_drill ?? false,
  };
}

export async function createRecoveryRequest(
  id: string,
  userId: string,
  threshold: number,
  expiresAt: number,
): Promise<StoredRecoveryRequest> {
  const rows = await sql`
    INSERT INTO recovery_requests (id, user_id, threshold, expires_at)
    VALUES (${id}, ${userId}, ${threshold}, ${new Date(expiresAt)})
    RETURNING *
  `;
  return rowToRecoveryRequest(rows[0]);
}

export async function getRecoveryRequest(requestId: string): Promise<StoredRecoveryRequest | null> {
  const rows = await sql`SELECT * FROM recovery_requests WHERE id = ${requestId}`;
  if (rows.length === 0) return null;
  return rowToRecoveryRequest(rows[0]);
}

export async function getActiveRecoveryRequest(userId: string): Promise<StoredRecoveryRequest | null> {
  const rows = await sql`
    SELECT * FROM recovery_requests
    WHERE user_id = ${userId} AND status IN ('pending', 'approved') AND expires_at > NOW()
    ORDER BY initiated_at DESC LIMIT 1
  `;
  if (rows.length === 0) return null;
  return rowToRecoveryRequest(rows[0]);
}

export async function createRecoveryApproval(
  requestId: string,
  guardianId: string,
  approvalToken: string,
): Promise<void> {
  await sql`
    INSERT INTO recovery_approvals (request_id, guardian_id, approval_token)
    VALUES (${requestId}, ${guardianId}, ${approvalToken})
    ON CONFLICT DO NOTHING
  `;
}

export async function approveRecoveryByToken(approvalToken: string): Promise<{ requestId: string; guardianId: string } | null> {
  // Find the approval
  const rows = await sql`
    SELECT * FROM recovery_approvals WHERE approval_token = ${approvalToken} AND approved_at IS NULL
  `;
  if (rows.length === 0) return null;

  const row = rows[0];

  // Mark approved
  await sql`
    UPDATE recovery_approvals SET approved_at = NOW(), approval_token = NULL
    WHERE request_id = ${row.request_id} AND guardian_id = ${row.guardian_id}
  `;

  // Increment approval count on request
  await sql`
    UPDATE recovery_requests SET approval_count = approval_count + 1
    WHERE id = ${row.request_id}
  `;

  return { requestId: row.request_id, guardianId: row.guardian_id };
}

export async function updateRecoveryRequestStatus(requestId: string, status: string): Promise<void> {
  const completedAt = status === 'completed' ? sql`NOW()` : null;
  await sql`
    UPDATE recovery_requests SET status = ${status}, completed_at = ${completedAt}
    WHERE id = ${requestId}
  `;
}

export async function createDrillRequest(
  id: string,
  userId: string,
  threshold: number,
  expiresAt: number,
): Promise<StoredRecoveryRequest> {
  const rows = await sql`
    INSERT INTO recovery_requests (id, user_id, threshold, expires_at, is_drill)
    VALUES (${id}, ${userId}, ${threshold}, ${new Date(expiresAt)}, TRUE)
    RETURNING *
  `;
  return rowToRecoveryRequest(rows[0]);
}

export async function setUserLastDrillAt(userId: string): Promise<void> {
  await sql`UPDATE users SET last_drill_at = NOW() WHERE id = ${userId}`;
}

export async function getUserLastDrillAt(userId: string): Promise<number | null> {
  const rows = await sql`SELECT last_drill_at FROM users WHERE id = ${userId}`;
  if (!rows.length || !rows[0].last_drill_at) return null;
  return new Date(rows[0].last_drill_at).getTime();
}

export async function getRecoveryApprovals(requestId: string): Promise<Array<{ guardianId: string; approvedAt: number | null }>> {
  const rows = await sql`
    SELECT guardian_id, approved_at FROM recovery_approvals WHERE request_id = ${requestId}
  `;
  return rows.map(r => ({
    guardianId: r.guardian_id,
    approvedAt: r.approved_at ? new Date(r.approved_at).getTime() : null,
  }));
}

// ============================================================================
// DEVICE LINK OPERATIONS
// ============================================================================

export async function createDeviceLink(token: string, userId: string, expiresAt: number): Promise<void> {
  await sql`
    INSERT INTO device_links (token, user_id, expires_at)
    VALUES (${token}, ${userId}, ${new Date(expiresAt)})
  `;
}

export async function getDeviceLink(token: string): Promise<{ userId: string; expiresAt: number; used: boolean } | null> {
  const rows = await sql`SELECT * FROM device_links WHERE token = ${token}`;
  if (rows.length === 0) return null;
  return {
    userId: rows[0].user_id,
    expiresAt: new Date(rows[0].expires_at).getTime(),
    used: rows[0].used,
  };
}

export async function markDeviceLinkUsed(token: string): Promise<void> {
  await sql`UPDATE device_links SET used = TRUE WHERE token = ${token}`;
}

// ============================================================================
// USER PROFILE OPERATIONS
// ============================================================================

export interface StoredUserProfile {
  userId: string;
  username: string;
  displayName: string | null;
  bio: string | null;
  avatarUrl: string | null;
  profileEmail: string | null;
  profileEmailIsRecovery: boolean;
  did: string | null;
  walletAddress: string | null;
  emailForwardEnabled: boolean;
  emailForwardMailcowId: string | null;
  createdAt: string;
  updatedAt: string;
}

async function rowToProfile(row: any): Promise<StoredUserProfile> {
  const [bioDecrypted, avatarUrlDecrypted, profileEmailDecrypted, walletDecrypted] = await Promise.all([
    decryptField(row.bio_enc),
    decryptField(row.avatar_url_enc),
    decryptField(row.profile_email_enc),
    decryptField(row.wallet_address_enc),
  ]);
  return {
    userId: row.id,
    username: row.username,
    displayName: row.display_name || null,
    bio: bioDecrypted ?? row.bio ?? null,
    avatarUrl: avatarUrlDecrypted ?? row.avatar_url ?? null,
    profileEmail: profileEmailDecrypted ?? row.profile_email ?? null,
    profileEmailIsRecovery: row.profile_email_is_recovery || false,
    did: row.did || null,
    walletAddress: walletDecrypted ?? row.wallet_address ?? null,
    emailForwardEnabled: row.email_forward_enabled || false,
    emailForwardMailcowId: row.email_forward_mailcow_id || null,
    createdAt: row.created_at?.toISOString?.() || new Date(row.created_at).toISOString(),
    updatedAt: row.updated_at?.toISOString?.() || row.created_at?.toISOString?.() || new Date().toISOString(),
  };
}

export async function getUserProfile(userId: string): Promise<StoredUserProfile | null> {
  const [row] = await sql`SELECT * FROM users WHERE id = ${userId}`;
  if (!row) return null;
  return await rowToProfile(row);
}

export interface UserProfileUpdates {
  displayName?: string | null;
  bio?: string | null;
  avatarUrl?: string | null;
  profileEmail?: string | null;
  profileEmailIsRecovery?: boolean;
  walletAddress?: string | null;
}

export async function updateUserProfile(userId: string, updates: UserProfileUpdates): Promise<StoredUserProfile | null> {
  const sets: string[] = [];
  const values: any[] = [];

  if (updates.displayName !== undefined) { sets.push('display_name'); values.push(updates.displayName); }
  if (updates.bio !== undefined) {
    sets.push('bio'); values.push(updates.bio);
    const enc = await encryptField(updates.bio); sets.push('bio_enc'); values.push(enc);
  }
  if (updates.avatarUrl !== undefined) {
    sets.push('avatar_url'); values.push(updates.avatarUrl);
    const enc = await encryptField(updates.avatarUrl); sets.push('avatar_url_enc'); values.push(enc);
  }
  if (updates.profileEmail !== undefined) {
    sets.push('profile_email'); values.push(updates.profileEmail);
    const enc = await encryptField(updates.profileEmail); sets.push('profile_email_enc'); values.push(enc);
  }
  if (updates.profileEmailIsRecovery !== undefined) { sets.push('profile_email_is_recovery'); values.push(updates.profileEmailIsRecovery); }
  if (updates.walletAddress !== undefined) {
    sets.push('wallet_address'); values.push(updates.walletAddress);
    const enc = await encryptField(updates.walletAddress); sets.push('wallet_address_enc'); values.push(enc);
  }

  if (sets.length === 0) {
    return getUserProfile(userId);
  }

  // Build dynamic update — use tagged template for each field
  // postgres lib doesn't easily support dynamic column names, so we use unsafe for the SET clause
  const setClauses = sets.map((col, i) => `${col} = $${i + 2}`).join(', ');
  const params = [userId, ...values];
  await sql.unsafe(
    `UPDATE users SET ${setClauses}, updated_at = NOW() WHERE id = $1`,
    params,
  );

  return getUserProfile(userId);
}

// ============================================================================
// ENCRYPTED ADDRESS OPERATIONS
// ============================================================================

export interface StoredEncryptedAddress {
  id: string;
  userId: string;
  ciphertext: string;
  iv: string;
  label: string;
  labelCustom: string | null;
  isDefault: boolean;
  createdAt: string;
  updatedAt: string;
}

function rowToAddress(row: any): StoredEncryptedAddress {
  return {
    id: row.id,
    userId: row.user_id,
    ciphertext: row.ciphertext,
    iv: row.iv,
    label: row.label,
    labelCustom: row.label_custom || null,
    isDefault: row.is_default || false,
    createdAt: row.created_at?.toISOString?.() || new Date(row.created_at).toISOString(),
    updatedAt: row.updated_at?.toISOString?.() || new Date(row.updated_at).toISOString(),
  };
}

export async function getUserAddresses(userId: string): Promise<StoredEncryptedAddress[]> {
  const rows = await sql`
    SELECT * FROM encrypted_addresses
    WHERE user_id = ${userId}
    ORDER BY is_default DESC, created_at ASC
  `;
  return rows.map(rowToAddress);
}

export async function getAddressById(id: string, userId: string): Promise<StoredEncryptedAddress | null> {
  const [row] = await sql`
    SELECT * FROM encrypted_addresses
    WHERE id = ${id} AND user_id = ${userId}
  `;
  if (!row) return null;
  return rowToAddress(row);
}

export async function saveUserAddress(
  userId: string,
  addr: { id: string; ciphertext: string; iv: string; label: string; labelCustom?: string; isDefault: boolean },
): Promise<StoredEncryptedAddress> {
  // If setting as default, unset all others first
  if (addr.isDefault) {
    await sql`UPDATE encrypted_addresses SET is_default = FALSE WHERE user_id = ${userId}`;
  }

  const rows = await sql`
    INSERT INTO encrypted_addresses (id, user_id, ciphertext, iv, label, label_custom, is_default)
    VALUES (${addr.id}, ${userId}, ${addr.ciphertext}, ${addr.iv}, ${addr.label}, ${addr.labelCustom || null}, ${addr.isDefault})
    ON CONFLICT (id, user_id) DO UPDATE SET
      ciphertext = ${addr.ciphertext},
      iv = ${addr.iv},
      label = ${addr.label},
      label_custom = ${addr.labelCustom || null},
      is_default = ${addr.isDefault},
      updated_at = NOW()
    RETURNING *
  `;
  return rowToAddress(rows[0]);
}

export async function deleteUserAddress(id: string, userId: string): Promise<boolean> {
  const result = await sql`
    DELETE FROM encrypted_addresses
    WHERE id = ${id} AND user_id = ${userId}
  `;
  return result.count > 0;
}

// ============================================================================
// LINKED WALLETS (SIWE-verified external wallet associations)
// ============================================================================

export interface StoredLinkedWallet {
  id: string;
  userId: string;
  ciphertext: string;
  iv: string;
  addressHash: string;
  source: 'external-eoa' | 'external-safe';
  verified: boolean;
  linkedAt: string;
}

function rowToLinkedWallet(row: any): StoredLinkedWallet {
  return {
    id: row.id,
    userId: row.user_id,
    ciphertext: row.ciphertext,
    iv: row.iv,
    addressHash: row.address_hash,
    source: row.source,
    verified: row.verified || false,
    linkedAt: row.linked_at?.toISOString?.() || new Date(row.linked_at).toISOString(),
  };
}

export async function createLinkedWallet(
  userId: string,
  wallet: { id: string; ciphertext: string; iv: string; addressHash: string; source: 'external-eoa' | 'external-safe' },
): Promise<StoredLinkedWallet> {
  const rows = await sql`
    INSERT INTO linked_wallets (id, user_id, ciphertext, iv, address_hash, source, verified)
    VALUES (${wallet.id}, ${userId}, ${wallet.ciphertext}, ${wallet.iv}, ${wallet.addressHash}, ${wallet.source}, TRUE)
    ON CONFLICT (id, user_id) DO UPDATE SET
      ciphertext = ${wallet.ciphertext},
      iv = ${wallet.iv},
      address_hash = ${wallet.addressHash},
      source = ${wallet.source},
      verified = TRUE,
      linked_at = NOW()
    RETURNING *
  `;
  return rowToLinkedWallet(rows[0]);
}

export async function getLinkedWallets(userId: string): Promise<StoredLinkedWallet[]> {
  const rows = await sql`
    SELECT * FROM linked_wallets
    WHERE user_id = ${userId}
    ORDER BY linked_at ASC
  `;
  return rows.map(rowToLinkedWallet);
}

export async function deleteLinkedWallet(userId: string, id: string): Promise<boolean> {
  const result = await sql`
    DELETE FROM linked_wallets
    WHERE id = ${id} AND user_id = ${userId}
  `;
  return result.count > 0;
}

export async function linkedWalletExists(userId: string, addressHash: string): Promise<boolean> {
  const [row] = await sql`
    SELECT 1 FROM linked_wallets
    WHERE user_id = ${userId} AND address_hash = ${addressHash}
    LIMIT 1
  `;
  return !!row;
}

// ============================================================================
// EMAIL FORWARDING OPERATIONS
// ============================================================================

export async function getEmailForwardStatus(userId: string): Promise<{
  enabled: boolean;
  mailcowId: string | null;
  username: string;
  profileEmail: string | null;
} | null> {
  const [row] = await sql`
    SELECT username, profile_email, profile_email_enc, email_forward_enabled, email_forward_mailcow_id
    FROM users WHERE id = ${userId}
  `;
  if (!row) return null;
  const profileEmailDecrypted = await decryptField(row.profile_email_enc);
  return {
    enabled: row.email_forward_enabled || false,
    mailcowId: row.email_forward_mailcow_id || null,
    username: row.username,
    profileEmail: profileEmailDecrypted ?? row.profile_email ?? null,
  };
}

export async function setEmailForward(userId: string, enabled: boolean, mailcowId: string | null): Promise<void> {
  await sql`
    UPDATE users
    SET email_forward_enabled = ${enabled}, email_forward_mailcow_id = ${mailcowId}, updated_at = NOW()
    WHERE id = ${userId}
  `;
}

// ============================================================================
// ADMIN OPERATIONS
// ============================================================================

export interface AdminUserInfo {
  userId: string;
  username: string;
  displayName: string | null;
  did: string | null;
  email: string | null;
  createdAt: string;
  credentialCount: number;
  spaceMembershipCount: number;
}

export async function listAllUsers(): Promise<AdminUserInfo[]> {
  const rows = await sql`
    SELECT u.id, u.username, u.display_name, u.did, u.email, u.email_enc, u.created_at,
           (SELECT COUNT(*)::int FROM credentials c WHERE c.user_id = u.id) as credential_count,
           (SELECT COUNT(*)::int FROM space_members sm WHERE sm.user_did = u.did) as space_membership_count
    FROM users u
    ORDER BY u.created_at DESC
  `;
  return Promise.all(rows.map(async row => {
    const emailDecrypted = await decryptField(row.email_enc);
    return {
      userId: row.id,
      username: row.username,
      displayName: row.display_name || null,
      did: row.did || null,
      email: emailDecrypted ?? row.email ?? null,
      createdAt: row.created_at?.toISOString?.() || new Date(row.created_at).toISOString(),
      credentialCount: Number(row.credential_count),
      spaceMembershipCount: Number(row.space_membership_count),
    };
  }));
}

export async function deleteUser(userId: string): Promise<boolean> {
  const user = await getUserById(userId);
  if (!user) return false;

  // Remove space memberships for this user's DID
  if (user.did) {
    await sql`DELETE FROM space_members WHERE user_did = ${user.did}`;
  }

  // Delete user (CASCADE handles credentials, recovery_tokens, guardians, etc.)
  const result = await sql`DELETE FROM users WHERE id = ${userId}`;
  return result.count > 0;
}

export async function deleteSpaceMembers(spaceSlug: string): Promise<number> {
  const result = await sql`DELETE FROM space_members WHERE space_slug = ${spaceSlug}`;
  return result.count;
}

// ============================================================================
// HEALTH CHECK
// ============================================================================

export async function checkDatabaseHealth(): Promise<boolean> {
  try {
    await sql`SELECT 1`;
    return true;
  } catch {
    return false;
  }
}

// ============================================================================
// SPACE INVITE OPERATIONS
// ============================================================================

export interface StoredSpaceInvite {
  id: string;
  spaceSlug: string;
  email: string | null;
  role: string;
  token: string;
  invitedBy: string;
  status: 'pending' | 'accepted' | 'expired' | 'revoked';
  createdAt: number;
  expiresAt: number;
  acceptedAt: number | null;
  acceptedByDid: string | null;
}

async function rowToInvite(row: any): Promise<StoredSpaceInvite> {
  const emailDecrypted = await decryptField(row.email_enc);
  return {
    id: row.id,
    spaceSlug: row.space_slug,
    email: emailDecrypted ?? row.email ?? null,
    role: row.role,
    token: row.token,
    invitedBy: row.invited_by,
    status: row.status,
    createdAt: new Date(row.created_at).getTime(),
    expiresAt: new Date(row.expires_at).getTime(),
    acceptedAt: row.accepted_at ? new Date(row.accepted_at).getTime() : null,
    acceptedByDid: row.accepted_by_did || null,
  };
}

export async function createSpaceInvite(
  id: string,
  spaceSlug: string,
  token: string,
  invitedBy: string,
  role: string,
  expiresAt: number,
  email?: string,
): Promise<StoredSpaceInvite> {
  const emailEnc = await encryptField(email || null);
  const rows = await sql`
    INSERT INTO space_invites (id, space_slug, email, email_enc, role, token, invited_by, expires_at)
    VALUES (${id}, ${spaceSlug}, ${email || null}, ${emailEnc}, ${role}, ${token}, ${invitedBy}, ${new Date(expiresAt)})
    RETURNING *
  `;
  return await rowToInvite(rows[0]);
}

export async function getSpaceInviteByToken(token: string): Promise<StoredSpaceInvite | null> {
  const rows = await sql`SELECT * FROM space_invites WHERE token = ${token}`;
  if (rows.length === 0) return null;
  return await rowToInvite(rows[0]);
}

export async function listSpaceInvites(spaceSlug: string): Promise<StoredSpaceInvite[]> {
  const [spaceRows, identityRows] = await Promise.all([
    sql`SELECT * FROM space_invites WHERE space_slug = ${spaceSlug} ORDER BY created_at DESC`,
    sql`SELECT * FROM identity_invites WHERE space_slug = ${spaceSlug} ORDER BY created_at DESC`,
  ]);
  const spaceInvites = await Promise.all(spaceRows.map(rowToInvite));
  const identityInvites = await Promise.all(identityRows.map(async (row: any) => {
    const emailDecrypted = await decryptField(row.email_enc);
    return {
      id: row.id,
      spaceSlug: row.space_slug,
      email: emailDecrypted ?? row.email ?? null,
      role: row.space_role || 'member',
      token: row.token,
      invitedBy: row.invited_by_user_id,
      status: row.status === 'claimed' ? 'accepted' : row.status,
      createdAt: new Date(row.created_at).getTime(),
      expiresAt: new Date(row.expires_at).getTime(),
      acceptedAt: row.claimed_at ? new Date(row.claimed_at).getTime() : null,
      acceptedByDid: row.claimed_by_user_id || null,
    } as StoredSpaceInvite;
  }));
  return [...spaceInvites, ...identityInvites].sort((a, b) => b.createdAt - a.createdAt);
}

export async function acceptSpaceInvite(token: string, acceptedByDid: string): Promise<StoredSpaceInvite | null> {
  const rows = await sql`
    UPDATE space_invites
    SET status = 'accepted', accepted_at = NOW(), accepted_by_did = ${acceptedByDid}
    WHERE token = ${token} AND status = 'pending' AND expires_at > NOW()
    RETURNING *
  `;
  if (rows.length === 0) return null;
  return await rowToInvite(rows[0]);
}

export async function revokeSpaceInvite(id: string, spaceSlug: string): Promise<boolean> {
  const result = await sql`
    UPDATE space_invites SET status = 'revoked'
    WHERE id = ${id} AND space_slug = ${spaceSlug} AND status = 'pending'
  `;
  if (result.count > 0) return true;
  // Also check identity_invites (email invites with space_slug)
  const result2 = await sql`
    UPDATE identity_invites SET status = 'revoked'
    WHERE id = ${id} AND space_slug = ${spaceSlug} AND status = 'pending'
  `;
  return result2.count > 0;
}

// ============================================================================
// NOTIFICATION OPERATIONS
// ============================================================================

export interface StoredNotification {
  id: string;
  userDid: string;
  category: 'space' | 'module' | 'system' | 'social' | 'payment';
  eventType: string;
  title: string;
  body: string | null;
  spaceSlug: string | null;
  moduleId: string | null;
  actionUrl: string | null;
  actorDid: string | null;
  actorUsername: string | null;
  metadata: Record<string, any>;
  read: boolean;
  dismissed: boolean;
  deliveredWs: boolean;
  deliveredEmail: boolean;
  deliveredPush: boolean;
  createdAt: string;
  readAt: string | null;
  expiresAt: string | null;
}

async function rowToNotification(row: any): Promise<StoredNotification> {
  const [titleDecrypted, bodyDecrypted, actorUsernameDecrypted] = await Promise.all([
    decryptField(row.title_enc),
    decryptField(row.body_enc),
    decryptField(row.actor_username_enc),
  ]);
  return {
    id: row.id,
    userDid: row.user_did,
    category: row.category,
    eventType: row.event_type,
    title: titleDecrypted ?? row.title,
    body: bodyDecrypted ?? row.body ?? null,
    spaceSlug: row.space_slug || null,
    moduleId: row.module_id || null,
    actionUrl: row.action_url || null,
    actorDid: row.actor_did || null,
    actorUsername: actorUsernameDecrypted ?? row.actor_username ?? null,
    metadata: row.metadata || {},
    read: row.read,
    dismissed: row.dismissed,
    deliveredWs: row.delivered_ws,
    deliveredEmail: row.delivered_email,
    deliveredPush: row.delivered_push,
    createdAt: row.created_at?.toISOString?.() || new Date(row.created_at).toISOString(),
    readAt: row.read_at ? (row.read_at?.toISOString?.() || new Date(row.read_at).toISOString()) : null,
    expiresAt: row.expires_at ? (row.expires_at?.toISOString?.() || new Date(row.expires_at).toISOString()) : null,
  };
}

export async function createNotification(notif: {
  id: string;
  userDid: string;
  category: 'space' | 'module' | 'system' | 'social' | 'payment';
  eventType: string;
  title: string;
  body?: string;
  spaceSlug?: string;
  moduleId?: string;
  actionUrl?: string;
  actorDid?: string;
  actorUsername?: string;
  metadata?: Record<string, any>;
  expiresAt?: Date;
}): Promise<StoredNotification> {
  const [titleEnc, bodyEnc, actorUsernameEnc] = await Promise.all([
    encryptField(notif.title),
    encryptField(notif.body || null),
    encryptField(notif.actorUsername || null),
  ]);
  const rows = await sql`
    INSERT INTO notifications (id, user_did, category, event_type, title, body, title_enc, body_enc, actor_username_enc, space_slug, module_id, action_url, actor_did, actor_username, metadata, expires_at)
    VALUES (
      ${notif.id},
      ${notif.userDid},
      ${notif.category},
      ${notif.eventType},
      ${notif.title},
      ${notif.body || null},
      ${titleEnc},
      ${bodyEnc},
      ${actorUsernameEnc},
      ${notif.spaceSlug || null},
      ${notif.moduleId || null},
      ${notif.actionUrl || null},
      ${notif.actorDid || null},
      ${notif.actorUsername || null},
      ${JSON.stringify(notif.metadata || {})},
      ${notif.expiresAt || null}
    )
    RETURNING *
  `;
  return await rowToNotification(rows[0]);
}

export async function getUserNotifications(
  userDid: string,
  opts: { unreadOnly?: boolean; limit?: number; offset?: number; category?: string } = {},
): Promise<StoredNotification[]> {
  const limit = opts.limit || 50;
  const offset = opts.offset || 0;

  let rows;
  if (opts.unreadOnly && opts.category) {
    rows = await sql`
      SELECT * FROM notifications
      WHERE user_did = ${userDid} AND NOT dismissed AND NOT read AND category = ${opts.category}
      ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}
    `;
  } else if (opts.unreadOnly) {
    rows = await sql`
      SELECT * FROM notifications
      WHERE user_did = ${userDid} AND NOT dismissed AND NOT read
      ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}
    `;
  } else if (opts.category) {
    rows = await sql`
      SELECT * FROM notifications
      WHERE user_did = ${userDid} AND NOT dismissed AND category = ${opts.category}
      ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}
    `;
  } else {
    rows = await sql`
      SELECT * FROM notifications
      WHERE user_did = ${userDid} AND NOT dismissed
      ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}
    `;
  }
  return Promise.all(rows.map(rowToNotification));
}

export async function getUnreadCount(userDid: string): Promise<number> {
  try {
    const [row] = await sql`
      SELECT COUNT(*)::int as count FROM notifications
      WHERE user_did = ${userDid} AND NOT read AND NOT dismissed
    `;
    return row?.count ?? 0;
  } catch (err) {
    console.error("[notifications] getUnreadCount failed:", err instanceof Error ? err.message : err);
    return 0;
  }
}

export async function markNotificationRead(id: string, userDid: string): Promise<boolean> {
  const result = await sql`
    UPDATE notifications SET read = TRUE, read_at = NOW()
    WHERE id = ${id} AND user_did = ${userDid}
  `;
  return result.count > 0;
}

export async function markAllNotificationsRead(
  userDid: string,
  opts: { spaceSlug?: string; category?: string } = {},
): Promise<number> {
  let result;
  if (opts.spaceSlug && opts.category) {
    result = await sql`
      UPDATE notifications SET read = TRUE, read_at = NOW()
      WHERE user_did = ${userDid} AND NOT read AND space_slug = ${opts.spaceSlug} AND category = ${opts.category}
    `;
  } else if (opts.spaceSlug) {
    result = await sql`
      UPDATE notifications SET read = TRUE, read_at = NOW()
      WHERE user_did = ${userDid} AND NOT read AND space_slug = ${opts.spaceSlug}
    `;
  } else if (opts.category) {
    result = await sql`
      UPDATE notifications SET read = TRUE, read_at = NOW()
      WHERE user_did = ${userDid} AND NOT read AND category = ${opts.category}
    `;
  } else {
    result = await sql`
      UPDATE notifications SET read = TRUE, read_at = NOW()
      WHERE user_did = ${userDid} AND NOT read
    `;
  }
  return result.count;
}

export async function dismissNotification(id: string, userDid: string): Promise<boolean> {
  const result = await sql`
    UPDATE notifications SET dismissed = TRUE
    WHERE id = ${id} AND user_did = ${userDid}
  `;
  return result.count > 0;
}

export async function markNotificationDelivered(id: string, channel: 'ws' | 'email' | 'push'): Promise<void> {
  const col = channel === 'ws' ? 'delivered_ws' : channel === 'email' ? 'delivered_email' : 'delivered_push';
  await sql.unsafe(`UPDATE notifications SET ${col} = TRUE WHERE id = $1`, [id]);
}

export async function cleanExpiredNotifications(): Promise<number> {
  const result = await sql`DELETE FROM notifications WHERE expires_at IS NOT NULL AND expires_at < NOW()`;
  return result.count;
}

// ── Notification Preferences ──

export interface StoredNotificationPreferences {
  userDid: string;
  emailEnabled: boolean;
  pushEnabled: boolean;
  quietHoursStart: string | null;
  quietHoursEnd: string | null;
  mutedSpaces: string[];
  mutedCategories: string[];
  digestFrequency: 'none' | 'daily' | 'weekly';
  updatedAt: string;
}

function rowToPreferences(row: any): StoredNotificationPreferences {
  return {
    userDid: row.user_did,
    emailEnabled: row.email_enabled,
    pushEnabled: row.push_enabled,
    quietHoursStart: row.quiet_hours_start || null,
    quietHoursEnd: row.quiet_hours_end || null,
    mutedSpaces: row.muted_spaces || [],
    mutedCategories: row.muted_categories || [],
    digestFrequency: row.digest_frequency || 'none',
    updatedAt: row.updated_at?.toISOString?.() || new Date(row.updated_at).toISOString(),
  };
}

export async function getNotificationPreferences(userDid: string): Promise<StoredNotificationPreferences | null> {
  const [row] = await sql`SELECT * FROM notification_preferences WHERE user_did = ${userDid}`;
  if (!row) return null;
  return rowToPreferences(row);
}

export async function upsertNotificationPreferences(
  userDid: string,
  prefs: Partial<Omit<StoredNotificationPreferences, 'userDid' | 'updatedAt'>>,
): Promise<StoredNotificationPreferences> {
  const rows = await sql`
    INSERT INTO notification_preferences (user_did, email_enabled, push_enabled, quiet_hours_start, quiet_hours_end, muted_spaces, muted_categories, digest_frequency)
    VALUES (
      ${userDid},
      ${prefs.emailEnabled ?? true},
      ${prefs.pushEnabled ?? true},
      ${prefs.quietHoursStart || null},
      ${prefs.quietHoursEnd || null},
      ${prefs.mutedSpaces || []},
      ${prefs.mutedCategories || []},
      ${prefs.digestFrequency || 'none'}
    )
    ON CONFLICT (user_did) DO UPDATE SET
      email_enabled = COALESCE(${prefs.emailEnabled ?? null}::boolean, notification_preferences.email_enabled),
      push_enabled = COALESCE(${prefs.pushEnabled ?? null}::boolean, notification_preferences.push_enabled),
      quiet_hours_start = COALESCE(${prefs.quietHoursStart ?? null}, notification_preferences.quiet_hours_start),
      quiet_hours_end = COALESCE(${prefs.quietHoursEnd ?? null}, notification_preferences.quiet_hours_end),
      muted_spaces = COALESCE(${prefs.mutedSpaces ?? null}, notification_preferences.muted_spaces),
      muted_categories = COALESCE(${prefs.mutedCategories ?? null}, notification_preferences.muted_categories),
      digest_frequency = COALESCE(${prefs.digestFrequency ?? null}, notification_preferences.digest_frequency),
      updated_at = NOW()
    RETURNING *
  `;
  return rowToPreferences(rows[0]);
}

// ============================================================================
// FUND CLAIM OPERATIONS
// ============================================================================

export interface StoredFundClaim {
  id: string;
  token: string;
  emailHash: string;
  email: string | null;
  walletAddress: string;
  openfortPlayerId: string | null;
  fiatAmount: string | null;
  fiatCurrency: string;
  sessionId: string | null;
  provider: string | null;
  status: 'pending' | 'claimed' | 'expired' | 'resent';
  claimedByUserId: string | null;
  createdAt: number;
  expiresAt: number;
  claimedAt: number | null;
}

async function rowToFundClaim(row: any): Promise<StoredFundClaim> {
  const [emailDecrypted, walletDecrypted] = await Promise.all([
    decryptField(row.email_enc),
    decryptField(row.wallet_address_enc),
  ]);
  return {
    id: row.id,
    token: row.token,
    emailHash: row.email_hmac || row.email_hash,
    email: emailDecrypted ?? row.email ?? null,
    walletAddress: walletDecrypted ?? row.wallet_address,
    openfortPlayerId: row.openfort_player_id || null,
    fiatAmount: row.fiat_amount || null,
    fiatCurrency: row.fiat_currency || 'USD',
    sessionId: row.session_id || null,
    provider: row.provider || null,
    status: row.status,
    claimedByUserId: row.claimed_by_user_id || null,
    createdAt: new Date(row.created_at).getTime(),
    expiresAt: new Date(row.expires_at).getTime(),
    claimedAt: row.claimed_at ? new Date(row.claimed_at).getTime() : null,
  };
}

export async function createFundClaim(claim: {
  id: string;
  token: string;
  emailHash: string;
  email: string;
  walletAddress: string;
  openfortPlayerId?: string;
  fiatAmount?: string;
  fiatCurrency?: string;
  sessionId?: string;
  provider?: string;
  expiresAt: number;
}): Promise<StoredFundClaim> {
  const [emailEnc, walletEnc, emailHmac] = await Promise.all([
    encryptField(claim.email),
    encryptField(claim.walletAddress),
    hashForLookup(claim.email),
  ]);
  const rows = await sql`
    INSERT INTO fund_claims (id, token, email_hash, email, email_enc, email_hmac, wallet_address, wallet_address_enc, openfort_player_id, fiat_amount, fiat_currency, session_id, provider, expires_at)
    VALUES (
      ${claim.id},
      ${claim.token},
      ${claim.emailHash},
      ${claim.email},
      ${emailEnc},
      ${emailHmac},
      ${claim.walletAddress},
      ${walletEnc},
      ${claim.openfortPlayerId || null},
      ${claim.fiatAmount || null},
      ${claim.fiatCurrency || 'USD'},
      ${claim.sessionId || null},
      ${claim.provider || null},
      ${new Date(claim.expiresAt)}
    )
    RETURNING *
  `;
  return await rowToFundClaim(rows[0]);
}

export async function getFundClaimByToken(token: string): Promise<StoredFundClaim | null> {
  const rows = await sql`SELECT * FROM fund_claims WHERE token = ${token}`;
  if (rows.length === 0) return null;
  return await rowToFundClaim(rows[0]);
}

export async function getFundClaimsByEmailHash(emailHash: string): Promise<StoredFundClaim[]> {
  // Try HMAC hash first, fall back to legacy SHA-256 hash
  let rows = await sql`
    SELECT * FROM fund_claims
    WHERE email_hmac = ${emailHash} AND status IN ('pending', 'resent') AND expires_at > NOW()
    ORDER BY created_at DESC
  `;
  if (rows.length === 0) {
    rows = await sql`
      SELECT * FROM fund_claims
      WHERE email_hash = ${emailHash} AND email_hmac IS NULL AND status IN ('pending', 'resent') AND expires_at > NOW()
      ORDER BY created_at DESC
    `;
  }
  return Promise.all(rows.map(rowToFundClaim));
}

export async function acceptFundClaim(token: string, userId: string): Promise<StoredFundClaim | null> {
  const rows = await sql`
    UPDATE fund_claims
    SET status = 'claimed', claimed_by_user_id = ${userId}, claimed_at = NOW(), email = NULL, email_enc = NULL
    WHERE token = ${token} AND status IN ('pending', 'resent') AND expires_at > NOW()
    RETURNING *
  `;
  if (rows.length === 0) return null;
  return await rowToFundClaim(rows[0]);
}

export async function accumulateFundClaim(claimId: string, additionalAmount: string, expiresAt: number): Promise<StoredFundClaim | null> {
  const rows = await sql`
    UPDATE fund_claims
    SET fiat_amount = (COALESCE(fiat_amount::numeric, 0) + ${additionalAmount}::numeric)::text,
        expires_at = ${new Date(expiresAt)}
    WHERE id = ${claimId} AND status IN ('pending', 'resent')
    RETURNING *
  `;
  if (rows.length === 0) return null;
  return await rowToFundClaim(rows[0]);
}

export async function expireFundClaim(claimId: string): Promise<void> {
  await sql`UPDATE fund_claims SET status = 'expired', email = NULL, email_enc = NULL WHERE id = ${claimId} AND status IN ('pending', 'resent')`;
}

export async function cleanExpiredFundClaims(): Promise<number> {
  // Null out email on expired claims, then mark them expired
  await sql`UPDATE fund_claims SET email = NULL, email_enc = NULL, status = 'expired' WHERE status IN ('pending', 'resent') AND expires_at < NOW()`;
  const result = await sql`DELETE FROM fund_claims WHERE status = 'expired' AND expires_at < NOW() - INTERVAL '30 days'`;
  return result.count;
}

// ============================================================================
// OIDC PROVIDER
// ============================================================================

export interface StoredOidcClient {
  clientId: string;
  clientSecret: string;
  name: string;
  redirectUris: string[];
  allowedEmails: string[];
  createdAt: number;
}

export async function getOidcClient(clientId: string): Promise<StoredOidcClient | null> {
  const rows = await sql`SELECT * FROM oidc_clients WHERE client_id = ${clientId}`;
  return rows.length ? mapOidcClientRow(rows[0]) : null;
}

export async function createOidcAuthCode(
  code: string,
  clientId: string,
  userId: string,
  redirectUri: string,
  scope: string,
): Promise<void> {
  const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
  await sql`
    INSERT INTO oidc_auth_codes (code, client_id, user_id, redirect_uri, scope, expires_at)
    VALUES (${code}, ${clientId}, ${userId}, ${redirectUri}, ${scope}, ${expiresAt})
  `;
}

export async function consumeOidcAuthCode(code: string): Promise<{
  clientId: string;
  userId: string;
  redirectUri: string;
  scope: string;
} | null> {
  const rows = await sql`
    UPDATE oidc_auth_codes
    SET used = TRUE
    WHERE code = ${code} AND used = FALSE AND expires_at > NOW()
    RETURNING client_id, user_id, redirect_uri, scope
  `;
  if (!rows.length) return null;
  const r = rows[0];
  return {
    clientId: r.client_id,
    userId: r.user_id,
    redirectUri: r.redirect_uri,
    scope: r.scope,
  };
}

export async function cleanExpiredOidcCodes(): Promise<number> {
  const result = await sql`DELETE FROM oidc_auth_codes WHERE expires_at < NOW() OR used = TRUE`;
  return result.count;
}

export async function seedOidcClients(clients: Array<{
  clientId: string;
  clientSecret: string;
  name: string;
  redirectUris: string[];
  allowedEmails?: string[];
}>): Promise<void> {
  for (const c of clients) {
    await sql`
      INSERT INTO oidc_clients (client_id, client_secret, name, redirect_uris, allowed_emails)
      VALUES (${c.clientId}, ${c.clientSecret}, ${c.name}, ${c.redirectUris}, ${c.allowedEmails || []})
      ON CONFLICT (client_id) DO UPDATE SET
        client_secret = EXCLUDED.client_secret,
        name = EXCLUDED.name,
        redirect_uris = EXCLUDED.redirect_uris,
        allowed_emails = EXCLUDED.allowed_emails
    `;
  }
}

function mapOidcClientRow(r: any): StoredOidcClient {
  return {
    clientId: r.client_id,
    clientSecret: r.client_secret,
    name: r.name,
    redirectUris: r.redirect_uris,
    allowedEmails: r.allowed_emails || [],
    createdAt: new Date(r.created_at).getTime(),
  };
}

export async function listOidcClients(): Promise<StoredOidcClient[]> {
  const rows = await sql`SELECT * FROM oidc_clients ORDER BY created_at`;
  return rows.map(mapOidcClientRow);
}

export async function updateOidcClient(clientId: string, updates: {
  name?: string;
  clientSecret?: string;
  redirectUris?: string[];
  allowedEmails?: string[];
}): Promise<StoredOidcClient | null> {
  const client = await getOidcClient(clientId);
  if (!client) return null;

  const rows = await sql`
    UPDATE oidc_clients SET
      name = ${updates.name ?? client.name},
      client_secret = ${updates.clientSecret ?? client.clientSecret},
      redirect_uris = ${updates.redirectUris ?? client.redirectUris},
      allowed_emails = ${updates.allowedEmails ?? client.allowedEmails}
    WHERE client_id = ${clientId}
    RETURNING *
  `;
  return rows.length ? mapOidcClientRow(rows[0]) : null;
}

export async function createOidcClient(client: {
  clientId: string;
  clientSecret: string;
  name: string;
  redirectUris: string[];
  allowedEmails?: string[];
}): Promise<StoredOidcClient> {
  const rows = await sql`
    INSERT INTO oidc_clients (client_id, client_secret, name, redirect_uris, allowed_emails)
    VALUES (${client.clientId}, ${client.clientSecret}, ${client.name}, ${client.redirectUris}, ${client.allowedEmails || []})
    RETURNING *
  `;
  return mapOidcClientRow(rows[0]);
}

export async function deleteOidcClient(clientId: string): Promise<boolean> {
  const result = await sql`DELETE FROM oidc_clients WHERE client_id = ${clientId}`;
  return result.count > 0;
}

// ============================================================================
// IDENTITY INVITES
// ============================================================================

export interface StoredIdentityInvite {
  id: string;
  token: string;
  email: string;
  invitedByUserId: string;
  invitedByUsername: string;
  message: string | null;
  spaceSlug: string | null;
  spaceRole: string;
  clientId: string | null;
  status: string;
  claimedByUserId: string | null;
  createdAt: number;
  expiresAt: number;
  claimedAt: number | null;
}

async function mapInviteRow(r: any): Promise<StoredIdentityInvite> {
  const [emailDecrypted, messageDecrypted] = await Promise.all([
    decryptField(r.email_enc),
    decryptField(r.message_enc),
  ]);
  return {
    id: r.id,
    token: r.token,
    email: emailDecrypted ?? r.email,
    invitedByUserId: r.invited_by_user_id,
    invitedByUsername: r.invited_by_username,
    message: messageDecrypted ?? r.message ?? null,
    spaceSlug: r.space_slug,
    spaceRole: r.space_role,
    clientId: r.client_id || null,
    status: r.status,
    claimedByUserId: r.claimed_by_user_id,
    createdAt: new Date(r.created_at).getTime(),
    expiresAt: new Date(r.expires_at).getTime(),
    claimedAt: r.claimed_at ? new Date(r.claimed_at).getTime() : null,
  };
}

export async function createIdentityInvite(invite: {
  id: string;
  token: string;
  email: string;
  invitedByUserId: string;
  invitedByUsername: string;
  message?: string;
  spaceSlug?: string;
  spaceRole?: string;
  clientId?: string;
  expiresAt: number;
}): Promise<StoredIdentityInvite> {
  const [emailEnc, emailHash, messageEnc] = await Promise.all([
    encryptField(invite.email),
    hashForLookup(invite.email),
    encryptField(invite.message || null),
  ]);
  const rows = await sql`
    INSERT INTO identity_invites (id, token, email, email_enc, email_hash, invited_by_user_id, invited_by_username, message, message_enc, space_slug, space_role, client_id, expires_at)
    VALUES (${invite.id}, ${invite.token}, ${invite.email}, ${emailEnc}, ${emailHash}, ${invite.invitedByUserId},
            ${invite.invitedByUsername}, ${invite.message || null}, ${messageEnc},
            ${invite.spaceSlug || null}, ${invite.spaceRole || 'member'},
            ${invite.clientId || null},
            ${new Date(invite.expiresAt).toISOString()})
    RETURNING *
  `;
  return await mapInviteRow(rows[0]);
}

export async function getIdentityInviteByToken(token: string): Promise<StoredIdentityInvite | null> {
  const rows = await sql`SELECT * FROM identity_invites WHERE token = ${token}`;
  return rows.length ? await mapInviteRow(rows[0]) : null;
}

export async function getIdentityInvitesByEmail(email: string): Promise<StoredIdentityInvite[]> {
  const hash = await hashForLookup(email);
  // Try hash lookup first, fall back to plaintext for pre-migration rows
  let rows = await sql`SELECT * FROM identity_invites WHERE email_hash = ${hash} ORDER BY created_at DESC`;
  if (rows.length === 0) {
    rows = await sql`SELECT * FROM identity_invites WHERE email = ${email} AND email_hash IS NULL ORDER BY created_at DESC`;
  }
  return Promise.all(rows.map(mapInviteRow));
}

export async function getIdentityInvitesByInviter(userId: string): Promise<StoredIdentityInvite[]> {
  const rows = await sql`SELECT * FROM identity_invites WHERE invited_by_user_id = ${userId} ORDER BY created_at DESC`;
  return Promise.all(rows.map(mapInviteRow));
}

export async function getIdentityInvitesByClient(clientId: string): Promise<StoredIdentityInvite[]> {
  const rows = await sql`SELECT * FROM identity_invites WHERE client_id = ${clientId} ORDER BY created_at DESC`;
  return Promise.all(rows.map(mapInviteRow));
}

export async function claimIdentityInvite(token: string, claimedByUserId: string): Promise<StoredIdentityInvite | null> {
  const rows = await sql`
    UPDATE identity_invites
    SET status = 'claimed', claimed_by_user_id = ${claimedByUserId}, claimed_at = NOW()
    WHERE token = ${token} AND status = 'pending' AND expires_at > NOW()
    RETURNING *
  `;
  return rows.length ? await mapInviteRow(rows[0]) : null;
}

export async function revokeIdentityInvite(id: string, userId: string): Promise<boolean> {
  const result = await sql`
    UPDATE identity_invites SET status = 'revoked'
    WHERE id = ${id} AND invited_by_user_id = ${userId} AND status = 'pending'
  `;
  return result.count > 0;
}

export async function cleanExpiredIdentityInvites(): Promise<number> {
  const result = await sql`
    UPDATE identity_invites SET status = 'expired'
    WHERE status = 'pending' AND expires_at < NOW()
  `;
  return result.count;
}

// ============================================================================
// PUSH SUBSCRIPTIONS
// ============================================================================

export interface StoredPushSubscription {
  id: string;
  userDid: string;
  endpoint: string;
  keyP256dh: string;
  keyAuth: string;
  userAgent: string | null;
  createdAt: string;
  lastUsed: string | null;
}

export async function savePushSubscription(sub: {
  id: string;
  userDid: string;
  endpoint: string;
  keyP256dh: string;
  keyAuth: string;
  userAgent?: string;
}): Promise<void> {
  await sql`
    INSERT INTO push_subscriptions (id, user_did, endpoint, key_p256dh, key_auth, user_agent)
    VALUES (${sub.id}, ${sub.userDid}, ${sub.endpoint}, ${sub.keyP256dh}, ${sub.keyAuth}, ${sub.userAgent ?? null})
    ON CONFLICT (user_did, endpoint) DO UPDATE SET
      key_p256dh = EXCLUDED.key_p256dh,
      key_auth = EXCLUDED.key_auth,
      user_agent = EXCLUDED.user_agent
  `;
}

export async function getUserPushSubscriptions(userDid: string): Promise<StoredPushSubscription[]> {
  const rows = await sql`
    SELECT id, user_did, endpoint, key_p256dh, key_auth, user_agent, created_at, last_used
    FROM push_subscriptions
    WHERE user_did = ${userDid}
  `;
  return rows.map(r => ({
    id: r.id,
    userDid: r.user_did,
    endpoint: r.endpoint,
    keyP256dh: r.key_p256dh,
    keyAuth: r.key_auth,
    userAgent: r.user_agent,
    createdAt: r.created_at,
    lastUsed: r.last_used,
  }));
}

export async function deletePushSubscription(id: string, userDid: string): Promise<boolean> {
  const result = await sql`
    DELETE FROM push_subscriptions WHERE id = ${id} AND user_did = ${userDid}
  `;
  return result.count > 0;
}

export async function deletePushSubscriptionByEndpoint(endpoint: string): Promise<void> {
  await sql`DELETE FROM push_subscriptions WHERE endpoint = ${endpoint}`;
}

export async function updatePushSubscriptionLastUsed(id: string): Promise<void> {
  await sql`UPDATE push_subscriptions SET last_used = NOW() WHERE id = ${id}`;
}

// ============================================================================
// DELEGATIONS (person-to-person liquid democracy)
// ============================================================================

export type DelegationAuthority = 'gov-ops' | 'fin-ops' | 'dev-ops' | 'custom';
export type DelegationState = 'active' | 'paused' | 'revoked';

export interface StoredDelegation {
  id: string;
  delegatorDid: string;
  delegateDid: string;
  authority: DelegationAuthority;
  weight: number;
  maxDepth: number;
  retainAuthority: boolean;
  spaceSlug: string;
  state: DelegationState;
  customScope: string | null;
  expiresAt: number | null;
  createdAt: number;
  updatedAt: number;
}

function mapDelegationRow(r: any): StoredDelegation {
  return {
    id: r.id,
    delegatorDid: r.delegator_did,
    delegateDid: r.delegate_did,
    authority: r.authority,
    weight: parseFloat(r.weight),
    maxDepth: r.max_depth,
    retainAuthority: r.retain_authority,
    spaceSlug: r.space_slug,
    state: r.state,
    customScope: r.custom_scope || null,
    expiresAt: r.expires_at ? new Date(r.expires_at).getTime() : null,
    createdAt: new Date(r.created_at).getTime(),
    updatedAt: new Date(r.updated_at).getTime(),
  };
}

export async function createDelegation(d: {
  id: string;
  delegatorDid: string;
  delegateDid: string;
  authority: DelegationAuthority;
  weight: number;
  maxDepth?: number;
  retainAuthority?: boolean;
  spaceSlug: string;
  customScope?: string;
  expiresAt?: number;
}): Promise<StoredDelegation> {
  const rows = await sql`
    INSERT INTO delegations (id, delegator_did, delegate_did, authority, weight, max_depth, retain_authority, space_slug, custom_scope, expires_at)
    VALUES (${d.id}, ${d.delegatorDid}, ${d.delegateDid}, ${d.authority}, ${d.weight},
            ${d.maxDepth ?? 3}, ${d.retainAuthority ?? true}, ${d.spaceSlug},
            ${d.customScope || null}, ${d.expiresAt ? new Date(d.expiresAt).toISOString() : null})
    RETURNING *
  `;
  return mapDelegationRow(rows[0]);
}

export async function getDelegation(id: string): Promise<StoredDelegation | null> {
  const rows = await sql`SELECT * FROM delegations WHERE id = ${id}`;
  return rows.length ? mapDelegationRow(rows[0]) : null;
}

export async function listDelegationsFrom(delegatorDid: string, spaceSlug: string): Promise<StoredDelegation[]> {
  const rows = await sql`
    SELECT * FROM delegations
    WHERE delegator_did = ${delegatorDid} AND space_slug = ${spaceSlug} AND state != 'revoked'
    ORDER BY authority, created_at
  `;
  return rows.map(mapDelegationRow);
}

export async function listDelegationsTo(delegateDid: string, spaceSlug: string): Promise<StoredDelegation[]> {
  const rows = await sql`
    SELECT * FROM delegations
    WHERE delegate_did = ${delegateDid} AND space_slug = ${spaceSlug} AND state != 'revoked'
    ORDER BY authority, created_at
  `;
  return rows.map(mapDelegationRow);
}

export async function updateDelegation(id: string, updates: {
  weight?: number;
  state?: DelegationState;
  maxDepth?: number;
  retainAuthority?: boolean;
  expiresAt?: number | null;
}): Promise<StoredDelegation | null> {
  const sets: string[] = [];
  const vals: any[] = [];
  if (updates.weight !== undefined) { sets.push('weight'); vals.push(updates.weight); }
  if (updates.state !== undefined) { sets.push('state'); vals.push(updates.state); }
  if (updates.maxDepth !== undefined) { sets.push('max_depth'); vals.push(updates.maxDepth); }
  if (updates.retainAuthority !== undefined) { sets.push('retain_authority'); vals.push(updates.retainAuthority); }
  if (updates.expiresAt !== undefined) { sets.push('expires_at'); vals.push(updates.expiresAt ? new Date(updates.expiresAt).toISOString() : null); }
  if (sets.length === 0) return getDelegation(id);

  const rows = await sql`
    UPDATE delegations SET
      ${sql(Object.fromEntries(sets.map((s, i) => [s, vals[i]])))}
      , updated_at = NOW()
    WHERE id = ${id} AND state != 'revoked'
    RETURNING *
  `;
  return rows.length ? mapDelegationRow(rows[0]) : null;
}

export async function revokeDelegation(id: string): Promise<boolean> {
  const result = await sql`
    UPDATE delegations SET state = 'revoked', updated_at = NOW()
    WHERE id = ${id} AND state != 'revoked'
  `;
  return result.count > 0;
}

/** Get total weight delegated by a user for a given authority in a space */
export async function getTotalDelegatedWeight(delegatorDid: string, authority: string, spaceSlug: string): Promise<number> {
  const rows = await sql`
    SELECT COALESCE(SUM(weight), 0) as total
    FROM delegations
    WHERE delegator_did = ${delegatorDid} AND authority = ${authority}
      AND space_slug = ${spaceSlug} AND state = 'active'
  `;
  return parseFloat(rows[0].total);
}

/** Get all active delegations in a space for a given authority (for trust computation) */
export async function listActiveDelegations(spaceSlug: string, authority?: string): Promise<StoredDelegation[]> {
  const rows = authority
    ? await sql`
        SELECT * FROM delegations
        WHERE space_slug = ${spaceSlug} AND authority = ${authority} AND state = 'active'
          AND (expires_at IS NULL OR expires_at > NOW())
      `
    : await sql`
        SELECT * FROM delegations
        WHERE space_slug = ${spaceSlug} AND state = 'active'
          AND (expires_at IS NULL OR expires_at > NOW())
      `;
  return rows.map(mapDelegationRow);
}

/** Clean expired delegations (mark as revoked) */
export async function cleanExpiredDelegations(): Promise<number> {
  const result = await sql`
    UPDATE delegations SET state = 'revoked', updated_at = NOW()
    WHERE state = 'active' AND expires_at IS NOT NULL AND expires_at < NOW()
  `;
  return result.count;
}

// ============================================================================
// TRUST EVENTS
// ============================================================================

export type TrustEventType =
  | 'delegation_created' | 'delegation_increased' | 'delegation_decreased'
  | 'delegation_revoked' | 'delegation_paused' | 'delegation_resumed'
  | 'endorsement' | 'flag' | 'collaboration' | 'guardian_link';

export interface StoredTrustEvent {
  id: string;
  sourceDid: string;
  targetDid: string;
  eventType: TrustEventType;
  authority: string | null;
  weightDelta: number | null;
  spaceSlug: string;
  metadata: Record<string, unknown>;
  createdAt: number;
}

function mapTrustEventRow(r: any): StoredTrustEvent {
  return {
    id: r.id,
    sourceDid: r.source_did,
    targetDid: r.target_did,
    eventType: r.event_type,
    authority: r.authority || null,
    weightDelta: r.weight_delta != null ? parseFloat(r.weight_delta) : null,
    spaceSlug: r.space_slug,
    metadata: r.metadata || {},
    createdAt: new Date(r.created_at).getTime(),
  };
}

export async function logTrustEvent(event: {
  id: string;
  sourceDid: string;
  targetDid: string;
  eventType: TrustEventType;
  authority?: string;
  weightDelta?: number;
  spaceSlug: string;
  metadata?: Record<string, unknown>;
}): Promise<StoredTrustEvent> {
  const rows = await sql`
    INSERT INTO trust_events (id, source_did, target_did, event_type, authority, weight_delta, space_slug, metadata)
    VALUES (${event.id}, ${event.sourceDid}, ${event.targetDid}, ${event.eventType},
            ${event.authority || null}, ${event.weightDelta ?? null}, ${event.spaceSlug},
            ${JSON.stringify(event.metadata || {})})
    RETURNING *
  `;
  return mapTrustEventRow(rows[0]);
}

export async function getTrustEvents(did: string, spaceSlug: string, limit = 50): Promise<StoredTrustEvent[]> {
  const rows = await sql`
    SELECT * FROM trust_events
    WHERE (source_did = ${did} OR target_did = ${did}) AND space_slug = ${spaceSlug}
    ORDER BY created_at DESC
    LIMIT ${limit}
  `;
  return rows.map(mapTrustEventRow);
}

export async function getTrustEventsSince(spaceSlug: string, since: number): Promise<StoredTrustEvent[]> {
  const rows = await sql`
    SELECT * FROM trust_events
    WHERE space_slug = ${spaceSlug} AND created_at >= ${new Date(since).toISOString()}
    ORDER BY created_at ASC
  `;
  return rows.map(mapTrustEventRow);
}

// ============================================================================
// TRUST SCORES (materialized)
// ============================================================================

export interface StoredTrustScore {
  sourceDid: string;
  targetDid: string;
  authority: string;
  spaceSlug: string;
  score: number;
  directWeight: number;
  transitiveWeight: number;
  lastComputed: number;
}

function mapTrustScoreRow(r: any): StoredTrustScore {
  return {
    sourceDid: r.source_did,
    targetDid: r.target_did,
    authority: r.authority,
    spaceSlug: r.space_slug,
    score: parseFloat(r.score),
    directWeight: parseFloat(r.direct_weight),
    transitiveWeight: parseFloat(r.transitive_weight),
    lastComputed: new Date(r.last_computed).getTime(),
  };
}

export async function upsertTrustScore(score: {
  sourceDid: string;
  targetDid: string;
  authority: string;
  spaceSlug: string;
  score: number;
  directWeight: number;
  transitiveWeight: number;
}): Promise<void> {
  await sql`
    INSERT INTO trust_scores (source_did, target_did, authority, space_slug, score, direct_weight, transitive_weight, last_computed)
    VALUES (${score.sourceDid}, ${score.targetDid}, ${score.authority}, ${score.spaceSlug},
            ${score.score}, ${score.directWeight}, ${score.transitiveWeight}, NOW())
    ON CONFLICT (source_did, target_did, authority, space_slug) DO UPDATE SET
      score = EXCLUDED.score,
      direct_weight = EXCLUDED.direct_weight,
      transitive_weight = EXCLUDED.transitive_weight,
      last_computed = NOW()
  `;
}

/** Get aggregated trust scores — total trust received by each user for an authority in a space */
export async function getAggregatedTrustScores(spaceSlug: string, authority: string): Promise<Array<{ did: string; totalScore: number; directScore: number; transitiveScore: number }>> {
  const rows = await sql`
    SELECT target_did,
           SUM(score) as total_score,
           SUM(direct_weight) as direct_score,
           SUM(transitive_weight) as transitive_score
    FROM trust_scores
    WHERE space_slug = ${spaceSlug} AND authority = ${authority}
    GROUP BY target_did
    ORDER BY total_score DESC
  `;
  return rows.map(r => ({
    did: r.target_did,
    totalScore: parseFloat(r.total_score),
    directScore: parseFloat(r.direct_score),
    transitiveScore: parseFloat(r.transitive_score),
  }));
}

/** Get trust scores for a specific user across all authorities */
export async function getTrustScoresByAuthority(did: string, spaceSlug: string): Promise<StoredTrustScore[]> {
  const rows = await sql`
    SELECT * FROM trust_scores
    WHERE target_did = ${did} AND space_slug = ${spaceSlug}
    ORDER BY authority
  `;
  return rows.map(mapTrustScoreRow);
}

/** List all users with trust metadata for a space (user directory) */
export async function listAllUsersWithTrust(spaceSlug: string): Promise<Array<{
  did: string;
  username: string;
  displayName: string | null;
  avatarUrl: string | null;
  role: string;
  trustScores: Record<string, number>;
}>> {
  const rows = await sql`
    SELECT u.did, u.username, u.display_name, u.avatar_url, sm.role,
           COALESCE(
             json_object_agg(ts.authority, ts.total) FILTER (WHERE ts.authority IS NOT NULL),
             '{}'
           ) as trust_scores
    FROM space_members sm
    JOIN users u ON u.did = sm.user_did
    LEFT JOIN (
      SELECT target_did, authority, SUM(score) as total
      FROM trust_scores
      WHERE space_slug = ${spaceSlug}
      GROUP BY target_did, authority
    ) ts ON ts.target_did = sm.user_did
    WHERE sm.space_slug = ${spaceSlug}
    GROUP BY u.did, u.username, u.display_name, u.avatar_url, sm.role
    ORDER BY u.username
  `;
  return rows.map(r => ({
    did: r.did,
    username: r.username,
    displayName: r.display_name || null,
    avatarUrl: r.avatar_url || null,
    role: r.role,
    trustScores: typeof r.trust_scores === 'string' ? JSON.parse(r.trust_scores) : (r.trust_scores || {}),
  }));
}

// ============================================================================
// LEGACY IDENTITY OPERATIONS (CryptID → EncryptID migration)
// ============================================================================

export interface StoredLegacyIdentity {
  id: string;
  userId: string;
  provider: 'cryptid';
  legacyUsername: string;
  legacyPublicKey: string;
  legacyPublicKeyHash: string;
  verified: boolean;
  migratedData: boolean;
  linkedAt: string;
  verifiedAt: string | null;
}

function rowToLegacyIdentity(row: any): StoredLegacyIdentity {
  return {
    id: row.id,
    userId: row.user_id,
    provider: row.provider,
    legacyUsername: row.legacy_username,
    legacyPublicKey: row.legacy_public_key,
    legacyPublicKeyHash: row.legacy_public_key_hash,
    verified: row.verified || false,
    migratedData: row.migrated_data || false,
    linkedAt: row.linked_at?.toISOString?.() || new Date(row.linked_at).toISOString(),
    verifiedAt: row.verified_at ? (row.verified_at?.toISOString?.() || new Date(row.verified_at).toISOString()) : null,
  };
}

export async function createLegacyIdentity(
  userId: string,
  identity: { id: string; provider: 'cryptid'; legacyUsername: string; legacyPublicKey: string; legacyPublicKeyHash: string },
): Promise<StoredLegacyIdentity> {
  const rows = await sql`
    INSERT INTO legacy_identities (id, user_id, provider, legacy_username, legacy_public_key, legacy_public_key_hash, verified)
    VALUES (${identity.id}, ${userId}, ${identity.provider}, ${identity.legacyUsername}, ${identity.legacyPublicKey}, ${identity.legacyPublicKeyHash}, TRUE)
    ON CONFLICT (provider, legacy_public_key_hash) DO UPDATE SET
      user_id = ${userId},
      legacy_username = ${identity.legacyUsername},
      legacy_public_key = ${identity.legacyPublicKey},
      verified = TRUE,
      verified_at = NOW()
    RETURNING *
  `;
  return rowToLegacyIdentity(rows[0]);
}

export async function getLegacyIdentityByPublicKeyHash(provider: string, hash: string): Promise<StoredLegacyIdentity | null> {
  const [row] = await sql`
    SELECT * FROM legacy_identities
    WHERE provider = ${provider} AND legacy_public_key_hash = ${hash}
  `;
  return row ? rowToLegacyIdentity(row) : null;
}

export async function getLegacyIdentitiesByUser(userId: string): Promise<StoredLegacyIdentity[]> {
  const rows = await sql`
    SELECT * FROM legacy_identities
    WHERE user_id = ${userId}
    ORDER BY linked_at ASC
  `;
  return rows.map(rowToLegacyIdentity);
}

export async function verifyLegacyIdentity(id: string): Promise<boolean> {
  const result = await sql`
    UPDATE legacy_identities
    SET verified = TRUE, verified_at = NOW()
    WHERE id = ${id}
  `;
  return result.count > 0;
}

export async function getUserByLegacyPublicKeyHash(provider: string, hash: string): Promise<{
  userId: string;
  username: string;
  did: string | null;
  legacyIdentityId: string;
} | null> {
  const [row] = await sql`
    SELECT u.id as user_id, u.username, u.did, li.id as legacy_identity_id
    FROM users u
    JOIN legacy_identities li ON li.user_id = u.id
    WHERE li.provider = ${provider} AND li.legacy_public_key_hash = ${hash} AND li.verified = TRUE
  `;
  if (!row) return null;
  return {
    userId: row.user_id,
    username: row.username,
    did: row.did || null,
    legacyIdentityId: row.legacy_identity_id,
  };
}

// ============================================================================
// UNIVERSAL PROFILE
// ============================================================================

export interface StoredUniversalProfile {
  upAddress: string;
  keyManagerAddress: string;
  chainId: number;
  deployedAt: Date;
}

export async function getUserUPAddress(userId: string): Promise<StoredUniversalProfile | null> {
  const [row] = await sql`
    SELECT up_address, up_address_enc, up_key_manager_address, up_chain_id, up_deployed_at
    FROM users WHERE id = ${userId} AND up_address IS NOT NULL
  `;
  if (!row) return null;
  const upDecrypted = await decryptField(row.up_address_enc);
  return {
    upAddress: upDecrypted ?? row.up_address,
    keyManagerAddress: row.up_key_manager_address,
    chainId: row.up_chain_id,
    deployedAt: new Date(row.up_deployed_at),
  };
}

export async function setUserUPAddress(
  userId: string,
  upAddress: string,
  keyManagerAddress: string,
  chainId: number,
): Promise<void> {
  const [upEnc, upHash] = await Promise.all([encryptField(upAddress), hashForLookup(upAddress)]);
  await sql`
    UPDATE users
    SET up_address = ${upAddress},
        up_address_enc = ${upEnc},
        up_address_hash = ${upHash},
        up_key_manager_address = ${keyManagerAddress},
        up_chain_id = ${chainId},
        up_deployed_at = NOW()
    WHERE id = ${userId}
  `;
}

export async function getUserByUPAddress(upAddress: string): Promise<{ userId: string; username: string } | null> {
  const hash = await hashForLookup(upAddress);
  // Try hash lookup first, fall back to plaintext for pre-migration rows
  const [row] = await sql`SELECT id, username FROM users WHERE up_address_hash = ${hash}`;
  if (row) return { userId: row.id, username: row.username };
  const [legacy] = await sql`SELECT id, username FROM users WHERE up_address = ${upAddress} AND up_address_hash IS NULL`;
  if (!legacy) return null;
  return { userId: legacy.id, username: legacy.username };
}

// ============================================================================
// SPACE EMAIL ALIAS OPERATIONS
// ============================================================================

export async function getSpaceEmailAlias(spaceSlug: string): Promise<{ spaceSlug: string; mailcowId: string } | null> {
  const [row] = await sql`SELECT * FROM space_email_aliases WHERE space_slug = ${spaceSlug}`;
  if (!row) return null;
  return { spaceSlug: row.space_slug, mailcowId: row.mailcow_id };
}

export async function setSpaceEmailAlias(spaceSlug: string, mailcowId: string): Promise<void> {
  await sql`
    INSERT INTO space_email_aliases (space_slug, mailcow_id)
    VALUES (${spaceSlug}, ${mailcowId})
    ON CONFLICT (space_slug)
    DO UPDATE SET mailcow_id = ${mailcowId}
  `;
}

export async function deleteSpaceEmailAlias(spaceSlug: string): Promise<boolean> {
  const result = await sql`DELETE FROM space_email_aliases WHERE space_slug = ${spaceSlug}`;
  return result.count > 0;
}

export async function upsertSpaceEmailForwarding(spaceSlug: string, userDid: string, optIn: boolean): Promise<void> {
  await sql`
    INSERT INTO space_email_forwarding (space_slug, user_did, opt_in, updated_at)
    VALUES (${spaceSlug}, ${userDid}, ${optIn}, NOW())
    ON CONFLICT (space_slug, user_did)
    DO UPDATE SET opt_in = ${optIn}, updated_at = NOW()
  `;
}

export async function removeSpaceEmailForwarding(spaceSlug: string, userDid: string): Promise<boolean> {
  const result = await sql`DELETE FROM space_email_forwarding WHERE space_slug = ${spaceSlug} AND user_did = ${userDid}`;
  return result.count > 0;
}

export async function getOptedInDids(spaceSlug: string): Promise<string[]> {
  const rows = await sql`
    SELECT user_did FROM space_email_forwarding
    WHERE space_slug = ${spaceSlug} AND opt_in = TRUE
  `;
  return rows.map((r) => r.user_did);
}

export async function getSpaceEmailForwarding(spaceSlug: string, userDid: string): Promise<{ optIn: boolean } | null> {
  const [row] = await sql`
    SELECT opt_in FROM space_email_forwarding
    WHERE space_slug = ${spaceSlug} AND user_did = ${userDid}
  `;
  if (!row) return null;
  return { optIn: row.opt_in };
}

/**
 * Batch DID→profileEmail lookup. Joins users table on did column,
 * decrypts profile_email_enc for each match.
 */
export async function getProfileEmailsByDids(dids: string[]): Promise<Map<string, string>> {
  if (dids.length === 0) return new Map();
  const rows = await sql`
    SELECT did, profile_email, profile_email_enc FROM users
    WHERE did = ANY(${dids})
  `;
  const result = new Map<string, string>();
  for (const row of rows) {
    const email = row.profile_email_enc
      ? await decryptField(row.profile_email_enc)
      : row.profile_email;
    if (email) result.set(row.did, email);
  }
  return result;
}

// ============================================================================
// AGENT MAILBOX OPERATIONS
// ============================================================================

export async function getAgentMailbox(spaceSlug: string): Promise<{ email: string; password: string } | null> {
  const [row] = await sql`SELECT email, password FROM agent_mailboxes WHERE space_slug = ${spaceSlug}`;
  if (!row) return null;
  return { email: row.email, password: row.password };
}

export async function setAgentMailbox(spaceSlug: string, email: string, password: string): Promise<void> {
  await sql`
    INSERT INTO agent_mailboxes (space_slug, email, password)
    VALUES (${spaceSlug}, ${email}, ${password})
    ON CONFLICT (space_slug)
    DO UPDATE SET email = ${email}, password = ${password}
  `;
}

export async function deleteAgentMailbox(spaceSlug: string): Promise<boolean> {
  const result = await sql`DELETE FROM agent_mailboxes WHERE space_slug = ${spaceSlug}`;
  return result.count > 0;
}

export async function listAllAgentMailboxes(): Promise<Array<{ spaceSlug: string; email: string; password: string }>> {
  const rows = await sql`SELECT space_slug, email, password FROM agent_mailboxes`;
  return rows.map((r) => ({ spaceSlug: r.space_slug, email: r.email, password: r.password }));
}

// ============================================================================
// POWER INDICES (Banzhaf, Shapley-Shubik coalitional power)
// ============================================================================

export interface StoredPowerIndex {
  did: string;
  spaceSlug: string;
  authority: string;
  rawWeight: number;
  banzhaf: number;
  shapleyShubik: number;
  swingCount: number;
  pivotalCount: number;
  giniCoefficient: number;
  herfindahlIndex: number;
  lastComputed: number;
}

function mapPowerIndexRow(r: any): StoredPowerIndex {
  return {
    did: r.did,
    spaceSlug: r.space_slug,
    authority: r.authority,
    rawWeight: parseFloat(r.raw_weight),
    banzhaf: parseFloat(r.banzhaf),
    shapleyShubik: parseFloat(r.shapley_shubik),
    swingCount: parseInt(r.swing_count),
    pivotalCount: parseInt(r.pivotal_count),
    giniCoefficient: parseFloat(r.gini_coefficient),
    herfindahlIndex: parseFloat(r.herfindahl_index),
    lastComputed: new Date(r.last_computed).getTime(),
  };
}

export async function upsertPowerIndex(idx: {
  did: string;
  spaceSlug: string;
  authority: string;
  rawWeight: number;
  banzhaf: number;
  shapleyShubik: number;
  swingCount: number;
  pivotalCount: number;
  giniCoefficient: number;
  herfindahlIndex: number;
}): Promise<void> {
  await sql`
    INSERT INTO power_indices (did, space_slug, authority, raw_weight, banzhaf, shapley_shubik, swing_count, pivotal_count, gini_coefficient, herfindahl_index, last_computed)
    VALUES (${idx.did}, ${idx.spaceSlug}, ${idx.authority}, ${idx.rawWeight}, ${idx.banzhaf}, ${idx.shapleyShubik}, ${idx.swingCount}, ${idx.pivotalCount}, ${idx.giniCoefficient}, ${idx.herfindahlIndex}, NOW())
    ON CONFLICT (did, space_slug, authority) DO UPDATE SET
      raw_weight = EXCLUDED.raw_weight,
      banzhaf = EXCLUDED.banzhaf,
      shapley_shubik = EXCLUDED.shapley_shubik,
      swing_count = EXCLUDED.swing_count,
      pivotal_count = EXCLUDED.pivotal_count,
      gini_coefficient = EXCLUDED.gini_coefficient,
      herfindahl_index = EXCLUDED.herfindahl_index,
      last_computed = NOW()
  `;
}

export async function getPowerIndices(spaceSlug: string, authority: string): Promise<StoredPowerIndex[]> {
  const rows = await sql`
    SELECT * FROM power_indices
    WHERE space_slug = ${spaceSlug} AND authority = ${authority}
    ORDER BY banzhaf DESC
  `;
  return rows.map(mapPowerIndexRow);
}

export async function getPowerIndex(did: string, spaceSlug: string): Promise<StoredPowerIndex[]> {
  const rows = await sql`
    SELECT * FROM power_indices
    WHERE did = ${did} AND space_slug = ${spaceSlug}
    ORDER BY authority
  `;
  return rows.map(mapPowerIndexRow);
}

export { sql };
