import { and, desc, eq, isNull } from 'drizzle-orm';
import argon2 from 'argon2';
import { randomBytes } from 'node:crypto';
import { db } from '../db';
import { davAppPasswords } from './schema';

export type DavAppPassword = typeof davAppPasswords.$inferSelect;

/** What the UI is allowed to see: everything except the hash. */
export type DavAppPasswordView = Omit<DavAppPassword, 'passwordHash'>;

const view = (row: DavAppPassword): DavAppPasswordView => {
  const { passwordHash: _hash, ...rest } = row;
  return rest;
};

// Base32-ish over an unambiguous alphabet: no 0/O/1/I/l, because this gets read off a screen and typed
// into a phone by hand. Grouped into blocks of four for the same reason.
const ALPHABET = 'abcdefghjkmnpqrstuvwxyz23456789';

function generateSecret(): string {
  const bytes = randomBytes(20);
  // 20 bytes over a 31-char alphabet ≈ 99 bits. Well past anything Basic auth over TLS needs, and it
  // still fits in five readable blocks.
  const chars = Array.from(bytes, (b) => ALPHABET[b % ALPHABET.length]).join('');
  return (chars.match(/.{1,4}/g) ?? [chars]).join('-');
}

export async function listDavAppPasswords(userId: number): Promise<DavAppPasswordView[]> {
  const rows = await db
    .select()
    .from(davAppPasswords)
    .where(eq(davAppPasswords.userId, userId))
    .orderBy(desc(davAppPasswords.createdAt));
  return rows.map(view);
}

/**
 * Mint a credential. The plaintext is returned HERE AND NOWHERE ELSE — it is not stored, so this return
 * value is the only chance the owner gets to see it.
 */
export async function createDavAppPassword(
  userId: number,
  label: string,
): Promise<{ entry: DavAppPasswordView; password: string }> {
  const password = generateSecret();
  const [row] = await db
    .insert(davAppPasswords)
    .values({
      userId,
      label,
      passwordHash: await argon2.hash(password),
      hint: password.slice(0, 8),
    })
    .returning();
  if (!row) throw new Error('failed to create dav app password');
  return { entry: view(row), password };
}

export async function revokeDavAppPassword(userId: number, id: number): Promise<boolean> {
  const [row] = await db
    .update(davAppPasswords)
    .set({ revokedAt: new Date() })
    .where(and(eq(davAppPasswords.id, id), eq(davAppPasswords.userId, userId), isNull(davAppPasswords.revokedAt)))
    .returning();
  return !!row;
}

export async function deleteDavAppPassword(userId: number, id: number): Promise<boolean> {
  const [row] = await db
    .delete(davAppPasswords)
    .where(and(eq(davAppPasswords.id, id), eq(davAppPasswords.userId, userId)))
    .returning();
  return !!row;
}

/**
 * Verify a Basic-auth credential, returning the owning user id.
 *
 * Every live credential is tried, because the username a DAV client sends is the account email, not a
 * row id — there is nothing in the request that says WHICH device is calling. That means the cost is
 * one argon2 verify per stored credential, which is why revoked rows are filtered in SQL and why the
 * UI should encourage deleting devices that are gone rather than accumulating them.
 *
 * `lastUsedAt` is written on success, at most once a minute: a syncing phone hits this constantly and
 * the column exists to answer "is this device still around", not to be an access log.
 */
export async function verifyDavAppPassword(userId: number, password: string): Promise<number | null> {
  const rows = await db
    .select()
    .from(davAppPasswords)
    .where(and(eq(davAppPasswords.userId, userId), isNull(davAppPasswords.revokedAt)));

  for (const row of rows) {
    let ok = false;
    try {
      ok = await argon2.verify(row.passwordHash, password);
    } catch {
      ok = false; // a corrupt hash must not take the whole login path down
    }
    if (!ok) continue;

    const now = Date.now();
    if (!row.lastUsedAt || now - row.lastUsedAt.getTime() > 60_000) {
      await db
        .update(davAppPasswords)
        .set({ lastUsedAt: new Date(now) })
        .where(eq(davAppPasswords.id, row.id));
    }
    return row.userId;
  }
  return null;
}
