Source: models/invite-link.js

'use strict';

const { getPostgresDAL } = require('../db-postgres');
const type = require('../dal').type;
const debug = require('../util/debug');
const config = require('config');
const isUUID = require('is-uuid');
const { randomUUID } = require('crypto');
const { DocumentNotFound } = require('../dal/lib/errors');
const { initializeModel } = require('../dal/lib/model-initializer');

let InviteLink = null;

/**
 * Initialize the PostgreSQL InviteLink model
 * @param {DataAccessLayer} dal - Optional DAL instance for testing
 */
async function initializeInviteLinkModel(dal = null) {
  const activeDAL = dal || await getPostgresDAL();

  if (!activeDAL) {
    debug.db('PostgreSQL DAL not available, skipping InviteLink model initialization');
    return null;
  }

  try {
    const schema = {
      id: type.string().uuid(4).default(() => randomUUID()),
      createdBy: type.string().uuid(4).required(true),
      createdOn: type.date().default(() => new Date()),
      usedBy: type.string().uuid(4),
      url: type.virtual().default(function() {
        const identifier = this.getValue ? this.getValue('id') : this.id;
        return identifier ? `${config.qualifiedURL}register/${identifier}` : undefined;
      })
    };

    const { model, isNew } = initializeModel({
      dal: activeDAL,
      baseTable: 'invite_links',
      schema,
      camelToSnake: {
        createdBy: 'created_by',
        createdOn: 'created_on',
        usedBy: 'used_by'
      },
      staticMethods: {
        getAvailable,
        getUsed,
        get: getInviteByID
      }
    });
    InviteLink = model;

    if (isNew) {
      debug.db('PostgreSQL InviteLink model initialized');
    }
  } catch (error) {
    debug.error('Failed to initialize PostgreSQL InviteLink model:', error);
    return null;
  }

  return InviteLink;
}

/**
 * Build the canonical registration URL for an invite link instance.
 *
 * @param {Object} invite - Invite link instance or plain object
 * @returns {string|undefined} Fully qualified invite URL
 */
function buildInviteURL(invite) {
  if (!invite) {
    return undefined;
  }
  const identifier = invite.id;
  return identifier ? `${config.qualifiedURL}register/${identifier}` : undefined;
}

/**
 * Normalize fields on a hydrated invite link instance.
 *
 * @param {Object} invite - Invite link instance
 * @returns {Object} The normalized invite instance
 */
function normalizeInviteInstance(invite) {
  if (!invite) {
    return invite;
  }
  invite.url = buildInviteURL(invite);
  return invite;
}

/**
 * Get pending invite links created by the given user.
 *
 * @param {Object} user - User whose pending invites to load
 * @param {string} user.id - User identifier
 * @returns {Promise<Object[]>} Pending invite links, newest first
 */
async function getAvailable(user) {
  if (!user || !user.id) {
    return [];
  }

  try {
    const query = `
      SELECT *
      FROM ${InviteLink.tableName}
      WHERE created_by = $1
        AND (used_by IS NULL)
      ORDER BY created_on DESC
    `;

    const result = await InviteLink.dal.query(query, [user.id]);
    return result.rows.map(row => normalizeInviteInstance(InviteLink._createInstance(row)));
  } catch (error) {
    debug.error('Failed to fetch pending invite links:', error);
    return [];
  }
}

/**
 * Get redeemed invite links created by the given user, including user info.
 *
 * @param {Object} user - User whose redeemed invites to load
 * @param {string} user.id - User identifier
 * @returns {Promise<Object[]>} Redeemed invite links, newest first
 */
async function getUsed(user) {
  if (!user || !user.id) {
    return [];
  }

  try {
    const query = `
      SELECT *
      FROM ${InviteLink.tableName}
      WHERE created_by = $1
        AND used_by IS NOT NULL
      ORDER BY created_on DESC
    `;

    const result = await InviteLink.dal.query(query, [user.id]);
    const invites = result.rows.map(row => normalizeInviteInstance(InviteLink._createInstance(row)));

    const usedByIds = [...new Set(invites.map(invite => invite.usedBy).filter(Boolean))];
    if (usedByIds.length === 0) {
      return invites;
    }

    const userQuery = `
      SELECT id, display_name, canonical_name, registration_date, is_trusted, is_site_moderator, is_super_user
      FROM users
      WHERE id = ANY($1)
    `;
    const userResult = await InviteLink.dal.query(userQuery, [usedByIds]);
    const userMap = new Map();

    userResult.rows.forEach(row => {
      userMap.set(row.id, {
        ...row,
        displayName: row.display_name,
        urlName: row.canonical_name || (row.display_name ? encodeURIComponent(row.display_name.replace(/ /g, '_')) : undefined),
        registrationDate: row.registration_date
      });
    });

    invites.forEach(invite => {
      if (invite.usedBy && userMap.has(invite.usedBy)) {
        invite.usedByUser = userMap.get(invite.usedBy);
      }
    });

    return invites;
  } catch (error) {
    debug.error('Failed to fetch redeemed invite links:', error);
    return [];
  }
}

/**
 * Fetch a single invite link by its identifier.
 *
 * @param {string} id - Invite link identifier (UUID)
 * @returns {Promise<Object>} Invite link instance
 * @throws {DocumentNotFound} When no invite with the provided id exists
 */
async function getInviteByID(id) {
  if (!id) {
    const error = new DocumentNotFound('Invite link not found');
    error.name = 'DocumentNotFoundError';
    throw error;
  }

  if (!isUUID.v4(id)) {
    const error = new DocumentNotFound(`invite_links with id ${id} not found`);
    error.name = 'DocumentNotFoundError';
    throw error;
  }

  try {
    const query = `
      SELECT *
      FROM ${InviteLink.tableName}
      WHERE id = $1
      LIMIT 1
    `;

    const result = await InviteLink.dal.query(query, [id]);
    if (result.rows.length === 0) {
      const error = new DocumentNotFound(`invite_links with id ${id} not found`);
      error.name = 'DocumentNotFoundError';
      throw error;
    }

    return normalizeInviteInstance(InviteLink._createInstance(result.rows[0]));
  } catch (error) {
    if (error instanceof DocumentNotFound || error.name === 'DocumentNotFoundError') {
      throw error;
    }
    debug.error('Failed to fetch invite link by id:', error);
    throw error;
  }
}

// Synchronous handle for production use - proxies to the registered model
// Create synchronous handle using the model handle factory
const { createAutoModelHandle } = require('../dal/lib/model-handle');

const InviteLinkHandle = createAutoModelHandle('invite_links', initializeInviteLinkModel);

/**
 * Obtain the PostgreSQL InviteLink model, initializing if necessary.
 *
 * @param {DataAccessLayer} [dal] - Optional DAL for testing
 * @returns {Promise<Function|null>} Initialized model constructor or null
 */
async function getPostgresInviteLinkModel(dal = null) {
  InviteLink = await initializeInviteLinkModel(dal);
  return InviteLink;
}

module.exports = InviteLinkHandle;

// Export factory function for fixtures and tests
module.exports.initializeModel = initializeInviteLinkModel;
module.exports.initializeInviteLinkModel = initializeInviteLinkModel; // Backward compatibility
module.exports.getPostgresInviteLinkModel = getPostgresInviteLinkModel;