/*
 * 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.solr.cloud.autoscaling;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.lucene.util.IOUtils;
import org.apache.solr.client.solrj.cloud.autoscaling.AlreadyExistsException;
import org.apache.solr.client.solrj.cloud.autoscaling.BadVersionException;
import org.apache.solr.client.solrj.cloud.DistribStateManager;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.cloud.autoscaling.TriggerEventType;

import org.apache.solr.client.solrj.cloud.autoscaling.VersionedData;
import org.apache.solr.common.AlreadyClosedException;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base class for {@link org.apache.solr.cloud.autoscaling.AutoScaling.Trigger} implementations.
 * It handles state snapshot / restore in ZK.
 */
public abstract class TriggerBase implements AutoScaling.Trigger {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  protected final String name;
  protected SolrCloudManager cloudManager;
  protected SolrResourceLoader loader;
  protected DistribStateManager stateManager;
  protected final Map<String, Object> properties = new HashMap<>();
  /**
   * Set of valid property names. Subclasses may add to this set
   * using {@link TriggerUtils#validProperties(Set, String...)}
   */
  protected final Set<String> validProperties = new HashSet<>();
  /**
   * Set of required property names. Subclasses may add to this set
   * using {@link TriggerUtils#requiredProperties(Set, Set, String...)}
   * (required properties are also valid properties).
   */
  protected final Set<String> requiredProperties = new HashSet<>();
  protected final TriggerEventType eventType;
  protected int waitForSecond;
  protected Map<String,Object> lastState;
  protected final AtomicReference<AutoScaling.TriggerEventProcessor> processorRef = new AtomicReference<>();
  protected List<TriggerAction> actions;
  protected boolean enabled;
  protected boolean isClosed;


  protected TriggerBase(TriggerEventType eventType, String name) {
    this.eventType = eventType;
    this.name = name;

    // subclasses may modify this set to include other supported properties
    TriggerUtils.validProperties(validProperties, "name", "class", "event", "enabled", "waitFor", "actions");
  }

  @Override
  public void configure(SolrResourceLoader loader, SolrCloudManager cloudManager, Map<String, Object> properties) throws TriggerValidationException {
    this.cloudManager = cloudManager;
    this.loader = loader;
    this.stateManager = cloudManager.getDistribStateManager();
    if (properties != null) {
      this.properties.putAll(properties);
    }
    this.enabled = Boolean.parseBoolean(String.valueOf(this.properties.getOrDefault("enabled", "true")));
    this.waitForSecond = ((Number) this.properties.getOrDefault("waitFor", -1L)).intValue();
    List<Map<String, Object>> o = (List<Map<String, Object>>) properties.get("actions");
    if (o != null && !o.isEmpty()) {
      actions = new ArrayList<>(3);
      for (Map<String, Object> map : o) {
        TriggerAction action = null;
        try {
          action = loader.newInstance((String)map.get("class"), TriggerAction.class);
        } catch (Exception e) {
          throw new TriggerValidationException("action", "exception creating action " + map + ": " + e.toString());
        }
        action.configure(loader, cloudManager, map);
        actions.add(action);
      }
    } else {
      actions = Collections.emptyList();
    }


    Map<String, String> results = new HashMap<>();
    TriggerUtils.checkProperties(this.properties, results, requiredProperties, validProperties);
    if (!results.isEmpty()) {
      throw new TriggerValidationException(name, results);
    }
  }

  @Override
  public void init() throws Exception {
    try {
      if (!stateManager.hasData(ZkStateReader.SOLR_AUTOSCALING_TRIGGER_STATE_PATH)) {
        stateManager.makePath(ZkStateReader.SOLR_AUTOSCALING_TRIGGER_STATE_PATH);
      }
    } catch (AlreadyExistsException e) {
      // ignore
    } catch (InterruptedException | KeeperException | IOException e) {
      log.warn("Exception checking ZK path " + ZkStateReader.SOLR_AUTOSCALING_TRIGGER_STATE_PATH, e);
      throw e;
    }
    for (TriggerAction action : actions) {
      action.init();
    }
  }

  @Override
  public void setProcessor(AutoScaling.TriggerEventProcessor processor) {
    processorRef.set(processor);
  }

  @Override
  public AutoScaling.TriggerEventProcessor getProcessor() {
    return processorRef.get();
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public TriggerEventType getEventType() {
    return eventType;
  }

  @Override
  public boolean isEnabled() {
    return enabled;
  }

  @Override
  public int getWaitForSecond() {
    return waitForSecond;
  }

  @Override
  public Map<String, Object> getProperties() {
    return properties;
  }

  @Override
  public List<TriggerAction> getActions() {
    return actions;
  }

  @Override
  public boolean isClosed() {
    synchronized (this) {
      return isClosed;
    }
  }

  @Override
  public void close() throws IOException {
    synchronized (this) {
      isClosed = true;
      IOUtils.closeWhileHandlingException(actions);
    }
  }

  @Override
  public int hashCode() {
    return Objects.hash(name, properties);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (obj.getClass().equals(this.getClass())) {
      TriggerBase that = (TriggerBase) obj;
      return this.name.equals(that.name)
          && this.properties.equals(that.properties);
    }
    return false;
  }

  /**
   * Prepare and return internal state of this trigger in a format suitable for persisting in ZK.
   * @return map of internal state properties. Note: values must be supported by {@link Utils#toJSON(Object)}.
   */
  protected abstract Map<String,Object> getState();

  /**
   * Restore internal state of this trigger from properties retrieved from ZK.
   * @param state never null but may be empty.
   */
  protected abstract void setState(Map<String,Object> state);

  /**
   * Returns an immutable deep copy of this trigger's state, suitible for saving.
   * This method is public only for tests that wish to do grey-box introspection
   *
   * @see #getState
   * @lucene.internal
   */
  public Map<String,Object> deepCopyState() {
    return Utils.getDeepCopy(getState(), 10, false, true);
  }
  
  @Override
  public void saveState() {
    Map<String,Object> state = deepCopyState();
    if (lastState != null && lastState.equals(state)) {
      // skip saving if identical
      return;
    }
    byte[] data = Utils.toJSON(state);
    String path = ZkStateReader.SOLR_AUTOSCALING_TRIGGER_STATE_PATH + "/" + getName();
    try {
      if (stateManager.hasData(path)) {
        // update
        stateManager.setData(path, data, -1);
      } else {
        // create
        stateManager.createData(path, data, CreateMode.PERSISTENT);
      }
      lastState = state;
    } catch (AlreadyExistsException e) {
      
    } catch (InterruptedException | BadVersionException | IOException | KeeperException e) {
      log.warn("Exception updating trigger state '" + path + "'", e);
    }
  }

  @Override
  public void restoreState() {
    byte[] data = null;
    String path = ZkStateReader.SOLR_AUTOSCALING_TRIGGER_STATE_PATH + "/" + getName();
    try {
      if (stateManager.hasData(path)) {
        VersionedData versionedData = stateManager.getData(path);
        data = versionedData.getData();
      }
    } catch (AlreadyClosedException e) {
     
    } catch (Exception e) {
      log.warn("Exception getting trigger state '" + path + "'", e);
    }
    if (data != null) {
      Map<String, Object> restoredState = (Map<String, Object>)Utils.fromJSON(data);
      // make sure lastState is sorted
      restoredState = Utils.getDeepCopy(restoredState, 10, false, true);
      setState(restoredState);
      lastState = restoredState;
    }
  }
}
