const db = require('../db/models');
const ValidationError = require('./notifications/errors/validation');

const Sequelize = db.Sequelize;
const Op = Sequelize.Op;

/**
 * @param {object} currentUser
 * @returns {Promise<Set<string>>}
 */
async function getUserPermissions(currentUser) {
  if (!currentUser) {
    throw new ValidationError('auth.unauthorized');
  }

  const permissions = new Set(
    (currentUser.custom_permissions || []).map((cp) => cp.name).filter(Boolean),
  );

  if (!currentUser.app_role) {
    throw new ValidationError('auth.forbidden');
  }

  const rolePermissions = await currentUser.app_role.getPermissions();
  for (const permission of rolePermissions) {
    if (permission?.name) {
      permissions.add(permission.name);
    }
  }

  return permissions;
}

/**
 * @param {Set<string>} permissions
 * @param {object} currentUser
 */
function hasPermission(permissions, permissionName) {
  return permissions.has(permissionName);
}

module.exports = class SearchService {
  static async search(searchQuery, currentUser) {
    if (!searchQuery) {
      throw new ValidationError('iam.errors.searchQueryRequired');
    }
    const tableColumns = {
      users: ['firstName', 'lastName', 'phoneNumber', 'email'],

      projects: [
        'name',
        'slug',
        'description',
        'logo_url',
        'favicon_url',
        'og_image_url',
      ],

      assets: ['name', 'cdn_url', 'storage_key', 'mime_type', 'checksum'],

      asset_variants: ['cdn_url'],

      presigned_url_requests: ['requested_key', 'mime_type', 'status'],

      tour_pages: [
        'source_key',

        'name',

        'slug',

        'background_image_url',

        'background_video_url',

        'background_audio_url',

        'ui_schema_json',
      ],

      project_audio_tracks: ['source_key', 'name', 'slug', 'url'],

      publish_events: ['error_message'],

      pwa_caches: ['cache_version', 'manifest_json', 'asset_list_json'],

      access_logs: ['path', 'ip_address', 'user_agent'],
    };
    const columnsInt = {
      assets: ['size_mb', 'width_px', 'height_px', 'duration_sec'],

      asset_variants: ['width_px', 'height_px', 'size_mb'],

      presigned_url_requests: ['requested_size_mb'],

      tour_pages: ['sort_order'],

      project_audio_tracks: ['volume', 'sort_order'],

      publish_events: ['pages_copied', 'transitions_copied', 'audios_copied'],
    };

    const permissionSet = await getUserPermissions(currentUser);
    const normalizedSearchQuery = searchQuery.toLowerCase();
    const SEARCH_LIMIT_PER_TABLE = 50;

    const searchTasks = Object.keys(tableColumns).map(async (tableName) => {
      if (
        !Object.prototype.hasOwnProperty.call(tableColumns, tableName) ||
        !db[tableName]
      ) {
        return [];
      }

      if (!hasPermission(permissionSet, `READ_${tableName.toUpperCase()}`)) {
        return [];
      }

      const attributesToSearch = tableColumns[tableName];
      const attributesIntToSearch = columnsInt[tableName] || [];
      const whereCondition = {
        [Op.or]: [
          ...attributesToSearch.map((attribute) => ({
            [attribute]: {
              [Op.iLike]: `%${searchQuery}%`,
            },
          })),
          ...attributesIntToSearch.map((attribute) =>
            Sequelize.where(
              Sequelize.cast(
                Sequelize.col(`${tableName}.${attribute}`),
                'varchar',
              ),
              { [Op.iLike]: `%${searchQuery}%` },
            ),
          ),
        ],
      };

      const foundRecords = await db[tableName].findAll({
        where: whereCondition,
        attributes: [...attributesToSearch, 'id', ...attributesIntToSearch],
        limit: SEARCH_LIMIT_PER_TABLE,
      });

      return foundRecords.map((record) => {
        const matchAttribute = [];

        for (const attribute of attributesToSearch) {
          if (
            record[attribute]?.toLowerCase()?.includes(normalizedSearchQuery)
          ) {
            matchAttribute.push(attribute);
          }
        }

        for (const attribute of attributesIntToSearch) {
          const castedValue = String(record[attribute]);
          if (
            castedValue &&
            castedValue.toLowerCase().includes(normalizedSearchQuery)
          ) {
            matchAttribute.push(attribute);
          }
        }

        return {
          ...record.get(),
          matchAttribute,
          tableName,
        };
      });
    });

    const resultsByTable = await Promise.all(searchTasks);
    return resultsByTable.flat();
  }
};
