Source: models/thing-slug.js

'use strict';

const { getPostgresDAL } = require('../db-postgres');
const type = require('../dal').type;
const { ConstraintError } = require('../dal/lib/errors');
const { initializeModel } = require('../dal/lib/model-initializer');
const debug = require('../util/debug');

let ThingSlug = null;

// Reserved slugs that must never be used without qualification
const reservedSlugs = [
  'register', 'actions', 'signin', 'login', 'teams', 'user', 'new',
  'signout', 'logout', 'api', 'faq', 'static', 'terms'
];

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

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

  try {
    const schema = {
      id: type.string().uuid(4),
      thingID: type.string().uuid(4).required(true),
      slug: type.string().max(255).required(true),
      createdOn: type.date().default(() => new Date()),
      createdBy: type.string().uuid(4),
      baseName: type.string().max(255),
      name: type.string().max(255),
      qualifierPart: type.string().max(255)
    };

    const { model } = initializeModel({
      dal: activeDAL,
      baseTable: 'thing_slugs',
      schema,
      camelToSnake: {
        thingID: 'thing_id',
        createdOn: 'created_on',
        createdBy: 'created_by',
        baseName: 'base_name',
        qualifierPart: 'qualifier_part'
      },
      staticMethods: {
        getByName
      },
      instanceMethods: {
        qualifiedSave
      }
    });

    ThingSlug = model;
    ThingSlug.reservedSlugs = reservedSlugs;

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

// 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 ThingSlugHandle = createAutoModelHandle('thing_slugs', initializeThingSlugModel);
ThingSlugHandle.reservedSlugs = reservedSlugs;

/**
 * Get the PostgreSQL ThingSlug model (initialize if needed)
 * @param {DataAccessLayer|null} [dal] - Optional DAL instance for testing
 */
async function getPostgresThingSlugModel(dal = null) {
  ThingSlug = await initializeThingSlugModel(dal);
  return ThingSlug;
}

/**
 * Get a thing slug by its name field.
 *
 * @param {string} name - The slug name to look up
 * @returns {Promise<ThingSlug|null>} The thing slug instance or null if not found
 * @static
 * @memberof ThingSlug
 */
async function getByName(name) {
  try {
    return await this.filter({ name }).first();
  } catch (error) {
    debug.error(`Error getting thing slug by name '${name}':`, error);
    return null;
  }
}

async function qualifiedSave() {
  const Model = this.constructor;
  const dal = Model.dal;

  if (!dal) {
    throw new Error('ThingSlug DAL not initialized');
  }

  if (!this.baseName) {
    this.baseName = this.name;
  }

  if (!this.createdOn) {
    this.createdOn = new Date();
  }

  const baseName = this.baseName;
  const thingID = this.thingID;
  const forceQualifier = reservedSlugs.includes((this.name || '').toLowerCase());

  const findByName = async name => {
    const existing = await dal.query(
      `SELECT * FROM ${Model.tableName} WHERE name = $1 LIMIT 1`,
      [name]
    );
    return existing.rows.length ? Model._createInstance(existing.rows[0]) : null;
  };

  const attemptSave = async () => {
    try {
      await this.save();
      return this;
    } catch (error) {
      if (error instanceof ConstraintError) {
        return null;
      }
      throw error;
    }
  };

  if (!forceQualifier) {
    const saved = await attemptSave();
    if (saved) {
      return saved;
    }
    const existing = await findByName(this.name);
    if (existing && existing.thingID === thingID) {
      return existing;
    }
  }

  const reuseQuery = `
    SELECT *
    FROM ${Model.tableName}
    WHERE base_name = $1
      AND thing_id = $2
    ORDER BY created_on DESC
    LIMIT 1
  `;
  let result = await dal.query(reuseQuery, [baseName, thingID]);
  if (result.rows.length) {
    return Model._createInstance(result.rows[0]);
  }

  const latestQuery = `
    SELECT qualifier_part
    FROM ${Model.tableName}
    WHERE base_name = $1
    ORDER BY created_on DESC
    LIMIT 1
  `;
  result = await dal.query(latestQuery, [baseName]);
  let nextQualifier = '2';
  if (result.rows.length && result.rows[0].qualifier_part) {
    const parsed = parseInt(result.rows[0].qualifier_part, 10);
    if (!Number.isNaN(parsed)) {
      nextQualifier = String(parsed + 1);
    }
  }

  while (true) {
    this.name = `${baseName}-${nextQualifier}`;
    this.slug = this.name;
    this.qualifierPart = nextQualifier;
    const saved = await attemptSave();
    if (saved) {
      return saved;
    }

    const existing = await findByName(this.name);
    if (existing && existing.thingID === thingID) {
      return existing;
    }

    const parsed = parseInt(nextQualifier, 10);
    nextQualifier = Number.isNaN(parsed) ? `${nextQualifier}2` : String(parsed + 1);
  }
}

module.exports = ThingSlugHandle;

// Export factory function for fixtures and tests
module.exports.initializeModel = initializeThingSlugModel;
module.exports.getPostgresThingSlugModel = getPostgresThingSlugModel;
module.exports.reservedSlugs = reservedSlugs;