/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.social.core.profileproperty;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.picocontainer.Startable;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.exoplatform.commons.ObjectAlreadyExistsException;
import org.exoplatform.commons.api.settings.SettingService;
import org.exoplatform.commons.api.settings.SettingValue;
import org.exoplatform.commons.api.settings.data.Context;
import org.exoplatform.commons.api.settings.data.Scope;
import org.exoplatform.commons.search.index.IndexingService;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.listener.ListenerService;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.social.core.jpa.search.ProfileIndexingServiceConnector;
import org.exoplatform.social.core.profileproperty.model.ProfilePropertyOption;
import org.exoplatform.social.core.profileproperty.model.ProfilePropertySetting;
import org.exoplatform.social.core.profileproperty.storage.ProfileSettingStorage;

import io.meeds.social.translation.model.TranslationField;
import io.meeds.social.translation.service.TranslationService;

public class ProfilePropertyServiceImpl implements ProfilePropertyService, Startable {

  private static final Log                           LOG                                    =
                                                         ExoLogger.getLogger(ProfilePropertyServiceImpl.class);

  private final ProfileSettingStorage                profileSettingStorage;

  private final SettingService                       settingService;

  private final IndexingService                      indexingService;

  private final ListenerService                      listenerService;

  private final TranslationService                   translationService;

  private static final String                        SYNCHRONIZED_DISABLED_PROPERTIES       = "synchronizationDisabledProperties";

  private static final String                        UNHIDDENABLE_PROPERTIES_PARAM          = "unHiddenableProperties";

  private static final String                        EXCLUDED_QUICK_SEARCH_PROPERTIES_PARAM = "excludedQuickSearchProperties";

  private static final String                        EXCLUDED_ANALYTICS_INDEX_PROPERTIES    = "excludedAnalyticsIndexProperties";

  private static final Scope                         HIDDEN_PROFILE_PROPERTY_SETTINGS_SCOPE =
                                                                                            Scope.APPLICATION.id("ProfilePropertySettings");

  private static final String                        HIDDEN_PROFILE_PROPERTY_SETTINGS_KEY   = "HiddenProfilePropertySettings";

  private static final String                        PROFILE_PROPERTY_FIELD_NAME            = "optionValue";

  private static final String                        PROFILE_PROPERTY_OBJECT_TYPE           = "propertySettingOption";

  protected List<ProfilePropertyDatabaseInitializer> profielPropertyPlugins                 = new ArrayList<>();

  private List<String>                               synchronizedGroupDisabledProperties    = new ArrayList<>();

  private List<String>                               nonHiddenableProps                     = new ArrayList<>();

  private List<String>                               excludedQuickSearchProps               = new ArrayList<>();

  private List<String>                               excludedAnalyticsIndexProps            = new ArrayList<>();

  private static final String                        USER_CARD_SETTINGS                     = "UserCardSettings";

  public ProfilePropertyServiceImpl(InitParams params,
                                    ProfileSettingStorage profileSettingStorage,
                                    SettingService settingService,
                                    IndexingService indexingService,
                                    ListenerService listenerService,
                                    TranslationService translationService) {
    this.profileSettingStorage = profileSettingStorage;
    this.settingService = settingService;
    this.indexingService = indexingService;
    this.listenerService = listenerService;
    this.translationService = translationService;
    if (params != null) {
      try {
        synchronizedGroupDisabledProperties = Arrays.asList(params.getValueParam(SYNCHRONIZED_DISABLED_PROPERTIES)
                                                                  .getValue()
                                                                  .split(","));
        nonHiddenableProps = Arrays.asList(params.getValueParam(UNHIDDENABLE_PROPERTIES_PARAM).getValue().split(","));
        excludedQuickSearchProps = Arrays.asList(params.getValueParam(EXCLUDED_QUICK_SEARCH_PROPERTIES_PARAM)
                                                       .getValue()
                                                       .split(","));
        excludedAnalyticsIndexProps = Arrays.asList(params.getValueParam(EXCLUDED_ANALYTICS_INDEX_PROPERTIES)
                                                          .getValue()
                                                          .split(","));
      } catch (Exception e) {
        LOG.warn("List of disabled properties for synchronization not provided, all properties can be synchronized! ");
      }
    }
  }

  @Override
  public List<ProfilePropertySetting> getPropertySettings() {
    return profileSettingStorage.getPropertySettings();
  }

  @Override
  public List<ProfilePropertySetting> getSynchronizedPropertySettings() {
    return profileSettingStorage.getSynchronizedPropertySettings();
  }

  @Override
  public ProfilePropertySetting getProfileSettingByName(String name) {
    return profileSettingStorage.findProfileSettingByName(name);
  }

  @Override
  public List<String> getExcludedAnalyticsIndexProperties() {
    return excludedAnalyticsIndexProps;
  }

  @Override
  public ProfilePropertySetting getProfileSettingById(Long id) {
    return profileSettingStorage.getProfileSettingById(id);
  }

  @Override
  public List<String> getUnhiddenableProfileProperties() {
    return nonHiddenableProps;
  }

  @Override
  public List<String> getExcludedQuickSearchProperties() {
    return excludedQuickSearchProps;
  }

  @Override
  public boolean isPropertySettingHiddenable(Long id) {
    return isPropertySettingHiddenable(getProfileSettingById(id));
  }

  @Override
  public ProfilePropertySetting createPropertySetting(ProfilePropertySetting profilePropertySetting) throws ObjectAlreadyExistsException {

    validatePropertySetting(profilePropertySetting);

    ProfilePropertySetting storedProfilePropertySetting =
                                                        profileSettingStorage.findProfileSettingByName(profilePropertySetting.getPropertyName());
    if (storedProfilePropertySetting != null) {
      throw new ObjectAlreadyExistsException(storedProfilePropertySetting,
                                             "A profile property with the provided name already exists.");
    }

    if (!isGroupSynchronizedEnabledProperty(profilePropertySetting)) {
      profilePropertySetting.setGroupSynchronized(false);
    }

    profilePropertySetting.setUpdated(System.currentTimeMillis());
    storedProfilePropertySetting = profileSettingStorage.saveProfilePropertySetting(profilePropertySetting,
                                                                                    true);
    savePropertyOptionsTranslations(storedProfilePropertySetting, profilePropertySetting.getPropertyOptions(), false);
    if (storedProfilePropertySetting.getOrder() == null) {
      storedProfilePropertySetting.setOrder(storedProfilePropertySetting.getId());
      storedProfilePropertySetting = profileSettingStorage.saveProfilePropertySetting(storedProfilePropertySetting, false);
    }

    try {
      listenerService.broadcast("profile-property-setting-created", this, storedProfilePropertySetting);
    } catch (Exception e) {
      LOG.error("An error occurred while broadcasting the creation event for the property setting '{}'.",
                storedProfilePropertySetting.getPropertyName(),
                e);
    }
    return storedProfilePropertySetting;
  }

  @Override
  public void updatePropertySetting(ProfilePropertySetting profilePropertySetting) {

    validatePropertySetting(profilePropertySetting);

    if (profilePropertySetting.isHiddenbale()
        && getUnhiddenableProfileProperties().contains(profilePropertySetting.getPropertyName())) {
      throw new IllegalArgumentException(String.format("%s cannot be hidden", profilePropertySetting.getPropertyName()));
    }
    if (profilePropertySetting.isMultiValued() && isUserCardFieldSettings(profilePropertySetting.getPropertyName())) {
      throw new IllegalArgumentException(String.format("%s cannot be multivalued", profilePropertySetting.getPropertyName()));
    }
    if (!isGroupSynchronizedEnabledProperty(profilePropertySetting)) {
      profilePropertySetting.setGroupSynchronized(false);
    }
    ProfilePropertySetting createdProfilePropertySetting =
                                                         profileSettingStorage.getProfileSettingById(profilePropertySetting.getId());
    if (createdProfilePropertySetting != null && isDefaultProperties(profilePropertySetting)) {
      profilePropertySetting.setMultiValued(createdProfilePropertySetting.isMultiValued());
    }
    // Prevent any attempt to update the property type of created property
    // setting
    if (createdProfilePropertySetting != null) {
      profilePropertySetting.setPropertyType(createdProfilePropertySetting.getPropertyType());
    }
    profilePropertySetting.setUpdated(System.currentTimeMillis());
    ProfilePropertySetting updatedPropertySetting =
                                                  profileSettingStorage.saveProfilePropertySetting(profilePropertySetting, false);
    savePropertyOptionsTranslations(updatedPropertySetting, profilePropertySetting.getPropertyOptions(), true);
    try {
      listenerService.broadcast("profile-property-setting-updated", this, updatedPropertySetting);
    } catch (Exception e) {
      LOG.error("An error occurred when broadcasting the update event of the property setting {}",
                updatedPropertySetting.getPropertyName(),
                e);
    }
  }

  @Override
  public void deleteProfilePropertySetting(Long id) {
    if (id <= 0) {
      throw new IllegalArgumentException("Profile Property Setting Technical Identifier is mandatory");
    }
    profileSettingStorage.deleteProfilePropertySetting(id);
  }

  @Override
  public boolean isGroupSynchronizedEnabledProperty(Long id) {
    return isGroupSynchronizedEnabledProperty(getProfileSettingById(id));
  }

  @Override
  public List<String> getPropertySettingNames() {
    return getPropertySettings().stream().map(ProfilePropertySetting::getPropertyName).toList();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void hidePropertySetting(long userIdentityId, long profilePropertyId) {
    List<Long> hiddenProperties = getHiddenProfilePropertyIds(userIdentityId);
    hiddenProperties.remove(profilePropertyId);
    hiddenProperties.add(profilePropertyId);
    settingService.set(Context.USER.id(String.valueOf(userIdentityId)),
                       HIDDEN_PROFILE_PROPERTY_SETTINGS_SCOPE,
                       HIDDEN_PROFILE_PROPERTY_SETTINGS_KEY,
                       SettingValue.create(hiddenProperties.toString()));
    indexingService.reindex(ProfileIndexingServiceConnector.TYPE, String.valueOf(userIdentityId));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void showPropertySetting(long userIdentityId, long profilePropertyId) {
    List<Long> hiddenProperties = getHiddenProfilePropertyIds(userIdentityId);
    hiddenProperties.remove(profilePropertyId);
    settingService.set(Context.USER.id(String.valueOf(userIdentityId)),
                       HIDDEN_PROFILE_PROPERTY_SETTINGS_SCOPE,
                       HIDDEN_PROFILE_PROPERTY_SETTINGS_KEY,
                       SettingValue.create(hiddenProperties.toString()));
    indexingService.reindex(ProfileIndexingServiceConnector.TYPE, String.valueOf(userIdentityId));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List<Long> getHiddenProfilePropertyIds(long userIdentityId) {
    List<Long> hiddenProfileProperties = new ArrayList<>();
    SettingValue<?> settingValue = settingService.get(Context.USER.id(String.valueOf(userIdentityId)),
                                                      HIDDEN_PROFILE_PROPERTY_SETTINGS_SCOPE,
                                                      HIDDEN_PROFILE_PROPERTY_SETTINGS_KEY);
    if (settingValue == null) {
      return hiddenProfileProperties;
    }

    try {
      ObjectMapper mapper = new ObjectMapper();
      hiddenProfileProperties = mapper.readValue(settingValue.getValue().toString(), new TypeReference<ArrayList<Long>>() {
      });
    } catch (Exception e) {
      LOG.error("Error while parsing hidden properties setting of user: {}", userIdentityId, e);
    }
    return hiddenProfileProperties;
  }

  @Override
  public void start() {

    for (ProfilePropertyDatabaseInitializer plugin : profielPropertyPlugins) {
      try {
        plugin.init(this);
      } catch (Exception ex) {
        LOG.error("Failed start Profile properties Service , probably because of configuration error. Error occurs when initializing properties for {}",
                  plugin.getClass().getName(),
                  ex);
      }
    }

  }

  @Override
  public void stop() {
    // Nothing to stop
  }

  @Override
  public void addProfilePropertyPlugin(ComponentPlugin profilePropertyInitPlugin) {
    this.profielPropertyPlugins.add((ProfilePropertyDatabaseInitializer) profilePropertyInitPlugin);
  }

  @Override
  public boolean isDefaultProperties(ProfilePropertySetting propertySetting) {
    for (ProfilePropertyDatabaseInitializer plugin : profielPropertyPlugins) {
      if (plugin.getConfig().getProfileProperties() != null && !plugin.getConfig().getProfileProperties().isEmpty()
          && plugin.getConfig()
                   .getProfileProperties()
                   .stream()
                   .anyMatch(profileProperty -> profileProperty.getPropertyName().equals(propertySetting.getPropertyName()))) {
        return true;
      }
    }
    return false;
  }

  private void validatePropertySetting(ProfilePropertySetting profilePropertySetting) {
    if (profilePropertySetting == null) {
      throw new IllegalArgumentException("Profile property setting object is mandatory.");
    }

    if (StringUtils.isBlank(profilePropertySetting.getPropertyName())) {
      throw new IllegalArgumentException("Profile property name is mandatory.");
    }

    if (profilePropertySetting.isDropdownList() && !"text".equals(profilePropertySetting.getPropertyType())) {
      throw new IllegalArgumentException("Only text properties can be dropdown lists.");
    }
  }

  private void savePropertyOptionsTranslations(ProfilePropertySetting savedProfilePropertySetting,
                                               List<ProfilePropertyOption> newOptions,
                                               boolean update) {
    if (!savedProfilePropertySetting.isDropdownList()) {
      return;
    }
    List<ProfilePropertyOption> savedOptions =
                                             profileSettingStorage.getProfilePropertyOptions(savedProfilePropertySetting.getId(),
                                                                                             0,
                                                                                             0);
    for (int i = 0; i < savedOptions.size(); i++) {
      ProfilePropertyOption savedOption = savedOptions.get(i);
      ProfilePropertyOption newOption = i < newOptions.size() ? newOptions.get(i) : null;
      if (savedOption == null || newOption == null) {
        continue;
      }
      try {
        TranslationField translationField = translationService.getTranslationField(PROFILE_PROPERTY_OBJECT_TYPE,
                                                                                   savedOption.getId(),
                                                                                   PROFILE_PROPERTY_FIELD_NAME);
        Map<Locale, String> translations = newOption.getTranslations()
                                                    .entrySet()
                                                    .stream()
                                                    .collect(Collectors.toMap(entry -> Locale.forLanguageTag(entry.getKey()),
                                                                              Map.Entry::getValue));
        translationService.saveTranslationLabels(PROFILE_PROPERTY_OBJECT_TYPE,
                                                 savedOption.getId(),
                                                 PROFILE_PROPERTY_FIELD_NAME,
                                                 translations);
        if (update && translationField != null && !translationField.getLabels().equals(translations)) {
          listenerService.broadcast("property_options_updated", newOption, translationField.getLabels());
        }
      } catch (Exception e) {
        LOG.error("Error while saving translation labels for profile property option {}", savedOption.getId(), e);
      }
    }
  }

  private boolean isPropertySettingHiddenable(ProfilePropertySetting propertySetting) {
    return propertySetting != null
           && !nonHiddenableProps.contains(propertySetting.getPropertyName())
           && !propertySetting.isHasChildProperties()
           && propertySetting.isHiddenbale();
  }

  private boolean isGroupSynchronizedEnabledProperty(ProfilePropertySetting profilePropertySetting) {
    if (profilePropertySetting == null
        || synchronizedGroupDisabledProperties.contains(profilePropertySetting.getPropertyName())) {
      return false;
    }
    if (profilePropertySetting.getParentId() != null && profilePropertySetting.getParentId() > 0) {
      ProfilePropertySetting parent = profileSettingStorage.getProfileSettingById(profilePropertySetting.getParentId());
      return parent == null || !synchronizedGroupDisabledProperties.contains(parent.getPropertyName());
    }
    return true;
  }

  public boolean isUserCardFieldSettings(String propertyName) {
    SettingValue<?> userCardFirstFieldSetting = settingService.get(Context.GLOBAL,
                                                                   new Scope(Scope.GLOBAL.getName(), USER_CARD_SETTINGS),
                                                                   "UserCardFirstFieldSetting");
    SettingValue<?> userCardSecondFieldSetting = settingService.get(Context.GLOBAL,
                                                                   new Scope(Scope.GLOBAL.getName(), USER_CARD_SETTINGS),
                                                                   "UserCardSecondFieldSetting");
    SettingValue<?> userCardThirdFieldSetting = settingService.get(Context.GLOBAL,
                                                                   new Scope(Scope.GLOBAL.getName(), USER_CARD_SETTINGS),
                                                                   "UserCardThirdFieldSetting");
    return Stream.of(userCardFirstFieldSetting, userCardSecondFieldSetting, userCardThirdFieldSetting)
                 .filter(Objects::nonNull)
                 .anyMatch(setting -> propertyName.equals(setting.getValue()));                                            
  }

}
