import {randomBytes} from 'node:crypto'

import {sha256} from '../lib/hash'
import {redis} from '../lib/redis'

const PREFIX = 'magic:'
// no collision with `magic:<64-hex>` token keys
const EMAIL_PTR_PREFIX = 'magic:email:'
const TTL_SECONDS = 600

const generate = () => randomBytes(32).toString('base64url')

export const issueMagicToken = async (
  email: string,
): Promise<string> => {
  const raw = generate()
  const hash = sha256(raw)
  const ptrKey = `${EMAIL_PTR_PREFIX}${sha256(email)}`
  // a reissue invalidates any outstanding token for the same email
  const prevHash = await redis.getDel(ptrKey)
  if (prevHash) await redis.del(`${PREFIX}${prevHash}`)
  await redis
    .multi()
    .set(`${PREFIX}${hash}`, email, {EX: TTL_SECONDS})
    .set(ptrKey, hash, {EX: TTL_SECONDS})
    .exec()
  return raw
}

// non-destructive: lets GET render the confirm page without burning the token
export const peekMagicToken = async (
  raw: string,
): Promise<boolean> => {
  const email = await redis.get(`${PREFIX}${sha256(raw)}`)
  return email !== null
}

export const consumeMagicToken = async (
  raw: string,
): Promise<string | null> => {
  const email = await redis.getDel(`${PREFIX}${sha256(raw)}`)
  return email ?? null
}

// the deep link carries a short-lived single-use code instead of the
// 30-day session token, so an intercepted link is worthless after
// first use or expiry
const EXCHANGE_PREFIX = 'exchange:'
const EXCHANGE_TTL_SECONDS = 60

export const issueExchangeCode = async (
  sessionToken: string,
): Promise<string> => {
  const raw = generate()
  await redis.set(`${EXCHANGE_PREFIX}${sha256(raw)}`, sessionToken, {
    EX: EXCHANGE_TTL_SECONDS,
  })
  return raw
}

export const consumeExchangeCode = async (
  raw: string,
): Promise<string | null> => {
  const token = await redis.getDel(`${EXCHANGE_PREFIX}${sha256(raw)}`)
  return token ?? null
}
