/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.ambari.server.orm.dao;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.orm.RequiresSession;
import org.apache.ambari.server.orm.cache.ConfigGroupHostMapping;
import org.apache.ambari.server.orm.cache.ConfigGroupHostMappingImpl;
import org.apache.ambari.server.orm.entities.ConfigGroupEntity;
import org.apache.ambari.server.orm.entities.ConfigGroupHostMappingEntity;
import org.apache.ambari.server.orm.entities.ConfigGroupHostMappingEntityPK;
import org.apache.ambari.server.orm.entities.HostEntity;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.Host;
import org.apache.ambari.server.state.cluster.ClusterFactory;
import org.apache.ambari.server.state.configgroup.ConfigGroup;
import org.apache.ambari.server.state.configgroup.ConfigGroupFactory;
import org.apache.ambari.server.state.host.HostFactory;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.Predicate;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.persist.Transactional;

@Singleton
public class ConfigGroupHostMappingDAO {
  @Inject
  Provider<EntityManager> entityManagerProvider;
  @Inject
  DaoUtils daoUtils;
  @Inject
  private ConfigGroupFactory configGroupFactory;
  @Inject
  private ClusterFactory clusterFactory;
  @Inject
  private HostFactory hostFactory;
  @Inject
  Clusters clusters;

  private final ReadWriteLock gl = new ReentrantReadWriteLock();

  private Map<Long, Set<ConfigGroupHostMapping>> configGroupHostMappingByHost;

  private volatile boolean cacheLoaded;


  private void populateCache() {

    if (!cacheLoaded) {
      gl.writeLock().lock();
      try {
        if (!cacheLoaded) {
          if (configGroupHostMappingByHost == null) {
            configGroupHostMappingByHost = new HashMap<>();

            TypedQuery<ConfigGroupHostMappingEntity> query = entityManagerProvider.get().createQuery(
                "SELECT entity FROM ConfigGroupHostMappingEntity entity",
                ConfigGroupHostMappingEntity.class);

            List<ConfigGroupHostMappingEntity> configGroupHostMappingEntities = daoUtils.selectList(query);

            for (ConfigGroupHostMappingEntity configGroupHostMappingEntity : configGroupHostMappingEntities) {

              Set<ConfigGroupHostMapping> setByHost = configGroupHostMappingByHost.get((configGroupHostMappingEntity.getHostId()));

              if (setByHost == null) {
                setByHost = new HashSet<>();
                configGroupHostMappingByHost.put(configGroupHostMappingEntity.getHostId(), setByHost);
              }

              ConfigGroupHostMapping configGroupHostMapping = buildConfigGroupHostMapping(configGroupHostMappingEntity);
              setByHost.add(configGroupHostMapping);
            }
          }
          cacheLoaded = true;
        }
      } finally {
        gl.writeLock().unlock();
      }


    }

  }

  /**
   * Return entity object which can be used for operations like remove
   * directly.
   * @param configGroupHostMappingEntityPK
   * @return
   */
  @RequiresSession
  public ConfigGroupHostMappingEntity findByPK(final ConfigGroupHostMappingEntityPK
        configGroupHostMappingEntityPK) {

    return entityManagerProvider.get()
      .find(ConfigGroupHostMappingEntity.class, configGroupHostMappingEntityPK);
  }

  @RequiresSession
  public Set<ConfigGroupHostMapping> findByHostId(Long hostId) {

    populateCache();

    if (!configGroupHostMappingByHost.containsKey(hostId)) {
      return null;
    }

    Set<ConfigGroupHostMapping> set = new HashSet<>(configGroupHostMappingByHost.get(hostId));

    return set;

  }

  @RequiresSession
  public Set<ConfigGroupHostMapping> findByGroup(final Long groupId) {

    populateCache();

    Set<ConfigGroupHostMapping> result = new HashSet<>();

    for (Set<ConfigGroupHostMapping> item : configGroupHostMappingByHost.values()) {

      Set<ConfigGroupHostMapping> setByHost = new HashSet<>(item);

      CollectionUtils.filter(setByHost, new Predicate() {

        @Override
        public boolean evaluate(Object arg0) {
          return ((ConfigGroupHostMapping) arg0).getConfigGroupId().equals(groupId);
        }
      });

      result.addAll(setByHost);

    }

    return result;

  }

  @RequiresSession
  public List<ConfigGroupHostMappingEntity> findAll() {
    return daoUtils.selectAll(entityManagerProvider.get(), ConfigGroupHostMappingEntity.class);
  }

  @Transactional
  public void create(ConfigGroupHostMappingEntity
                         configGroupHostMappingEntity) {
    populateCache();

    entityManagerProvider.get().persist(configGroupHostMappingEntity);

    //create in cache
    Set<ConfigGroupHostMapping> set = configGroupHostMappingByHost.get(configGroupHostMappingEntity.getHostId());
    if (set == null){
      set = new HashSet<>();
      configGroupHostMappingByHost.put(configGroupHostMappingEntity.getHostId(), set);
    }

    set.add(buildConfigGroupHostMapping(configGroupHostMappingEntity));
  }

  @Transactional
  public ConfigGroupHostMappingEntity merge(ConfigGroupHostMappingEntity configGroupHostMappingEntity) {

    populateCache();

    Set<ConfigGroupHostMapping> set = configGroupHostMappingByHost.get(configGroupHostMappingEntity.getHostId());
    if (set == null){
      set = new HashSet<>();
      configGroupHostMappingByHost.put(configGroupHostMappingEntity.getHostId(), set);
    }

    //Update object in set
    set.remove(buildConfigGroupHostMapping(configGroupHostMappingEntity));
    set.add(buildConfigGroupHostMapping(configGroupHostMappingEntity));


    return entityManagerProvider.get().merge(configGroupHostMappingEntity);
  }

  @Transactional
  public void refresh(ConfigGroupHostMappingEntity
                         configGroupHostMappingEntity) {
    cacheLoaded = false;
    populateCache();

    entityManagerProvider.get().refresh(configGroupHostMappingEntity);
  }

  @Transactional
  public void remove(final ConfigGroupHostMappingEntity
                         configGroupHostMappingEntity) {

    populateCache();

    entityManagerProvider.get().remove(merge(configGroupHostMappingEntity));

    Set<ConfigGroupHostMapping> setByHost = configGroupHostMappingByHost.get(configGroupHostMappingEntity.getHostId());

    if (setByHost != null) {
      CollectionUtils.filter(setByHost, new Predicate() {

        @Override
        public boolean evaluate(Object arg0) {
          return !((ConfigGroupHostMapping) arg0).getConfigGroupId().
              equals(configGroupHostMappingEntity.getConfigGroupId());
        }
      });
    }
  }

  @Transactional
  public void removeByPK(final ConfigGroupHostMappingEntityPK
                         configGroupHostMappingEntityPK) {
    populateCache();

    entityManagerProvider.get().remove(findByPK(configGroupHostMappingEntityPK));

    Set<ConfigGroupHostMapping> setByHost = configGroupHostMappingByHost.get(configGroupHostMappingEntityPK.getHostId());

    if (setByHost != null) {
      CollectionUtils.filter(setByHost, new Predicate() {

        @Override
        public boolean evaluate(Object arg0) {
          return !((ConfigGroupHostMapping) arg0).getConfigGroupId().
              equals(configGroupHostMappingEntityPK.getConfigGroupId());
        }
      });
    }

  }

  @Transactional
  public void removeAllByGroup(final Long groupId) {
    populateCache();

    TypedQuery<Long> query = entityManagerProvider.get().createQuery
      ("DELETE FROM ConfigGroupHostMappingEntity confighosts WHERE " +
        "confighosts.configGroupId = ?1", Long.class);

    daoUtils.executeUpdate(query, groupId);
    // Flush to current transaction required in order to avoid Eclipse link
    // from re-ordering delete
    entityManagerProvider.get().flush();

    for (Set<ConfigGroupHostMapping> setByHost : configGroupHostMappingByHost.values()) {

      CollectionUtils.filter(setByHost, new Predicate() {

        @Override
        public boolean evaluate(Object arg0) {
          return !((ConfigGroupHostMapping) arg0).getConfigGroupId().equals(groupId);
        }
      });
    }

  }

  @Transactional
  public void removeAllByHost(Long hostId) {
    TypedQuery<String> query = entityManagerProvider.get().createQuery
      ("DELETE FROM ConfigGroupHostMappingEntity confighosts WHERE " +
        "confighosts.hostId = ?1", String.class);

    daoUtils.executeUpdate(query, hostId);


    Set<ConfigGroupHostMapping> setByHost = configGroupHostMappingByHost.get(hostId);

    setByHost.clear();
  }

  private ConfigGroupHostMapping buildConfigGroupHostMapping(
      ConfigGroupHostMappingEntity configGroupHostMappingEntity) {

    ConfigGroupHostMappingImpl configGroupHostMapping = new ConfigGroupHostMappingImpl();
    configGroupHostMapping.setConfigGroup(buildConfigGroup(configGroupHostMappingEntity.getConfigGroupEntity()));
    configGroupHostMapping.setConfigGroupId(configGroupHostMappingEntity.getConfigGroupId());
    configGroupHostMapping.setHost(buildHost(configGroupHostMappingEntity.getHostEntity()));
    configGroupHostMapping.setHostId(configGroupHostMappingEntity.getHostId());

    return configGroupHostMapping;
  }

  private ConfigGroup buildConfigGroup(ConfigGroupEntity configGroupEntity) {
    Cluster cluster = null;
    try {
      cluster = clusters.getClusterById(configGroupEntity.getClusterId());
    } catch (AmbariException e) {
      //almost impossible
    }
    ConfigGroup configGroup = configGroupFactory.createExisting(cluster, configGroupEntity);

    return configGroup;
  }

  private Host buildHost(HostEntity hostEntity) {
    Host host = hostFactory.create(hostEntity);
    return host;
  }
}
