/*
 * 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.servicecomb.localregistry;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.apache.servicecomb.config.YAMLUtil;
import org.apache.servicecomb.foundation.common.utils.BeanUtils;
import org.apache.servicecomb.foundation.common.utils.JvmUtils;
import org.apache.servicecomb.localregistry.RegistryBean.Instance;
import org.apache.servicecomb.registry.api.registry.FindInstancesResponse;
import org.apache.servicecomb.registry.api.registry.Microservice;
import org.apache.servicecomb.registry.api.registry.MicroserviceFactory;
import org.apache.servicecomb.registry.api.registry.MicroserviceInstance;
import org.apache.servicecomb.registry.api.registry.MicroserviceInstances;
import org.apache.servicecomb.swagger.SwaggerUtils;
import org.apache.servicecomb.swagger.generator.SwaggerGenerator;

import com.google.common.annotations.VisibleForTesting;

public class LocalRegistryStore {
  private static final String REGISTRY_FILE_NAME = "registry.yaml";

  public static final LocalRegistryStore INSTANCE = new LocalRegistryStore();

  private Microservice selfMicroservice;

  private MicroserviceInstance selfMicroserviceInstance;

  // key is microservice id
  private final Map<String, Microservice> microserviceMap = new ConcurrentHashMap<>();

  // first key is microservice id
  // second key is instance id
  private final Map<String, Map<String, MicroserviceInstance>> microserviceInstanceMap = new ConcurrentHashMap<>();

  private LocalRegistryStore() {

  }

  @VisibleForTesting
  public void initSelfWithMocked(Microservice microservice, MicroserviceInstance microserviceInstance) {
    this.selfMicroservice = microservice;
    this.selfMicroserviceInstance = microserviceInstance;
  }

  public void init() {
    MicroserviceFactory microserviceFactory = new MicroserviceFactory();
    selfMicroservice = microserviceFactory.create();
    selfMicroserviceInstance = selfMicroservice.getInstance();
    microserviceMap.clear();
    microserviceInstanceMap.clear();
  }

  public void run() {
    selfMicroservice.setServiceId("[local]-[" + selfMicroservice.getAppId()
        + "]-[" + selfMicroservice.getServiceName() + "]");
    selfMicroserviceInstance.setInstanceId(selfMicroservice.getServiceId());
    selfMicroserviceInstance.setServiceId(selfMicroservice.getServiceId());

    List<RegistryBean> beans = loadYamlBeans();
    BeanUtils.getBeansOfType(RegistryBean.class).forEach((key, value) -> beans.add(value));

    initRegistryFromBeans(beans);

    addSelf();
  }

  private void addSelf() {
    microserviceMap.put(selfMicroservice.getServiceId(), selfMicroservice);
    Map<String, MicroserviceInstance> selfInstanceMap = new HashMap<>(1);
    selfInstanceMap.put(selfMicroserviceInstance.getInstanceId(), selfMicroserviceInstance);
    microserviceInstanceMap.put(selfMicroservice.getServiceId(), selfInstanceMap);
  }

  public Microservice getSelfMicroservice() {
    return selfMicroservice;
  }

  public MicroserviceInstance getSelfMicroserviceInstance() {
    return selfMicroserviceInstance;
  }

  private List<RegistryBean> loadYamlBeans() {
    List<RegistryBean> beans = new ArrayList<>();

    try {
      ClassLoader loader = JvmUtils.findClassLoader();
      Enumeration<URL> urls = loader.getResources(REGISTRY_FILE_NAME);

      while (urls.hasMoreElements()) {
        URL url = urls.nextElement();

        try (InputStream is = url.openStream()) {
          if (is != null) {
            beans.addAll(initFromData(is));
          }
        }
      }
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }

    return beans;
  }

  private List<RegistryBean> initFromData(InputStream is) {
    Map<String, Object> data = YAMLUtil.yaml2Properties(is);
    return initFromData(data);
  }

  @SuppressWarnings("unchecked")
  private List<RegistryBean> initFromData(Map<String, Object> data) {
    List<RegistryBean> beans = new ArrayList<>();

    for (Entry<String, Object> entry : data.entrySet()) {
      String name = entry.getKey();
      List<Map<String, Object>> serviceConfigs = (List<Map<String, Object>>) entry.getValue();
      for (Map<String, Object> serviceConfig : serviceConfigs) {
        beans.add(RegistryBean.buildFromYamlModel(name, serviceConfig));
      }
    }
    return beans;
  }

  private void initRegistryFromBeans(List<RegistryBean> beans) {
    beans.forEach((bean -> {
      Microservice microservice = new Microservice();
      microservice.setAppId(bean.getAppId());
      microservice.setServiceName(bean.getServiceName());
      microservice.setVersion(bean.getVersion());
      microservice.setServiceId(bean.getId());
      microservice.setSchemas(bean.getSchemaIds());
      addSchemaInterface(bean, microservice);
      microserviceMap.put(microservice.getServiceId(), microservice);
      addInstances(bean, microservice);
    }));
  }

  private void addSchemaInterface(RegistryBean bean, Microservice microservice) {
    bean.getSchemaInterfaces().forEach((k, v) -> {
      SwaggerGenerator generator = SwaggerGenerator.create(v);
      microservice.getSchemaMap().put(k, SwaggerUtils.swaggerToString(generator.generate()));
    });
  }

  private void addInstances(RegistryBean bean, Microservice microservice) {
    Map<String, MicroserviceInstance> instanceMap = new ConcurrentHashMap<>();
    microserviceInstanceMap.put(microservice.getServiceId(), instanceMap);

    for (Instance item : bean.getInstances().getInstances()) {
      MicroserviceInstance instance = new MicroserviceInstance();
      instance.setInstanceId(UUID.randomUUID().toString());
      instance.setEndpoints(item.getEndpoints());
      instance.setServiceId(microservice.getServiceId());
      instanceMap.put(instance.getInstanceId(), instance);
    }
  }

  public Microservice getMicroservice(String microserviceId) {
    return microserviceMap.get(microserviceId);
  }

  public List<Microservice> getAllMicroservices() {
    return microserviceMap.values().stream().collect(Collectors.toList());
  }

  public String getSchema(String microserviceId, String schemaId) {
    Microservice microservice = microserviceMap.get(microserviceId);
    if (microservice == null) {
      return null;
    }
    return microserviceMap.get(microserviceId).getSchemaMap().get(schemaId);
  }

  public MicroserviceInstance findMicroserviceInstance(String serviceId, String instanceId) {
    Map<String, MicroserviceInstance> microserviceInstance = microserviceInstanceMap.get(serviceId);
    if (microserviceInstance == null) {
      return null;
    }
    return microserviceInstanceMap.get(serviceId).get(instanceId);
  }

  // local registry do not care about version and revision
  public MicroserviceInstances findServiceInstances(String appId, String serviceName, String versionRule) {
    MicroserviceInstances microserviceInstances = new MicroserviceInstances();
    FindInstancesResponse findInstancesResponse = new FindInstancesResponse();
    List<MicroserviceInstance> instances = new ArrayList<>();

    Collectors.toList();
    microserviceInstanceMap.values().forEach(
        allInstances -> allInstances.values().stream().filter(
            aInstance -> {
              Microservice service = microserviceMap.get(aInstance.getServiceId());
              return service.getAppId().equals(appId) && service.getServiceName().equals(serviceName);
            }
        ).forEach(instances::add));
    if (instances.isEmpty()) {
      microserviceInstances.setMicroserviceNotExist(true);
    } else {
      findInstancesResponse.setInstances(instances);
      microserviceInstances.setMicroserviceNotExist(false);
      microserviceInstances.setInstancesResponse(findInstancesResponse);
    }
    return microserviceInstances;
  }
}
