'use strict';

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

const crypto = require('crypto');
const {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} = require('@simplewebauthn/server');

const APP_NAME = 'Bill Tracker';
const CHALLENGE_TTL_MS = 15 * 60 * 1000;

function getRpId(): string {
  return process.env.WEBAUTHN_RP_ID || 'localhost';
}

function getOrigin(): string {
  const o = process.env.WEBAUTHN_ORIGIN;
  if (o) return o;
  const port = process.env.PORT || 3000;
  return `http://localhost:${port}`;
}

// ── Registration ──────────────────────────────────────────────────────────────

async function createRegistrationChallenge(db: Db, userId: number, username: string) {
  let { webauthn_user_id } =
    db.prepare('SELECT webauthn_user_id FROM users WHERE id = ?').get(userId) || {};

  if (!webauthn_user_id) {
    webauthn_user_id = crypto.randomBytes(32).toString('base64url');
    db.prepare(
      "UPDATE users SET webauthn_user_id = ?, updated_at = datetime('now') WHERE id = ?",
    ).run(webauthn_user_id, userId);
  }

  // Exclude already-registered credentials so the authenticator won't re-register them
  const existing = db
    .prepare('SELECT credential_id FROM webauthn_credentials WHERE user_id = ?')
    .all(userId);

  const options = await generateRegistrationOptions({
    rpName: APP_NAME,
    rpID: getRpId(),
    userID: Buffer.from(webauthn_user_id, 'base64url'),
    userName: username,
    userDisplayName: username,
    attestationType: 'none',
    excludeCredentials: existing.map((c: any) => ({ id: c.credential_id })),
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred',
    },
    supportedAlgorithmIDs: [-7, -257],
  });

  const challengeId = crypto.randomUUID();
  const expiresAt = new Date(Date.now() + CHALLENGE_TTL_MS)
    .toISOString()
    .slice(0, 19)
    .replace('T', ' ');

  db.prepare(
    "DELETE FROM webauthn_challenges WHERE user_id = ? AND challenge_type = 'registration'",
  ).run(userId);
  db.prepare(
    'INSERT INTO webauthn_challenges (id, user_id, challenge_type, challenge, expires_at) VALUES (?, ?, ?, ?, ?)',
  ).run(challengeId, userId, 'registration', options.challenge, expiresAt);

  return { options, challengeId };
}

async function verifyRegistration(
  db: Db,
  userId: number,
  challengeId: string,
  response: any,
  credentialName: any,
) {
  const row = db
    .prepare(
      "SELECT challenge FROM webauthn_challenges WHERE id = ? AND user_id = ? AND challenge_type = 'registration' AND expires_at > datetime('now')",
    )
    .get(challengeId, userId);
  if (!row) return { verified: false, error: 'Challenge expired or invalid' };

  try {
    const verification = await verifyRegistrationResponse({
      response,
      expectedChallenge: row.challenge,
      expectedOrigin: getOrigin(),
      expectedRPID: getRpId(),
    });

    if (!verification.verified)
      return { verified: false, error: 'Registration verification failed' };

    const { credential, aaguid, credentialDeviceType, credentialBackedUp } =
      verification.registrationInfo;

    db.prepare(
      `
      INSERT INTO webauthn_credentials
        (user_id, credential_id, public_key, sign_count, transports, backup_eligible, backup_state, credential_name, aaguid)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `,
    ).run(
      userId,
      credential.id,
      Buffer.from(credential.publicKey).toString('base64url'),
      credential.counter,
      credential.transports ? JSON.stringify(credential.transports) : null,
      credentialDeviceType === 'multiDevice' ? 1 : 0,
      credentialBackedUp ? 1 : 0,
      credentialName || 'Security Key',
      aaguid || null,
    );

    db.prepare('DELETE FROM webauthn_challenges WHERE id = ?').run(challengeId);

    return { verified: true, credentialId: credential.id };
  } catch (err: any) {
    console.error('[webauthn] registration error:', err.message);
    return { verified: false, error: err.message };
  }
}

// ── Authentication ────────────────────────────────────────────────────────────

async function createAuthenticationChallenge(db: Db, userId: number) {
  const credentials = db
    .prepare('SELECT credential_id, transports FROM webauthn_credentials WHERE user_id = ?')
    .all(userId);
  if (!credentials.length) throw new Error('No registered WebAuthn credentials');

  const options = await generateAuthenticationOptions({
    rpID: getRpId(),
    allowCredentials: credentials.map((c: any) => ({
      id: c.credential_id,
      transports: c.transports ? JSON.parse(c.transports) : undefined,
    })),
    userVerification: 'preferred',
  });

  const challengeId = crypto.randomUUID();
  const expiresAt = new Date(Date.now() + CHALLENGE_TTL_MS)
    .toISOString()
    .slice(0, 19)
    .replace('T', ' ');

  db.prepare(
    "DELETE FROM webauthn_challenges WHERE user_id = ? AND challenge_type = 'authentication'",
  ).run(userId);
  db.prepare(
    'INSERT INTO webauthn_challenges (id, user_id, challenge_type, challenge, expires_at) VALUES (?, ?, ?, ?, ?)',
  ).run(challengeId, userId, 'authentication', options.challenge, expiresAt);

  return { options, challengeId };
}

async function verifyAuthentication(db: Db, userId: number, challengeId: string, response: any) {
  const row = db
    .prepare(
      "SELECT challenge FROM webauthn_challenges WHERE id = ? AND user_id = ? AND challenge_type = 'authentication' AND expires_at > datetime('now')",
    )
    .get(challengeId, userId);
  if (!row) return { verified: false, error: 'Challenge expired or invalid' };

  const cred = db
    .prepare(
      'SELECT credential_id, public_key, sign_count FROM webauthn_credentials WHERE user_id = ? AND credential_id = ?',
    )
    .get(userId, response.id);
  if (!cred) return { verified: false, error: 'Credential not registered to this user' };

  try {
    const verification = await verifyAuthenticationResponse({
      response,
      expectedChallenge: row.challenge,
      expectedOrigin: getOrigin(),
      expectedRPID: getRpId(),
      credential: {
        id: cred.credential_id,
        publicKey: Buffer.from(cred.public_key, 'base64url'),
        counter: cred.sign_count,
      },
    });

    if (!verification.verified) return { verified: false, error: 'Authentication failed' };

    // Update sign count to detect cloned authenticators
    db.prepare(
      "UPDATE webauthn_credentials SET sign_count = ?, updated_at = datetime('now') WHERE user_id = ? AND credential_id = ?",
    ).run(verification.authenticationInfo.newCounter, userId, cred.credential_id);

    db.prepare('DELETE FROM webauthn_challenges WHERE id = ?').run(challengeId);

    return { verified: true };
  } catch (err: any) {
    console.error('[webauthn] authentication error:', err.message);
    return { verified: false, error: err.message };
  }
}

// ── Login challenge (mirrors totpService.createChallenge / consumeChallenge) ──
// Issued after password passes; consumed when the WebAuthn assertion is verified.

function createLoginChallenge(db: Db, userId: number, webauthnChallengeId: any): string {
  const id = crypto.randomUUID();
  const expiresAt = new Date(Date.now() + CHALLENGE_TTL_MS)
    .toISOString()
    .slice(0, 19)
    .replace('T', ' ');
  db.prepare("DELETE FROM webauthn_challenges WHERE user_id = ? AND challenge_type = 'login'").run(
    userId,
  );
  // Piggy-back the authentication challengeId in the challenge column for retrieval
  db.prepare(
    'INSERT INTO webauthn_challenges (id, user_id, challenge_type, challenge, expires_at) VALUES (?, ?, ?, ?, ?)',
  ).run(id, userId, 'login', webauthnChallengeId, expiresAt);
  return id;
}

function consumeLoginChallenge(
  db: Db,
  loginChallengeId: any,
): { userId: number; authChallengeId: string } | null {
  const row = db
    .prepare(
      "SELECT user_id, challenge FROM webauthn_challenges WHERE id = ? AND challenge_type = 'login' AND expires_at > datetime('now')",
    )
    .get(loginChallengeId);
  if (!row) return null;
  db.prepare('DELETE FROM webauthn_challenges WHERE id = ?').run(loginChallengeId);
  return { userId: row.user_id, authChallengeId: row.challenge };
}

// ── Credential management ─────────────────────────────────────────────────────

function getCredentials(db: Db, userId: number): any[] {
  return db
    .prepare(
      'SELECT id, credential_id, credential_name, aaguid, backup_eligible, backup_state, created_at FROM webauthn_credentials WHERE user_id = ? ORDER BY created_at DESC',
    )
    .all(userId);
}

function deleteCredential(db: Db, credentialId: any, userId: number) {
  return db
    .prepare('DELETE FROM webauthn_credentials WHERE credential_id = ? AND user_id = ?')
    .run(credentialId, userId);
}

// ── Cleanup ───────────────────────────────────────────────────────────────────

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

module.exports = {
  createRegistrationChallenge,
  verifyRegistration,
  createAuthenticationChallenge,
  verifyAuthentication,
  createLoginChallenge,
  consumeLoginChallenge,
  getCredentials,
  deleteCredential,
  pruneExpiredChallenges,
};
