import { eq, and, lt, sql } from 'drizzle-orm';
import { db } from '../db';
import { users, passkeys, passkeyChallenges, tokenBlacklist, OWNER_USER_ID } from './schema';
import type { UserSelect, UserInsert, PasskeySelect, PasskeyInsert } from '../types';

// ── Users ──

export async function getUsers(): Promise<UserSelect[]> {
  return db.select().from(users);
}

export async function getUserById(id: number): Promise<UserSelect | undefined> {
  const [user] = await db.select().from(users).where(eq(users.id, id));
  return user;
}

export async function getUserByEmail(email: string): Promise<UserSelect | undefined> {
  const [user] = await db.select().from(users).where(eq(users.email, email));
  return user;
}

// `username` is unique in the schema, so this exists to turn a would-be constraint violation into a
// sentence. Creating an account is a form someone fills in, and "duplicate key value violates unique
// constraint users_username_unique" is not an answer to give them.
export async function getUserByUsername(username: string): Promise<UserSelect | undefined> {
  const [user] = await db.select().from(users).where(eq(users.username, username));
  return user;
}

// The owner: the one account with role 'Super Admin'. Sidecars that need "who is the owner" (e.g. the
// agent sidecar, which PM2 starts with no email in its env) resolve it here rather than being told by
// the main server.
//
// This was `order by id limit 1` — the first account created. That was the same claim the role column
// now makes, asserted a second way, and the two would part company the moment the owner was not user
// #1. Returns undefined rather than falling back to the lowest id when no row holds the role: the agent
// sidecar refusing to start beats it silently running as the wrong person.
//
// `order by id` is not cosmetic. Without it, two rows holding the role would make "who owns this
// server" whatever Postgres happened to return first — and that answer feeds the agent sidecar's
// identity, the vault and origin scoping. The write paths refuse to create a second Super Admin
// (create-user.ts and updateUserRoleHandler), so this should never have a choice to make; ordering is
// what makes the outcome deterministic if one ever gets in by another route, and id 1 is the bootstrap
// account the CHECK constraint already pins.
export async function getOwnerUser(): Promise<UserSelect | undefined> {
  const [user] = await db.select().from(users).where(eq(users.role, 'Super Admin')).orderBy(users.id).limit(1);
  return user;
}

export async function getUserCount(): Promise<number> {
  const [result] = await db.select({ count: sql<number>`count(*)::int` }).from(users);
  return result?.count ?? 0;
}

export async function createUser(data: UserInsert): Promise<UserSelect> {
  const [user] = await db.insert(users).values(data).returning();
  return user!;
}

export async function updateUser(id: number, data: Partial<Omit<UserSelect, 'id'>>): Promise<UserSelect | undefined> {
  const [user] = await db
    .update(users)
    .set({ ...data, updatedAt: new Date() })
    .where(eq(users.id, id))
    .returning();
  return user;
}

// The owner cannot be deleted. ck_users_owner_is_super_admin pins user 1's role, but a row-level CHECK
// cannot stop a DELETE, and deleting the owner is the same outcome by another route: a platform with
// nobody who can reach the vault, no identity for the agent sidecar to run as, and a web origin
// restricted to a Super Admin that no longer exists. Cascades would take the passkeys with it, so there
// is no signing back in either.
//
// Throws rather than returning false. This has no callers today; the one it will get is a
// manage-users flow, and a silent `false` there reads as "already gone".
export async function deleteUser(id: number): Promise<boolean> {
  if (id === OWNER_USER_ID) {
    throw new Error(`Refusing to delete user ${OWNER_USER_ID}: the platform owner cannot be removed.`);
  }
  const result = await db.delete(users).where(eq(users.id, id)).returning({ id: users.id });
  return result.length > 0;
}

// ── Passkeys ──

export async function getPasskeysByUserId(userId: number): Promise<PasskeySelect[]> {
  return db.select().from(passkeys).where(eq(passkeys.userId, userId));
}

export async function getPasskeysByUserIdAndOrigin(userId: number, origin: string): Promise<PasskeySelect[]> {
  return db
    .select()
    .from(passkeys)
    .where(and(eq(passkeys.userId, userId), eq(passkeys.origin, origin)));
}

export async function getPasskeyByCredentialId(
  userId: number,
  credentialId: string,
): Promise<PasskeySelect | undefined> {
  const [passkey] = await db
    .select()
    .from(passkeys)
    .where(and(eq(passkeys.userId, userId), eq(passkeys.credentialId, credentialId)));
  return passkey;
}

export async function createPasskey(data: PasskeyInsert): Promise<PasskeySelect> {
  const [passkey] = await db.insert(passkeys).values(data).returning();
  return passkey!;
}

export async function updatePasskey(
  id: number,
  data: Partial<Omit<PasskeySelect, 'id'>>,
): Promise<PasskeySelect | undefined> {
  const [passkey] = await db.update(passkeys).set(data).where(eq(passkeys.id, id)).returning();
  return passkey;
}

// ── Passkey Challenges ──

export async function storeChallenge(userId: number, origin: string, challenge: string, ttlMs: number) {
  const expiresAt = new Date(Date.now() + ttlMs);

  // Upsert: delete existing challenge for this user+origin, then insert
  await db
    .delete(passkeyChallenges)
    .where(and(eq(passkeyChallenges.userId, userId), eq(passkeyChallenges.origin, origin)));
  await db.insert(passkeyChallenges).values({ userId, origin, challenge, expiresAt });
}

export async function consumeChallenge(userId: number, origin: string): Promise<string | null> {
  const now = new Date();

  // Clean up expired challenges
  await db.delete(passkeyChallenges).where(lt(passkeyChallenges.expiresAt, now));

  // Find and delete the matching challenge
  const [entry] = await db
    .delete(passkeyChallenges)
    .where(and(eq(passkeyChallenges.userId, userId), eq(passkeyChallenges.origin, origin)))
    .returning();

  if (!entry) return null;
  if (entry.expiresAt < now) return null;
  return entry.challenge;
}

// ── Token Blacklist ──

export async function blacklistToken(jti: string, expiresAt: number) {
  // expiresAt comes as Unix seconds from JWT exp claim
  const expiresDate = new Date(expiresAt * 1000);
  await db.insert(tokenBlacklist).values({ jti, expiresAt: expiresDate }).onConflictDoNothing();
}

export async function isTokenBlacklisted(jti: string): Promise<boolean> {
  const [entry] = await db.select({ jti: tokenBlacklist.jti }).from(tokenBlacklist).where(eq(tokenBlacklist.jti, jti));
  return !!entry;
}

export async function cleanupExpiredTokens() {
  await db.delete(tokenBlacklist).where(lt(tokenBlacklist.expiresAt, new Date()));
}
