const db = require('../db/models');
const ValidationError = require('./notifications/errors/validation');
const RolesDBApi = require('../db/api/roles');
const GooglePlacesService = require('./googlePlaces');

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

// Cache for the 'Public' role object
let publicRoleCache = null;

async function getPublicRole() {
  if (publicRoleCache) return publicRoleCache;
  publicRoleCache = await RolesDBApi.findBy({ name: 'Public' });
  return publicRoleCache;
}

/**
 * @param {string} permission
 * @param {object} currentUser
 */
async function checkPermissions(permission, currentUser) {
  let role = null;

  if (currentUser) {
    const userPermission = currentUser.custom_permissions?.find(
      (cp) => cp.name === permission,
    );
    if (userPermission) return true;
    role = currentUser.app_role;
  } else {
    role = await getPublicRole();
  }

  if (!role) {
    return false;
  }

  try {
    let permissions = [];
    if (typeof role.getPermissions === 'function') {
        permissions = await role.getPermissions();
    } else {
        permissions = role.permissions || [];
    }
    return !!permissions.find((p) => p.name === permission);
  } catch (e) {
    console.error("Search permission check error:", e);
    return false;
  }
}

module.exports = class SearchService {
  static async search(searchQuery, currentUser, location ) {
    try {
      if (!searchQuery) {
        throw new ValidationError('iam.errors.searchQueryRequired');
      }
      
      const roleName = currentUser?.app_role?.name || 'Public';
      const isAdmin = roleName === 'Administrator' || roleName === 'Platform Owner';

      // Columns that can be searched using iLike
      const searchableColumns = {
            "users": [
                "firstName",
                "lastName",
                "phoneNumber",
                "email",
            ],
            "categories": [
                "name",
                "slug",
                "description",
            ],
            "locations": [
                "label",
                "city",
                "state",
                "zip",
            ],
            "businesses": [
                "name",
                "slug",
                "description",
                "phone",
                "email",
                "website",
                "address",
                "city",
                "state",
                "zip",
            ],
      };

      // All columns to be returned in the result
      const returnColumns = {
            "users": [...searchableColumns.users],
            "categories": [...searchableColumns.categories, "icon"],
            "locations": [...searchableColumns.locations],
            "businesses": [
              ...searchableColumns.businesses,
              "is_claimed",
              "rating",
              "lat",
              "lng",
              "reliability_score",
              "response_time_median_minutes"
            ],
      };

      const columnsInt = {
            "businesses": [
                "lat",
                "lng",
                "reliability_score",
                "response_time_median_minutes",
            ],
      };

      let allFoundRecords = [];

      for (const tableName in searchableColumns) {
        if (searchableColumns.hasOwnProperty(tableName)) {
          const attributesToSearch = searchableColumns[tableName];
          const attributesIntToSearch = columnsInt[tableName] || [];
          
          const searchConditions = [
            ...attributesToSearch.map(attribute => ({
              [attribute]: {
                [Op.iLike] : `%${searchQuery}%`,
              },
            })),
            ...attributesIntToSearch.map(attribute => (
              Sequelize.where(
                Sequelize.cast(Sequelize.col(`${tableName}.${attribute}`), 'varchar'),
                { [Op.iLike]: `%${searchQuery}%` }
              )
            )),
          ];

          const whereCondition = {
            [Op.or]: searchConditions,
          };

          // Only show active businesses for non-admins
          if (tableName === 'businesses' && !isAdmin) {
             whereCondition[Op.and] = whereCondition[Op.and] || [];
             whereCondition[Op.and].push({ is_active: true });
          }

          // If location is provided, bias local results by location for businesses and locations
          if (location && (tableName === 'businesses' || tableName === 'locations')) {
             const locationConditions = [
               { zip: { [Op.iLike]: `%${location}%` } },
               { city: { [Op.iLike]: `%${location}%` } },
               { state: { [Op.iLike]: `%${location}%` } },
             ];
             
             // locations table doesn't have address column
             if (tableName === 'businesses') {
               locationConditions.push({ address: { [Op.iLike]: `%${location}%` } });
             }

             whereCondition[Op.and] = whereCondition[Op.and] || [];
             whereCondition[Op.and].push({
                 [Op.or]: locationConditions
             });
          }

          const hasPerm = await checkPermissions(`READ_${tableName.toUpperCase()}`, currentUser);
          if (!hasPerm) {
            continue;
          }

          const include = [];
          if (tableName === 'businesses') {
            include.push({
              model: db.business_photos,
              as: 'business_photos_business',
              include: [{
                model: db.file,
                as: 'photos'
              }]
            });
          }

          const foundRecords = await db[tableName].findAll({
            where: whereCondition,
            attributes: [...returnColumns[tableName], 'id'],
            include
          });
      
          const modifiedRecords = foundRecords.map((record) => {
            const matchAttribute = [];
 
            for (const attribute of attributesToSearch) {
              if (record[attribute] && typeof record[attribute] === 'string' && record[attribute].toLowerCase().includes(searchQuery.toLowerCase())) {
                matchAttribute.push(attribute);
              }
            }

            for (const attribute of attributesIntToSearch) {
              const castedValue = String(record[attribute]);
              if (castedValue && castedValue.toLowerCase().includes(searchQuery.toLowerCase())) {
                matchAttribute.push(attribute);
              }
            }
      
            return {
              ...record.get(),
              matchAttribute,
              tableName,
            };
          });

          allFoundRecords = allFoundRecords.concat(modifiedRecords);
        }
      }

      // Special case: if we found categories, find businesses in those categories
      const foundCategories = allFoundRecords.filter(r => r.tableName === 'categories');
      if (foundCategories.length > 0) {
        const categoryIds = foundCategories.map(c => c.id);
        const businessesInCategories = await db.businesses.findAll({
          where: !isAdmin ? { is_active: true } : {},
          include: [
            {
              model: db.business_categories,
              as: 'business_categories_business',
              where: { categoryId: { [Op.in]: categoryIds } }
            },
            {
              model: db.business_photos,
              as: 'business_photos_business',
              include: [{
                model: db.file,
                as: 'photos'
              }]
            }
          ]
        });

        for (const biz of businessesInCategories) {
          if (!allFoundRecords.find(r => r.id === biz.id && r.tableName === 'businesses')) {
            allFoundRecords.push({
              ...biz.get(),
              matchAttribute: ['category'],
              tableName: 'businesses',
            });
          }
        }
      }

      // If few local businesses found, try Google Places
      const localBusinessesCount = allFoundRecords.filter(r => r.tableName === 'businesses').length;
      if (localBusinessesCount < 5) {
        try {
          // If no location in search query, try to append a default location to help Google
          let refinedQuery = searchQuery;
          if (location) {
            refinedQuery = `${searchQuery} ${location}`;
          } else if (!searchQuery.match(/\d{5}/) && !searchQuery.match(/in\s+[A-Za-z]+/i)) {
             refinedQuery = `${searchQuery} USA`; // Search nationwide if no location specified
          }

          const googleResults = await GooglePlacesService.searchPlaces(refinedQuery);
          for (const gPlace of googleResults) {
            // Import each place to our DB
            const importedBusiness = await GooglePlacesService.importFromGoogle(gPlace);
            
            // Re-fetch with associations to get photos
            const fullBusiness = await db.businesses.findByPk(importedBusiness.id, {
              include: [{
                model: db.business_photos,
                as: 'business_photos_business',
                include: [{
                  model: db.file,
                  as: 'photos'
                }]
              }]
            });

            // Add to search results if not already there
            if (fullBusiness && !allFoundRecords.find(r => r.id === fullBusiness.id)) {
              allFoundRecords.push({
                ...fullBusiness.get(),
                matchAttribute: ['name'],
                tableName: 'businesses',
                isFromGoogle: true,
              });
            }
          }
        } catch (gError) {
          console.error("Google Search fallback error:", gError);
        }
      }

      return allFoundRecords;
    } catch (error) {
      throw error;
    }
  }
}