import { eq, and, isNull, sql } from 'drizzle-orm';
import { db } from '../db';
import { apiKeys } from './schema';
import { users } from '../auth/schema';
import type { ApiKeySelect } from '../types';

// Every read here is scoped by userId except `findLiveApiKeyByHash`, which cannot be: authentication is
// the one call that does not yet know who is asking. That is what the unique index on key_hash is for.

/** What the resolver needs to turn a key into a caller. Joined so authentication is a single round trip. */
export type ApiKeyIdentity = {
  keyId: number;
  userId: number;
  email: string;
  username: string | null;
  lastUsedAt: Date | null;
};

/**
 * Look a key up by the hash of its secret, for authentication.
 *
 * Filters revoked and expired rows in SQL rather than returning them for the caller to check — a
 * forgotten check at a call site is an accepted credential, so there is deliberately no way to ask this
 * function for a dead key.
 */
export async function findLiveApiKeyByHash(keyHash: string): Promise<ApiKeyIdentity | null> {
  const [row] = await db
    .select({
      keyId: apiKeys.id,
      userId: apiKeys.userId,
      email: users.email,
      username: users.username,
      lastUsedAt: apiKeys.lastUsedAt,
    })
    .from(apiKeys)
    .innerJoin(users, eq(users.id, apiKeys.userId))
    .where(
      and(
        eq(apiKeys.keyHash, keyHash),
        isNull(apiKeys.revokedAt),
        sql`(${apiKeys.expiresAt} IS NULL OR ${apiKeys.expiresAt} > now())`,
      ),
    )
    .limit(1);
  return row ?? null;
}

type CreateApiKeyParams = {
  userId: number;
  name: string;
  keyHash: string;
  prefix: string;
  expiresAt?: Date | null;
};

export async function createApiKey(params: CreateApiKeyParams): Promise<ApiKeySelect> {
  const [row] = await db
    .insert(apiKeys)
    .values({
      userId: params.userId,
      name: params.name,
      keyHash: params.keyHash,
      prefix: params.prefix,
      expiresAt: params.expiresAt ?? null,
    })
    .returning();
  return row!;
}

/** The caller's own keys, newest first. Never returns key_hash — nothing outside authentication needs it. */
export async function listApiKeys(userId: number): Promise<Omit<ApiKeySelect, 'keyHash'>[]> {
  return db
    .select({
      id: apiKeys.id,
      userId: apiKeys.userId,
      name: apiKeys.name,
      prefix: apiKeys.prefix,
      lastUsedAt: apiKeys.lastUsedAt,
      expiresAt: apiKeys.expiresAt,
      revokedAt: apiKeys.revokedAt,
      createdAt: apiKeys.createdAt,
    })
    .from(apiKeys)
    .where(eq(apiKeys.userId, userId))
    .orderBy(sql`${apiKeys.createdAt} desc`);
}

/**
 * Revoke one of the caller's own keys. Scoped by userId in the same statement as the id, so a caller
 * naming someone else's key id revokes nothing and is told the same "not found" as for an id that never
 * existed — the row count, not a separate ownership read that could drift from this one.
 */
export async function revokeApiKey(id: number, userId: number): Promise<boolean> {
  const rows = await db
    .update(apiKeys)
    .set({ revokedAt: new Date() })
    .where(and(eq(apiKeys.id, id), eq(apiKeys.userId, userId), isNull(apiKeys.revokedAt)))
    .returning({ id: apiKeys.id });
  return rows.length > 0;
}

/** How stale last_used_at is allowed to get. A write per request is not worth this column's value. */
const TOUCH_DEBOUNCE_MS = 60_000;

/**
 * Record that a key was used, at most once a minute per key.
 *
 * Callers pass the `lastUsedAt` they already read during authentication, so the common case costs no
 * extra query — and this deliberately does not await anything the request depends on.
 */
export async function touchApiKey(id: number, lastUsedAt: Date | null): Promise<void> {
  if (lastUsedAt && Date.now() - lastUsedAt.getTime() < TOUCH_DEBOUNCE_MS) return;
  await db.update(apiKeys).set({ lastUsedAt: new Date() }).where(eq(apiKeys.id, id));
}
