'use strict';

import type { Db } from '../types/db';

const crypto = require('crypto');
const { generateSecret, generateURI, verifySync } = require('otplib');
const QRCode = require('qrcode');
const { encryptSecret, decryptSecret } = require('./encryptionService.cts');

const APP_NAME = 'Bill Tracker';
const RECOVERY_CODE_COUNT = 8;
const CHALLENGE_TTL_MS = 5 * 60 * 1000;

function newSecret(): string {
  return generateSecret(20);
}

async function generateQrCode(
  secret: string,
  username: string,
): Promise<{ uri: string; qr_data_url: string }> {
  const uri = generateURI({ secret, account: username, issuer: APP_NAME, type: 'totp' });
  const qr_data_url = await QRCode.toDataURL(uri, { width: 200, margin: 2 });
  return { uri, qr_data_url };
}

function verifyToken(
  encryptedSecret: string | null | undefined,
  token: string | number | null | undefined,
): boolean {
  if (!encryptedSecret || !token) return false;
  try {
    const secret = decryptSecret(encryptedSecret);
    const result = verifySync({ secret, token: String(token).replace(/\s/g, ''), type: 'totp' });
    return result?.valid === true;
  } catch {
    return false;
  }
}

function verifyTokenRaw(
  secret: string | null | undefined,
  token: string | number | null | undefined,
): boolean {
  if (!secret || !token) return false;
  try {
    const result = verifySync({ secret, token: String(token).replace(/\s/g, ''), type: 'totp' });
    return result?.valid === true;
  } catch {
    return false;
  }
}

function makeRecoveryCodes(): string[] {
  return Array.from({ length: RECOVERY_CODE_COUNT }, () => {
    const bytes = crypto.randomBytes(5);
    const hex = bytes.toString('hex').toUpperCase();
    return `${hex.slice(0, 5)}-${hex.slice(5)}`;
  });
}

function hashRecoveryCode(code: string): string {
  return crypto.createHash('sha256').update(code.replace(/-/g, '').toUpperCase()).digest('hex');
}

function consumeRecoveryCode(
  db: Db,
  userId: number,
  code: string,
): { used: boolean; remaining?: number } {
  const normalized = code.replace(/[-\s]/g, '').toUpperCase();
  const user = db.prepare('SELECT totp_recovery_codes FROM users WHERE id = ?').get(userId);
  if (!user?.totp_recovery_codes) return { used: false };

  let stored: string[];
  try {
    stored = JSON.parse(decryptSecret(user.totp_recovery_codes));
  } catch {
    return { used: false };
  }

  const incomingHash = crypto.createHash('sha256').update(normalized).digest('hex');
  const idx = stored.findIndex((h) => h === incomingHash);
  if (idx === -1) return { used: false };

  stored.splice(idx, 1);
  db.prepare('UPDATE users SET totp_recovery_codes = ? WHERE id = ?').run(
    encryptSecret(JSON.stringify(stored)),
    userId,
  );

  return { used: true, remaining: stored.length };
}

function createChallenge(db: Db, userId: number): string {
  const id = crypto.randomUUID();
  const expiresAt = new Date(Date.now() + CHALLENGE_TTL_MS)
    .toISOString()
    .slice(0, 19)
    .replace('T', ' ');
  db.prepare('DELETE FROM totp_challenges WHERE user_id = ?').run(userId);
  db.prepare('INSERT INTO totp_challenges (id, user_id, expires_at) VALUES (?, ?, ?)').run(
    id,
    userId,
    expiresAt,
  );
  return id;
}

function consumeChallenge(db: Db, challengeId: string): number | null {
  const row = db
    .prepare("SELECT user_id FROM totp_challenges WHERE id = ? AND expires_at > datetime('now')")
    .get(challengeId);
  if (!row) return null;
  db.prepare('DELETE FROM totp_challenges WHERE id = ?').run(challengeId);
  return row.user_id;
}

function pruneExpiredChallenges(db: Db): void {
  db.prepare("DELETE FROM totp_challenges WHERE expires_at <= datetime('now')").run();
}

module.exports = {
  generateSecret: newSecret,
  generateQrCode,
  verifyToken,
  verifyTokenRaw,
  generateRecoveryCodes: makeRecoveryCodes,
  hashRecoveryCode,
  consumeRecoveryCode,
  createChallenge,
  consumeChallenge,
  pruneExpiredChallenges,
};
