Source: models/team.js

'use strict';

const { createModelModule } = require('../dal/lib/model-handle');
const { proxy: TeamHandle, register: registerTeamHandle } = createModelModule({
  tableName: 'teams'
});

module.exports = TeamHandle;

const { getPostgresDAL } = require('../db-postgres');
const type = require('../dal').type;
const mlString = require('../dal').mlString;
const debug = require('../util/debug');
const isValidLanguage = require('../locales/languages').isValid;
const User = require('./user');
const Review = require('./review');
const TeamJoinRequest = require('./team-join-request');
const { initializeModel } = require('../dal/lib/model-initializer');

let Team = null;

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

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

  try {
    // Create the schema with revision fields and JSONB columns
    const teamSchema = {
      id: type.string().uuid(4),
      
      // JSONB multilingual fields
      name: mlString.getSchema({ maxLength: 100 }),
      motto: mlString.getSchema({ maxLength: 200 }),
      description: type.object().validator(_validateTextHtmlObject),
      rules: type.object().validator(_validateTextHtmlObject),
      
      // CamelCase relational fields that map to snake_case database columns
      modApprovalToJoin: type.boolean().default(false),
      onlyModsCanBlog: type.boolean().default(false),
      createdBy: type.string().uuid(4).required(true),
      createdOn: type.date().required(true),
      canonicalSlugName: type.string(),
      originalLanguage: type.string().max(4).validator(isValidLanguage),
      
      // JSONB for permissions configuration
      confersPermissions: type.object().validator(_validateConfersPermissions),
      
      // Virtual fields for feeds and permissions
      reviewOffsetDate: type.virtual().default(null),
      userIsFounder: type.virtual().default(false),
      userIsMember: type.virtual().default(false),
      userIsModerator: type.virtual().default(false),
      userCanBlog: type.virtual().default(false),
      userCanJoin: type.virtual().default(false),
      userCanLeave: type.virtual().default(false),
      userCanEdit: type.virtual().default(false),
      userCanDelete: type.virtual().default(false),
      
      urlID: type.virtual().default(function() {
        const slugName = this.getValue ? this.getValue('canonicalSlugName') : this.canonicalSlugName;
        return slugName ? encodeURIComponent(slugName) : this.id;
      })
    };

    const { model, isNew } = initializeModel({
      dal: activeDAL,
      baseTable: 'teams',
      schema: teamSchema,
      camelToSnake: {
        modApprovalToJoin: 'mod_approval_to_join',
        onlyModsCanBlog: 'only_mods_can_blog',
        createdBy: 'created_by',
        createdOn: 'created_on',
        canonicalSlugName: 'canonical_slug_name',
        originalLanguage: 'original_language',
        confersPermissions: 'confers_permissions'
      },
      withRevision: {
        static: [
          'createFirstRevision', 
          'getNotStaleOrDeleted', 
          'filterNotStaleOrDeleted',
          'getMultipleNotStaleOrDeleted'
        ],
        instance: ['newRevision', 'deleteAllRevisions']
      },
      staticMethods: {
        getWithData
      },
      instanceMethods: {
        populateUserInfo,
        updateSlug
      },
      relations: [
        {
          name: 'members',
          targetTable: 'users',
          sourceKey: 'id',
          targetKey: 'id',
          hasRevisions: false,
          through: {
            table: 'team_members',
            sourceForeignKey: 'team_id',
            targetForeignKey: 'user_id'
          },
          cardinality: 'many'
        },
        {
          name: 'moderators',
          targetTable: 'users',
          sourceKey: 'id',
          targetKey: 'id',
          hasRevisions: false,
          through: {
            table: 'team_moderators',
            sourceForeignKey: 'team_id',
            targetForeignKey: 'user_id'
          },
          cardinality: 'many'
        }
      ]
    });
    Team = model;

    if (!isNew) {
      return Team;
    }

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

// NOTE: STATIC METHODS --------------------------------------------------------

/**
 * Retrieve a team and some of the joined data like reviews or members
 *
 * @param {String} id - the team to look up
 * @param {Object} [options] - query criteria
 * @param {Boolean} options.withMembers=true - get the user objects representing the members
 * @param {Boolean} options.withModerators=true - get the user objects representing the moderators
 * @param {Boolean} options.withJoinRequests=true - get the pending requests to join this team
 * @param {Boolean} options.withJoinRequestDetails=false - get user objects for join requests
 * @param {Boolean} options.withReviews=false - get reviews associated with this team
 * @param {Number} options.reviewLimit=1 - get up to this number of reviews
 * @param {Date} options.reviewOffsetDate - only get reviews older than this date
 * @returns {Team} team with joined data
 * @async
 */
async function getWithData(id, {
  withMembers = true,
  withModerators = true,
  withJoinRequests = true,
  withJoinRequestDetails = false,
  withReviews = false,
  reviewLimit = 1,
  reviewOffsetDate = null
} = {}) {

  // For now, get the basic team record
  const team = await Team.getNotStaleOrDeleted(id);

  // Join implementations would need proper many-to-many relationship handling
  if (withMembers) {
    team.members = await _getTeamMembers(id);
  }

  if (withModerators) {
    team.moderators = await _getTeamModerators(id);
  }

  if (withJoinRequests) {
    team.joinRequests = await _getTeamJoinRequests(id, withJoinRequestDetails);
  }

  if (withReviews) {
    const reviewData = await _getTeamReviews(id, reviewLimit, reviewOffsetDate);
    team.reviews = reviewData.reviews;
    team.reviewCount = reviewData.totalCount;

    if (reviewData.hasMore && team.reviews.length > 0) {
      const lastReview = team.reviews[team.reviews.length - 1];
      const offsetDate = lastReview?.createdOn || lastReview?.created_on;
      if (offsetDate) {
        team.reviewOffsetDate = offsetDate;
        team.review_offset_date = offsetDate;
      }
    }
  }

  return team;
}

// NOTE: INSTANCE METHODS ------------------------------------------------------

/**
 * Populate the virtual permission and metadata fields for this team with
 * information for a given user.
 *
 * @param {User} user - user whose permissions and metadata to use
 * @memberof Team
 * @instance
 */
function populateUserInfo(user) {
  if (!user) {
    return; // Permissions remain at defaults (false)
  }

  const team = this;
  
  // Check membership
  if (team.members && team.members.some(member => member.id === user.id)) {
    team.userIsMember = true;
  }

  // Check moderator status
  if (team.moderators && team.moderators.some(moderator => moderator.id === user.id)) {
    team.userIsModerator = true;
  }

  // Check founder status
  if (user.id === team.createdBy) {
    team.userIsFounder = true;
  }

  // Determine blogging permissions
  if (team.userIsMember && (!team.onlyModsCanBlog || team.userIsModerator)) {
    team.userCanBlog = true;
  }

  // Determine join permissions (can't join if already member or has pending request)
  // Users can rejoin if their previous request was withdrawn or approved
  if (!team.userIsMember &&
      (!team.joinRequests || !team.joinRequests.some(request =>
        request.userID === user.id && request.status === 'pending'))) {
    team.userCanJoin = true;
  }

  // Determine edit permissions
  if (team.userIsFounder || team.userIsModerator || user.isSuperUser) {
    team.userCanEdit = true;
  }

  // Determine delete permissions (only site-wide mods for now)
  if (user.isSuperUser || user.isSiteModerator) {
    team.userCanDelete = true;
  }

  // Determine leave permissions (founders can't leave, must delete team)
  if (!team.userIsFounder && team.userIsMember) {
    team.userCanLeave = true;
  }
}

// Helper functions for joins (these would need proper implementation with join tables)

/**
 * Get team members from join table
 * @param {String} teamId - Team ID
 * @returns {Promise<User[]>} Array of user objects
 */
async function _getTeamMembers(teamId) {
  try {
    const memberTableName = Team.dal.schemaNamespace ? 
      `${Team.dal.schemaNamespace}team_members` : 'team_members';
    const userTableName = Team.dal.schemaNamespace ? 
      `${Team.dal.schemaNamespace}users` : 'users';
    
    const query = `
      SELECT u.* FROM ${userTableName} u
      JOIN ${memberTableName} tm ON u.id = tm.user_id
      WHERE tm.team_id = $1
    `;
    
    const result = await Team.dal.query(query, [teamId]);
    return result.rows.map(row => {
      delete row.password;
      delete row.email;
      return User._createInstance(row);
    });
  } catch (error) {
    debug.error('Error getting team members:', error);
    return [];
  }
}

/**
 * Get team moderators from join table
 * @param {String} teamId - Team ID
 * @returns {Promise<User[]>} Array of user objects
 */
async function _getTeamModerators(teamId) {
  try {
    const moderatorTableName = Team.dal.schemaNamespace ? 
      `${Team.dal.schemaNamespace}team_moderators` : 'team_moderators';
    const userTableName = Team.dal.schemaNamespace ? 
      `${Team.dal.schemaNamespace}users` : 'users';
    
    const query = `
      SELECT u.* FROM ${userTableName} u
      JOIN ${moderatorTableName} tm ON u.id = tm.user_id
      WHERE tm.team_id = $1
    `;
    
    const result = await Team.dal.query(query, [teamId]);
    return result.rows.map(row => {
      delete row.password;
      delete row.email;
      return User._createInstance(row);
    });
  } catch (error) {
    debug.error('Error getting team moderators:', error);
    return [];
  }
}

/**
 * Get team join requests
 * @param {String} teamId - Team ID
 * @param {Boolean} withDetails - Whether to include user details
 * @returns {Promise<Object[]>} Array of join request objects
 */
async function _getTeamJoinRequests(teamId, withDetails = false) {
  let query = '';
  try {
    const joinRequestTableName = Team.dal.schemaNamespace ?
      `${Team.dal.schemaNamespace}team_join_requests` : 'team_join_requests';

    query = `SELECT * FROM ${joinRequestTableName} WHERE team_id = $1`;
    const result = await Team.dal.query(query, [teamId]);

    // Convert rows to TeamJoinRequest instances
    const requests = result.rows.map(row =>
      TeamJoinRequest._createInstance ?
        TeamJoinRequest._createInstance(row) :
        new TeamJoinRequest(row)
    );

    // If details requested, load the user for each request
    if (withDetails) {
      for (const request of requests) {
        if (request.userID) {
          try {
            const user = await User.get(request.userID);
            if (user) {
              delete user.password;
              request.user = user;
            }
          } catch (error) {
            debug.error(`Error loading user ${request.userID} for join request:`, error);
          }
        }
      }
    }

    return requests;
  } catch (error) {
    debug.error(`Error getting team join requests: ${error.message}`);
    debug.error(`Query: ${query}`);
    debug.error(`Params: ${JSON.stringify([teamId])}`);
    debug.error('Full error:', error);
    return [];
  }
}

/**
 * Get team reviews with pagination
 * @param {String} teamId - Team ID
 * @param {Number} limit - Maximum number of reviews
 * @param {Date} offsetDate - Date offset for pagination
 * @returns {Promise<Object>} Object with reviews array and total count
 */
async function _getTeamReviews(teamId, limit, offsetDate) {
  try {
    const reviewTeamTableName = Team.dal.schemaNamespace ? 
      `${Team.dal.schemaNamespace}review_teams` : 'review_teams';
    const reviewTableName = Team.dal.schemaNamespace ? 
      `${Team.dal.schemaNamespace}reviews` : 'reviews';
    let query = `
      SELECT r.id, r.created_on FROM ${reviewTableName} r
      JOIN ${reviewTeamTableName} rt ON r.id = rt.review_id
      WHERE rt.team_id = $1
        AND (r._old_rev_of IS NULL)
        AND (r._rev_deleted IS NULL OR r._rev_deleted = false)
    `;
    
    const params = [teamId];
    let paramIndex = 2;
    
    if (offsetDate) {
      query += ` AND r.created_on < $${paramIndex}`;
      params.push(offsetDate);
      paramIndex++;
    }
    
    query += ` ORDER BY r.created_on DESC LIMIT $${paramIndex}`;
    params.push(limit + 1);
    
    // Get reviews
    const reviewResult = await Team.dal.query(query, params);
    const reviewIDs = reviewResult.rows.map(row => row.id);
    const reviews = [];
    const hasMoreRows = reviewResult.rows.length > limit;

    for (const id of reviewIDs) {
      try {
        const review = await Review.getWithData(id);
        if (review) {
          reviews.push(review);
        }
      } catch (error) {
        debug.error('Error loading review for team:', error);
      }
    }
    
    if (hasMoreRows && reviews.length > limit) {
      reviews.length = limit;
    }
    
    // Get total count
    const countQuery = `
      SELECT COUNT(*) as total FROM ${reviewTableName} r
      JOIN ${reviewTeamTableName} rt ON r.id = rt.review_id
      WHERE rt.team_id = $1
        AND (r._old_rev_of IS NULL)
        AND (r._rev_deleted IS NULL OR r._rev_deleted = false)
    `;
    
    const countResult = await Team.dal.query(countQuery, [teamId]);
    
    return {
      reviews,
      totalCount: parseInt(countResult.rows[0]?.total || 0, 10),
      hasMore: hasMoreRows && reviews.length === limit
    };
  } catch (error) {
    debug.error('Error getting team reviews:', error);
    return { reviews: [], totalCount: 0, hasMore: false };
  }
}

// Validation functions

/**
 * Validate text/html object structure for description and rules
 * @param {Object} value - Object to validate
 * @returns {Boolean} true if valid
 */
function _validateTextHtmlObject(value) {
  if (value === null || value === undefined) {
    return true;
  }
  
  if (typeof value !== 'object' || Array.isArray(value)) {
    throw new Error('Description/rules must be an object with text and html properties');
  }
  
  // Should have text and html properties, both multilingual strings
  if (value.text !== undefined) {
    mlString.validate(value.text);
  }
  
  if (value.html !== undefined) {
    mlString.validate(value.html);
  }
  
  return true;
}

/**
 * Validate confers_permissions object structure
 * @param {Object} value - Object to validate
 * @returns {Boolean} true if valid
 */
function _validateConfersPermissions(value) {
  if (value === null || value === undefined) {
    return true;
  }
  
  if (typeof value !== 'object' || Array.isArray(value)) {
    throw new Error('Confers permissions must be an object');
  }
  
  // Validate known permission fields
  const validPermissions = ['show_error_details', 'translate'];
  for (const [key, val] of Object.entries(value)) {
    if (validPermissions.includes(key)) {
      if (typeof val !== 'boolean') {
        throw new Error(`Permission ${key} must be a boolean`);
      }
    }
  }
  
  return true;
}

/**
 * Update the canonical slug name for this team based on its name field.
 * Only updates if the language matches the original language.
 * 
 * @param {String} userID - User ID to attribute the slug creation to
 * @param {String} language - Language to use for slug generation
 * @returns {Team} This team instance with updated canonical slug name
 * @memberof Team
 * @instance
 */
async function updateSlug(userID, language) {
  const TeamSlug = require('./team-slug');
  const originalLanguage = this.originalLanguage || 'en';
  const slugLanguage = language || originalLanguage;

  if (slugLanguage !== originalLanguage) {
    return this;
  }

  if (!this.name) {
    return this;
  }

  const resolved = mlString.resolve(slugLanguage, this.name);
  if (!resolved || typeof resolved.str !== 'string' || !resolved.str.trim()) {
    return this;
  }

  let baseSlug;
  try {
    baseSlug = _generateSlugName(resolved.str);
  } catch (error) {
    return this;
  }

  if (!baseSlug || baseSlug === this.canonicalSlugName) {
    return this;
  }

  if (!this.id) {
    const { randomUUID } = require('crypto');
    this.id = randomUUID();
  }

  const slug = new TeamSlug({});
  slug.slug = baseSlug;
  slug.name = baseSlug;
  slug.teamID = this.id;
  slug.createdOn = new Date();
  slug.createdBy = userID;

  const savedSlug = await slug.qualifiedSave();
  if (savedSlug && savedSlug.name) {
    this.canonicalSlugName = savedSlug.name;
    this._changed.add(Team._getDbFieldName('canonicalSlugName'));
  }

  return this;
}

/**
 * Generate a URL-friendly slug from a string
 * @param {String} str - Source string
 * @returns {String} Slug name
 */
function _generateSlugName(str) {
  const unescapeHTML = require('unescape-html');
  const isUUID = require('is-uuid');
  
  if (typeof str !== 'string') {
    throw new Error('Source string is undefined or not a string.');
  }

  str = str.trim();

  if (str === '') {
    throw new Error('Source string cannot be empty.');
  }

  let slugName = unescapeHTML(str)
    .trim()
    .toLowerCase()
    .replace(/[?&"″'`'<>:]/g, '')
    .replace(/[ _/]/g, '-')
    .replace(/-{2,}/g, '-');

  if (!slugName) {
    throw new Error('Source string cannot be converted to a valid slug.');
  }

  if (isUUID.v4(slugName)) {
    throw new Error('Source string cannot be a UUID.');
  }

  return slugName;
}

registerTeamHandle({
  initializeModel: initializeTeamModel,
  additionalExports: {
    initializeTeamModel
  }
});