package org.infinispan.server.eventlogger;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.infinispan.Cache;
import org.infinispan.commons.CacheException;
import org.infinispan.commons.util.Util;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.query.Search;
import org.infinispan.query.dsl.FilterConditionContext;
import org.infinispan.query.dsl.QueryFactory;
import org.infinispan.query.dsl.SortOrder;
import org.infinispan.util.TimeService;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
import org.infinispan.util.logging.events.EventLog;
import org.infinispan.util.logging.events.EventLogCategory;
import org.infinispan.util.logging.events.EventLogLevel;
import org.infinispan.util.logging.events.EventLogger;


/**
 * ServerEventLogger. This event logger takes care of maintaining the server event log cache and
 * provides methods for querying its contents across all nodes. For resilience, the event log is
 * stored in a local, bounded, persistent cache and distributed executors are used to gather logs
 * from all the nodes in the cluster.
 *
 * @author Tristan Tarrant
 * @since 8.2
 */
public class ServerEventLogger implements EventLogger {
   public static final String EVENT_LOG_CACHE = "___event_log_cache";
   public static final Log log = LogFactory.getLog(ServerEventLogger.class);
   private final EmbeddedCacheManager cacheManager;
   private final TimeService timeService;
   private Cache<UUID, ServerEventImpl> eventCache;

   ServerEventLogger(EmbeddedCacheManager cacheManager, TimeService timeService) {
      this.cacheManager = cacheManager;
      this.timeService = timeService;
   }

   private Cache<UUID, ServerEventImpl> getEventCache() {
      if (eventCache == null) {
         eventCache = cacheManager.getCache(EVENT_LOG_CACHE);
      }
      return eventCache;
   }

   @Override
   public void log(EventLogLevel level, EventLogCategory category, String message) {
      textLog(level, category, message);
      eventLog(new ServerEventImpl(level, category, timeService.instant(), message));
   }

   void textLog(EventLogLevel level, EventLogCategory category, String message) {
      LogFactory.getLogger(category.toString()).log(level.toLoggerLevel(), message);
   }

   void eventLog(ServerEventImpl event) {
      getEventCache().putAsync(Util.threadLocalRandomUUID(), event);
   }

   @Override
   public EventLogger scope(String scope) {
      return new DecoratedServerEventLogger(this).scope(scope);
   }

   @Override
   public EventLogger context(String cacheName) {
      return new DecoratedServerEventLogger(this).context(cacheName);
   }

   @Override
   public EventLogger detail(String detail) {
      return new DecoratedServerEventLogger(this).detail(detail);
   }

   @Override
   public EventLogger who(String s) {
      return new DecoratedServerEventLogger(this).who(s);
   }

   TimeService getTimeService() {
      return timeService;
   }

   @Override
   public List<EventLog> getEvents(Instant start, int count, Optional<EventLogCategory> category, Optional<EventLogLevel> level) {
      List<EventLog> events = Collections.synchronizedList(new ArrayList<>());
      AtomicReference<Throwable> throwable = new AtomicReference<>();
      try {
         cacheManager.executor().submitConsumer(m -> {
            Cache<Object, Object> cache = m.getCache(EVENT_LOG_CACHE);
            QueryFactory queryFactory = Search.getQueryFactory(cache);
            FilterConditionContext filter = queryFactory.from(ServerEventImpl.class)
                  .orderBy("when", SortOrder.DESC)
                  .maxResults(count)
                  .having("when").lte(start);
            category.map(c -> filter.and(queryFactory.having("category").eq(c)));
            level.map(l -> filter.and(queryFactory.having("level").eq(l)));
            List<EventLog> nodeEvents = filter.toBuilder().build().list();
            return nodeEvents;
         }, (address, nodeEvents, t) -> {
            if (t == null) {
               events.addAll(nodeEvents);
            } else {
               throwable.set(t);
            }
         }).get(1, TimeUnit.MINUTES);
         Throwable th = throwable.get();
         if (th != null) {
            throw new CacheException(th);
         }
      } catch (CacheException e) {
         log.debug("Could not retrieve events", e);
         throw e;
      } catch (Exception e) {
         log.debug("Could not retrieve events", e);
         throw new CacheException(e);
      }
      Collections.sort(events);
      return events.subList(0, Math.min(events.size(), count));
   }

}
