/*
 * 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.ambari.server.api.services;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.UriInfo;

import org.apache.ambari.server.api.predicate.QueryLexer;
import org.apache.ambari.server.api.resources.ResourceDefinition;
import org.apache.ambari.server.api.resources.ResourceInstance;

/**
 * Factory for {@link Request} instances.
 */
public class RequestFactory {
  /**
   * Create a request instance.
   *
   * @param headers      http headers
   * @param uriInfo      uri information
   * @param requestType  http request type
   * @param resource     associated resource instance
   *
   * @return a new Request instance
   */
  public Request createRequest(HttpHeaders headers, RequestBody body, UriInfo uriInfo, Request.Type requestType,
                               ResourceInstance resource) {
    switch (requestType) {
      case GET:
        return createGetRequest(headers, body, uriInfo, resource);
      case PUT:
        return createPutRequest(headers, body, uriInfo, resource);
      case DELETE:
        return createDeleteRequest(headers, body, uriInfo, resource);
      case POST:
        return createPostRequest(headers, body, uriInfo, resource);
      default:
        throw new IllegalArgumentException("Invalid request type: " + requestType);
    }
  }

  /**
   * Create a GET request.  This will apply any eligible directives supplied in the URI.
   *
   * @param headers  http headers
   * @param uriInfo  uri information
   * @param resource associated resource instance
   * @return new post request
   */
  private Request createGetRequest(HttpHeaders headers, RequestBody body, UriInfo uriInfo, ResourceInstance resource) {
    applyDirectives(Request.Type.GET, body, uriInfo, resource);
    return new GetRequest(headers, body, uriInfo, resource);
  }

  /**
   * Create a POST request.  This will either be a standard post request or a query post request.
   * A query post request first applies a query to a collection resource and then creates
   * sub-resources to all matches of the predicate.
   *
   * @param headers   http headers
   * @param uriInfo   uri information
   * @param resource  associated resource instance
   *
   * @return new post request
   */
  private Request createPostRequest(HttpHeaders headers, RequestBody body, UriInfo uriInfo, ResourceInstance resource) {
    boolean batchCreate = !applyDirectives(Request.Type.POST, body, uriInfo, resource);;

    return (batchCreate) ?
        new QueryPostRequest(headers, body, uriInfo, resource) :
        new PostRequest(headers, body, uriInfo, resource);
  }

  /**
   * Creates a PUT request. It will apply any eligible directives supplied in the URI
   */
  private Request createPutRequest(HttpHeaders headers, RequestBody body, UriInfo uriInfo, ResourceInstance resource) {
    applyDirectives(Request.Type.PUT, body, uriInfo, resource);
    return new PutRequest(headers, body, uriInfo, resource);
  }

  /**
   * Creates a DELETE request. It will apply any eligible directives supplied in the URI
   */
  private DeleteRequest createDeleteRequest(HttpHeaders headers, RequestBody body, UriInfo uriInfo, ResourceInstance resource) {
    applyDirectives(Request.Type.DELETE, body, uriInfo, resource);
    return new DeleteRequest(headers, body, uriInfo, resource);
  }

  /**
   * Gather query parameters from uri and body query string.
   *
   * @param uriInfo  contains uri info
   * @param body     request body
   *
   * @return map of query parameters or an empty map if no parameters are present
   */
  private Map<String, String> getQueryParameters(UriInfo uriInfo, RequestBody body) {
    Map<String, String> queryParameters = new HashMap<>();
    for (Map.Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
      queryParameters.put(entry.getKey(), entry.getValue().get(0));
    }

    String bodyQueryString = body.getQueryString();
    if (bodyQueryString != null && ! bodyQueryString.isEmpty()) {
      String[] toks = bodyQueryString.split("&");
      for (String tok : toks) {
        String[] keyVal = tok.split("=");
        queryParameters.put(keyVal[0], keyVal.length == 2 ? keyVal[1] : "");
      }
    }
    return queryParameters;
  }

  /**
   * Applies directives and determines if a query predicate exists or not.
   * <p/>
   * Depending on the request type (POST, PUT, etc...), retrieves the appropriate set of directives:
   * <ul>
   * <li><code>POST</code> - {@link org.apache.ambari.server.api.resources.ResourceDefinition#getCreateDirectives()}</li>
   * <li><code>PUT</code> - {@link org.apache.ambari.server.api.resources.ResourceDefinition#getUpdateDirectives()}</li>
   * </ul>
   * <p/>
   * Note: Only <code>POST</code> and <code>PUT</code> are supported.
   * <p/>
   * Iterates through the query parameters adding those that are known to be directives to the map
   * of request info properties from {@link RequestBody#getRequestInfoProperties()}.
   * <p/>
   * If a query property is found that is not a directive, a query predicate exists and
   * <code>false</code> is returned; else all query parameters are directives, leaving no query predicate,
   * and <code>true</code> is returned.
   *
   * @return true if all the directives are supported; false if at least one directive is not supported
   */
  private boolean applyDirectives(Request.Type requestType, RequestBody body, UriInfo uriInfo, ResourceInstance resource) {
    Map<String, String> queryParameters = getQueryParameters(uriInfo, body);
    queryParameters.remove(QueryLexer.QUERY_DOAS); // KNOX appends a doAs parameter to every request. Ignore this as it's neither a query predicate nor a directive.
    Map<String, String> requestInfoProperties;
    boolean allDirectivesApplicable = true;
    if (!queryParameters.isEmpty()) {
      ResourceDefinition resourceDefinition = resource.getResourceDefinition();
      Collection<String> directives;
      switch (requestType) {
        case PUT:
          directives = resourceDefinition.getUpdateDirectives();
          break;
        case POST:
          directives = resourceDefinition.getCreateDirectives();
          break;
        case GET:
          directives = resourceDefinition.getReadDirectives();
          break;
        case DELETE:
          directives = resourceDefinition.getDeleteDirectives();
          break;
        default:
          // not yet implemented for other types
          return false;
      }
      requestInfoProperties = body.getRequestInfoProperties();
      for (Map.Entry<String, String> entry : queryParameters.entrySet()) {
        if (directives.contains(entry.getKey())) {
          requestInfoProperties.put(entry.getKey(), entry.getValue());
        }
        else {
          allDirectivesApplicable = false;
        }
      }
    }
    return allDirectivesApplicable;
  }
}
