/*
 * 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.ltr.store.rest;

import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.ltr.feature.Feature;
import org.apache.solr.ltr.store.FeatureStore;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.rest.BaseSolrResource;
import org.apache.solr.rest.ManagedResource;
import org.apache.solr.rest.ManagedResourceObserver;
import org.apache.solr.rest.ManagedResourceStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Managed resource for a storing a feature. */
public class ManagedFeatureStore extends ManagedResource
    implements ManagedResource.ChildResourceSupport {

  public static void registerManagedFeatureStore(
      SolrResourceLoader solrResourceLoader, ManagedResourceObserver managedResourceObserver) {
    solrResourceLoader
        .getManagedResourceRegistry()
        .registerManagedResource(
            REST_END_POINT, ManagedFeatureStore.class, managedResourceObserver);
  }

  public static ManagedFeatureStore getManagedFeatureStore(SolrCore core) {
    return (ManagedFeatureStore) core.getRestManager().getManagedResource(REST_END_POINT);
  }

  /** the feature store rest endpoint */
  public static final String REST_END_POINT = "/schema/feature-store";

  /** name of the attribute containing the feature class */
  static final String CLASS_KEY = "class";
  /** name of the attribute containing the feature name */
  static final String NAME_KEY = "name";
  /** name of the attribute containing the feature params */
  static final String PARAMS_KEY = "params";
  /** name of the attribute containing the feature store used */
  static final String FEATURE_STORE_NAME_KEY = "store";

  private final Map<String, FeatureStore> stores = new HashMap<>();

  /** Managed feature store: the name of the attribute containing all the feature stores */
  private static final String FEATURE_STORE_JSON_FIELD = "featureStores";

  /**
   * Managed feature store: the name of the attribute containing all the features of a feature store
   */
  private static final String FEATURES_JSON_FIELD = "features";

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  public ManagedFeatureStore(
      String resourceId, SolrResourceLoader loader, ManagedResourceStorage.StorageIO storageIO)
      throws SolrException {
    super(resourceId, loader, storageIO);
  }

  public Map<String, FeatureStore> getStores() {
    return stores;
  }

  public synchronized FeatureStore getFeatureStore(String name) {
    if (name == null) {
      name = FeatureStore.DEFAULT_FEATURE_STORE_NAME;
    }
    if (!stores.containsKey(name)) {
      stores.put(name, new FeatureStore(name));
    }
    return stores.get(name);
  }

  @Override
  protected void onManagedDataLoadedFromStorage(NamedList<?> managedInitArgs, Object managedData)
      throws SolrException {

    stores.clear();
    log.info("------ managed feature ~ loading ------");
    if (managedData instanceof List) {
      @SuppressWarnings("unchecked")
      final List<Map<String, Object>> up = (List<Map<String, Object>>) managedData;
      for (final Map<String, Object> u : up) {
        final String featureStore = (String) u.get(FEATURE_STORE_NAME_KEY);
        addFeature(u, featureStore);
      }
    }
  }

  public synchronized void addFeature(Map<String, Object> map, String featureStore) {
    log.info("register feature based on {}", map);
    final FeatureStore fstore = getFeatureStore(featureStore);
    final Feature feature = fromFeatureMap(solrResourceLoader, map);
    fstore.add(feature);
  }

  @SuppressWarnings("unchecked")
  @Override
  public Object applyUpdatesToManagedData(Object updates) {
    if (updates instanceof List) {
      final List<Map<String, Object>> up = (List<Map<String, Object>>) updates;
      for (final Map<String, Object> u : up) {
        final String featureStore = (String) u.get(FEATURE_STORE_NAME_KEY);
        addFeature(u, featureStore);
      }
    }

    if (updates instanceof Map) {
      // a unique feature
      Map<String, Object> updatesMap = (Map<String, Object>) updates;
      final String featureStore = (String) updatesMap.get(FEATURE_STORE_NAME_KEY);
      addFeature(updatesMap, featureStore);
    }

    final List<Object> features = new ArrayList<>();
    for (final FeatureStore fs : stores.values()) {
      features.addAll(featuresAsManagedResources(fs));
    }
    return features;
  }

  @Override
  public synchronized void doDeleteChild(BaseSolrResource endpoint, String childId) {
    if (stores.containsKey(childId)) {
      stores.remove(childId);
    }
    storeManagedData(applyUpdatesToManagedData(null));
  }

  /**
   * Called to retrieve a named part (the given childId) of the resource at the given endpoint.
   * Note: since we have a unique child feature store we ignore the childId.
   */
  @Override
  public void doGet(BaseSolrResource endpoint, String childId) {
    final SolrQueryResponse response = endpoint.getSolrResponse();

    // If no feature store specified, show all the feature stores available
    if (childId == null) {
      response.add(FEATURE_STORE_JSON_FIELD, stores.keySet());
    } else {
      // check for non-existent feature store name
      if (!stores.containsKey(childId)) {
        throw new SolrException(
            SolrException.ErrorCode.BAD_REQUEST, "Missing feature store: " + childId);
      }
      final FeatureStore store = getFeatureStore(childId);
      response.add(FEATURES_JSON_FIELD, featuresAsManagedResources(store));
    }
  }

  private static List<Object> featuresAsManagedResources(FeatureStore store) {
    final List<Feature> storedFeatures = store.getFeatures();
    final List<Object> features = new ArrayList<>(storedFeatures.size());
    for (final Feature f : storedFeatures) {
      final LinkedHashMap<String, Object> m = toFeatureMap(f);
      m.put(FEATURE_STORE_NAME_KEY, store.getName());
      features.add(m);
    }
    return features;
  }

  private static LinkedHashMap<String, Object> toFeatureMap(Feature feat) {
    final LinkedHashMap<String, Object> o = new LinkedHashMap<>(4, 1.0f);
    // 4 = 3 + 1 i.e. 1 extra for caller to add store
    o.put(NAME_KEY, feat.getName());
    o.put(CLASS_KEY, feat.getClass().getName());
    o.put(PARAMS_KEY, feat.paramsToMap());
    return o;
  }

  private static Feature fromFeatureMap(
      SolrResourceLoader solrResourceLoader, Map<String, Object> featureMap) {
    final String className = (String) featureMap.get(CLASS_KEY);

    final String name = (String) featureMap.get(NAME_KEY);

    @SuppressWarnings("unchecked")
    final Map<String, Object> params = (Map<String, Object>) featureMap.get(PARAMS_KEY);

    return Feature.getInstance(solrResourceLoader, className, name, params);
  }
}
