/**
 * 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';

// ============================================================================
// 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;
}

export interface StoredChallenge {
  challenge: string;
  userId?: string;
  type: 'registration' | 'authentication' | 'device_registration' | 'wallet_link';
  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): Promise<void> {
  // Ensure user exists first
  await createUser(cred.userId, cred.username);

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

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, 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,
  }));
}

// ============================================================================
// 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> {
  await sql`UPDATE users SET email = ${email} WHERE id = ${userId}`;
}

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

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

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

export interface StoredRecoveryToken {
  token: string;
  userId: string;
  type: 'email_verify' | 'account_recovery';
  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',
    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;
}

function rowToGuardian(row: any): StoredGuardian {
  return {
    id: row.id,
    userId: row.user_id,
    name: row.name,
    email: 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 rows = await sql`
    INSERT INTO guardians (id, user_id, name, email, invite_token, invite_expires_at)
    VALUES (${id}, ${userId}, ${name}, ${email}, ${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 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 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 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 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;
}

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,
  };
}

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 = 'pending' 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 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;
}

function rowToProfile(row: any): StoredUserProfile {
  return {
    userId: row.id,
    username: row.username,
    displayName: row.display_name || null,
    bio: row.bio || null,
    avatarUrl: row.avatar_url || null,
    profileEmail: row.profile_email || null,
    profileEmailIsRecovery: row.profile_email_is_recovery || false,
    did: row.did || null,
    walletAddress: 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 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); }
  if (updates.avatarUrl !== undefined) { sets.push('avatar_url'); values.push(updates.avatarUrl); }
  if (updates.profileEmail !== undefined) { sets.push('profile_email'); values.push(updates.profileEmail); }
  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); }

  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, email_forward_enabled, email_forward_mailcow_id
    FROM users WHERE id = ${userId}
  `;
  if (!row) return null;
  return {
    enabled: row.email_forward_enabled || false,
    mailcowId: row.email_forward_mailcow_id || null,
    username: row.username,
    profileEmail: 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.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 rows.map(row => ({
    userId: row.id,
    username: row.username,
    displayName: row.display_name || null,
    did: row.did || null,
    email: 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;
}

function rowToInvite(row: any): StoredSpaceInvite {
  return {
    id: row.id,
    spaceSlug: row.space_slug,
    email: 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 rows = await sql`
    INSERT INTO space_invites (id, space_slug, email, role, token, invited_by, expires_at)
    VALUES (${id}, ${spaceSlug}, ${email || null}, ${role}, ${token}, ${invitedBy}, ${new Date(expiresAt)})
    RETURNING *
  `;
  return 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 rowToInvite(rows[0]);
}

export async function listSpaceInvites(spaceSlug: string): Promise<StoredSpaceInvite[]> {
  const rows = await sql`
    SELECT * FROM space_invites
    WHERE space_slug = ${spaceSlug}
    ORDER BY created_at DESC
  `;
  return rows.map(rowToInvite);
}

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 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'
  `;
  return result.count > 0;
}

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

export interface StoredNotification {
  id: string;
  userDid: string;
  category: 'space' | 'module' | 'system' | 'social';
  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;
}

function rowToNotification(row: any): StoredNotification {
  return {
    id: row.id,
    userDid: row.user_did,
    category: row.category,
    eventType: row.event_type,
    title: row.title,
    body: row.body || null,
    spaceSlug: row.space_slug || null,
    moduleId: row.module_id || null,
    actionUrl: row.action_url || null,
    actorDid: row.actor_did || null,
    actorUsername: 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';
  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 rows = await sql`
    INSERT INTO notifications (id, user_did, category, event_type, title, body, 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},
      ${notif.spaceSlug || null},
      ${notif.moduleId || null},
      ${notif.actionUrl || null},
      ${notif.actorDid || null},
      ${notif.actorUsername || null},
      ${JSON.stringify(notif.metadata || {})},
      ${notif.expiresAt || null}
    )
    RETURNING *
  `;
  return 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 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;
}

function rowToFundClaim(row: any): StoredFundClaim {
  return {
    id: row.id,
    token: row.token,
    emailHash: row.email_hash,
    email: row.email || null,
    walletAddress: 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 rows = await sql`
    INSERT INTO fund_claims (id, token, email_hash, email, wallet_address, openfort_player_id, fiat_amount, fiat_currency, session_id, provider, expires_at)
    VALUES (
      ${claim.id},
      ${claim.token},
      ${claim.emailHash},
      ${claim.email},
      ${claim.walletAddress},
      ${claim.openfortPlayerId || null},
      ${claim.fiatAmount || null},
      ${claim.fiatCurrency || 'USD'},
      ${claim.sessionId || null},
      ${claim.provider || null},
      ${new Date(claim.expiresAt)}
    )
    RETURNING *
  `;
  return 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 rowToFundClaim(rows[0]);
}

export async function getFundClaimsByEmailHash(emailHash: string): Promise<StoredFundClaim[]> {
  const rows = await sql`
    SELECT * FROM fund_claims
    WHERE email_hash = ${emailHash} AND status IN ('pending', 'resent') AND expires_at > NOW()
    ORDER BY created_at DESC
  `;
  return 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
    WHERE token = ${token} AND status IN ('pending', 'resent') AND expires_at > NOW()
    RETURNING *
  `;
  if (rows.length === 0) return null;
  return 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 rowToFundClaim(rows[0]);
}

export async function expireFundClaim(claimId: string): Promise<void> {
  await sql`UPDATE fund_claims SET status = 'expired', email = 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, 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;
}

function mapInviteRow(r: any): StoredIdentityInvite {
  return {
    id: r.id,
    token: r.token,
    email: r.email,
    invitedByUserId: r.invited_by_user_id,
    invitedByUsername: r.invited_by_username,
    message: r.message,
    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 rows = await sql`
    INSERT INTO identity_invites (id, token, email, invited_by_user_id, invited_by_username, message, space_slug, space_role, client_id, expires_at)
    VALUES (${invite.id}, ${invite.token}, ${invite.email}, ${invite.invitedByUserId},
            ${invite.invitedByUsername}, ${invite.message || null},
            ${invite.spaceSlug || null}, ${invite.spaceRole || 'member'},
            ${invite.clientId || null},
            ${new Date(invite.expiresAt).toISOString()})
    RETURNING *
  `;
  return 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 ? mapInviteRow(rows[0]) : null;
}

export async function getIdentityInvitesByEmail(email: string): Promise<StoredIdentityInvite[]> {
  const rows = await sql`SELECT * FROM identity_invites WHERE email = ${email} ORDER BY created_at DESC`;
  return 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 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 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 ? 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}`;
}

export { sql };
