import { createHmac, randomBytes, randomUUID, scrypt, timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";
import type { Store, UserRecord } from "./db/store.js";

/**
 * Password hashing and session tokens.
 *
 * Real scrypt and real HMAC even though the store is in-memory: getting these
 * wrong is the kind of thing that survives into production once a Neon
 * connection string appears, and there is no cost to doing it properly now.
 */

const scryptAsync = promisify(scrypt) as (
  password: string,
  salt: string,
  keylen: number,
) => Promise<Buffer>;

const SCRYPT_KEYLEN = 64;
const TOKEN_TTL_MS = 12 * 60 * 60 * 1000;

/**
 * Dev-only fallback secret. Regenerated per process, so restarting invalidates
 * every outstanding token rather than signing with a known constant.
 */
const SECRET = process.env["PSX_SESSION_SECRET"] ?? randomBytes(32).toString("hex");

export async function hashPassword(password: string): Promise<string> {
  const salt = randomBytes(16).toString("hex");
  const derived = await scryptAsync(password, salt, SCRYPT_KEYLEN);
  return `${salt}:${derived.toString("hex")}`;
}

export async function verifyPassword(password: string, stored: string): Promise<boolean> {
  const [salt, expected] = stored.split(":");
  if (!salt || !expected) return false;

  const derived = await scryptAsync(password, salt, SCRYPT_KEYLEN);
  const expectedBuffer = Buffer.from(expected, "hex");
  // Length check first: timingSafeEqual throws on mismatched lengths.
  if (expectedBuffer.length !== derived.length) return false;
  return timingSafeEqual(derived, expectedBuffer);
}

interface TokenPayload {
  sub: string;
  exp: number;
  jti: string;
}

const sign = (data: string): string =>
  createHmac("sha256", SECRET).update(data).digest("base64url");

/** Compact signed token. Not a full JWT — there is no need for the header. */
export function issueToken(userId: string): string {
  const payload: TokenPayload = {
    sub: userId,
    exp: Date.now() + TOKEN_TTL_MS,
    jti: randomUUID(),
  };
  const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
  return `${body}.${sign(body)}`;
}

export function verifyToken(token: string): { userId: string } | null {
  const [body, signature] = token.split(".");
  if (!body || !signature) return null;

  const expected = sign(body);
  const givenBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);
  if (givenBuffer.length !== expectedBuffer.length) return null;
  if (!timingSafeEqual(givenBuffer, expectedBuffer)) return null;

  try {
    const payload = JSON.parse(Buffer.from(body, "base64url").toString()) as TokenPayload;
    if (typeof payload.sub !== "string" || typeof payload.exp !== "number") return null;
    if (payload.exp < Date.now()) return null;
    return { userId: payload.sub };
  } catch {
    return null;
  }
}

/** Resolves the caller from an `Authorization: Bearer …` header. */
export async function userFromAuthHeader(
  store: Store,
  header: string | undefined,
): Promise<UserRecord | null> {
  if (!header?.startsWith("Bearer ")) return null;
  const verified = verifyToken(header.slice(7));
  return verified ? store.findUserById(verified.userId) : null;
}
