/*
 * 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.knox.gateway.service.metadata;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;

import java.io.IOException;
import java.nio.file.Paths;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;

import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;

import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.service.definition.Metadata;
import org.apache.knox.gateway.service.definition.ServiceDefinitionPair;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.ServerInfoService;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.registry.ServiceDefinitionRegistry;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.services.topology.TopologyService;
import org.apache.knox.gateway.topology.Service;
import org.apache.knox.gateway.topology.Topology;
import org.apache.knox.gateway.util.X509CertificateUtil;

@Singleton
@Path("/api/v1/metadata")
public class KnoxMetadataResource {
  private static final MetadataServiceMessages LOG = MessagesFactory.get(MetadataServiceMessages.class);
  private static final String SNAPSHOT_VERSION_POSTFIX = "-SNAPSHOT";
  private static final Set<String> UNREAL_SERVICES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("NAMENODE", "JOBTRACKER", "RESOURCEMANAGERAPI")));

  private Set<String> pinnedTopologies;
  private java.nio.file.Path pemFilePath;
  private java.nio.file.Path jksFilePath;

  @Context
  private HttpServletRequest request;

  @GET
  @Produces({ APPLICATION_JSON, APPLICATION_XML })
  @Path("info")
  public GeneralProxyInformation getGeneralProxyInformation() {
    final GeneralProxyInformation proxyInfo = new GeneralProxyInformation();
    final GatewayServices gatewayServices = (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
    if (gatewayServices != null) {
      final ServerInfoService serviceInfoService = gatewayServices.getService(ServiceType.SERVER_INFO_SERVICE);
      final String versionInfo = serviceInfoService.getBuildVersion() + " (hash=" + serviceInfoService.getBuildHash() + ")";
      proxyInfo.setVersion(versionInfo);
      proxyInfo.setAdminApiBookUrl(
          String.format(Locale.ROOT, "https://knox.apache.org/books/knox-%s/user-guide.html#Admin+API", getAdminApiBookVersion(serviceInfoService.getBuildVersion())));
      final GatewayConfig config = (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
      proxyInfo.setAdminUiUrl(getBaseGatewayUrl(config) + "/manager/admin-ui/");
    }

    return proxyInfo;
  }

  private String getAdminApiBookVersion(String buildVersion) {
    return buildVersion.replaceAll(SNAPSHOT_VERSION_POSTFIX, "").replaceAll("\\.", "-");
  }

  @GET
  @Produces(APPLICATION_OCTET_STREAM)
  @Path("publicCert")
  public Response getPublicCertification(@QueryParam("type") @DefaultValue("pem") String certType) {
    final GatewayServices gatewayServices = (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
    if (gatewayServices != null) {
      final GatewayConfig config = (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
      final Certificate certificate = getPublicCertificate(gatewayServices, config);
      if (certificate != null) {
        if ("pem".equals(certType)) {
          generateCertificatePem(certificate, config);
          return generateSuccessFileDownloadResponse(pemFilePath);
        } else if ("jks".equals(certType)) {
          generateCertificateJks(certificate, config);
          return generateSuccessFileDownloadResponse(jksFilePath);
        } else {
          return generateFailureFileDownloadResponse(Status.BAD_REQUEST, "Invalid certification type provided!");
        }
      }
    }
    return generateFailureFileDownloadResponse(Status.SERVICE_UNAVAILABLE, "Could not generate public certificate");
  }

  private Response generateSuccessFileDownloadResponse(java.nio.file.Path publicCertFilePath) {
    final ResponseBuilder responseBuilder = Response.ok(publicCertFilePath.toFile());
    responseBuilder.header("Content-Disposition", "attachment;filename=" + publicCertFilePath.getFileName().toString());
    return responseBuilder.build();
  }

  private Response generateFailureFileDownloadResponse(Status status, String errorMessage) {
    final ResponseBuilder responseBuilder = Response.status(status);
    responseBuilder.entity(errorMessage);
    return responseBuilder.build();
  }

  private Certificate getPublicCertificate(GatewayServices gatewayServices, GatewayConfig config) {
    try {
      final KeystoreService keystoreService = gatewayServices.getService(ServiceType.KEYSTORE_SERVICE);
      return keystoreService.getKeystoreForGateway().getCertificate(config.getIdentityKeyAlias());
    } catch (KeyStoreException | KeystoreServiceException e) {
      LOG.failedToFetchPublicCert(e.getMessage(), e);
      return null;
    }
  }

  private void generateCertificatePem(Certificate certificate, GatewayConfig gatewayConfig) {
    try {
      if (pemFilePath == null || !pemFilePath.toFile().exists()) {
        pemFilePath = Paths.get(gatewayConfig.getGatewaySecurityDir(), "gateway-client-trust.pem");
        X509CertificateUtil.writeCertificateToFile(certificate, pemFilePath.toFile());
      }
    } catch (CertificateEncodingException | IOException e) {
      LOG.failedToGeneratePublicCert("PEM", e.getMessage(), e);
    }
  }

  private void generateCertificateJks(Certificate certificate, GatewayConfig gatewayConfig) {
    try {
      if (jksFilePath == null || !jksFilePath.toFile().exists()) {
        jksFilePath = Paths.get(gatewayConfig.getGatewaySecurityDir(), "gateway-client-trust.jks");
        X509CertificateUtil.writeCertificateToJks(certificate, jksFilePath.toFile());
      }
    } catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException e) {
      LOG.failedToGeneratePublicCert("JKS", e.getMessage(), e);
    }
  }

  private String getBaseGatewayUrl(GatewayConfig config) {
    return request.getRequestURL().substring(0, request.getRequestURL().length() - request.getRequestURI().length()) + "/" + config.getGatewayPath();
  }

  @GET
  @Produces({ APPLICATION_XML, APPLICATION_JSON })
  @Path("topologies")
  public TopologyInformationWrapper getTopologies() {
    return getTopologies(null);
  }

  @GET
  @Produces({ APPLICATION_XML, APPLICATION_JSON })
  @Path("topologies/{name}")
  public TopologyInformationWrapper getTopology(@PathParam("name") String topologyName) {
    return getTopologies(topologyName);
  }

  private TopologyInformationWrapper getTopologies(String topologyName) {
    final TopologyInformationWrapper topologies = new TopologyInformationWrapper();
    final GatewayServices gatewayServices = (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
    final GatewayConfig config = (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
    final ServiceDefinitionRegistry serviceDefinitionRegistry = gatewayServices.getService(ServiceType.SERVICE_DEFINITION_REGISTRY);
    final Set<String> hiddenTopologies = config.getHiddenTopologiesOnHomepage();
    if (gatewayServices != null) {
      final TopologyService topologyService = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
      for (Topology topology : topologyService.getTopologies()) {
        if (!hiddenTopologies.contains(topology.getName()) && (topologyName == null || topology.getName().equalsIgnoreCase(topologyName))) {
          Set<ServiceModel> apiServices = new HashSet<>();
          Set<ServiceModel> uiServices = new HashSet<>();
          topology.getServices().stream().filter(service -> !UNREAL_SERVICES.contains(service.getRole())).forEach(service -> {
            service.getUrls().forEach(serviceUrl -> {
              ServiceModel serviceModel = getServiceModel(request, config.getGatewayPath(), topology.getName(), service, getServiceMetadata(serviceDefinitionRegistry, service),
                  serviceUrl);
              if (ServiceModel.Type.UI == serviceModel.getType()) {
                uiServices.add(serviceModel);
              } else if (ServiceModel.Type.API_AND_UI == serviceModel.getType()) {
                uiServices.add(serviceModel);
                apiServices.add(serviceModel);
              } else {
                apiServices.add(serviceModel);
              }
            });
          });
          topologies.addTopology(topology.getName(), isPinnedTopology(topology.getName(), config), new TreeSet<>(apiServices), new TreeSet<>(uiServices));
        }
      }
    }
    return topologies;
  }

  boolean isPinnedTopology(String topologyName, GatewayConfig config) {
    if (pinnedTopologies == null) {
      pinnedTopologies = config.getPinnedTopologiesOnHomepage();
    }
    return pinnedTopologies.contains(topologyName);
  }

  private Metadata getServiceMetadata(ServiceDefinitionRegistry serviceDefinitionRegistry, Service service) {
    final Optional<ServiceDefinitionPair> serviceDefinition = serviceDefinitionRegistry.getServiceDefinitions().stream()
        .filter(serviceDefinitionPair -> serviceDefinitionPair.getService().getRole().equalsIgnoreCase(service.getRole()))
        .filter(serviceDefinitionPair -> service.getVersion() == null || service.getVersion().toString().equalsIgnoreCase(serviceDefinitionPair.getService().getVersion()))
        .findFirst();
    return serviceDefinition.isPresent() ? serviceDefinition.get().getService().getMetadata() : null;
  }

  private ServiceModel getServiceModel(HttpServletRequest request, String gatewayPath, String topologyName, Service service, Metadata serviceMetadata, String serviceUrl) {
    final ServiceModel serviceModel = new ServiceModel();
    serviceModel.setRequest(request);
    serviceModel.setGatewayPath(gatewayPath);
    serviceModel.setTopologyName(topologyName);
    serviceModel.setService(service);
    serviceModel.setServiceMetadata(serviceMetadata);
    serviceModel.setServiceUrl(serviceUrl);
    return serviceModel;
  }

}
