// Copyright (C) 2021 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;

/**
 * Represents a configuration on request level that matches requests by request type, URI pattern,
 * caller and/or project pattern.
 */
@AutoValue
public abstract class RequestConfig {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  public static ImmutableList<RequestConfig> parseConfigs(Config cfg, String section) {
    ImmutableList.Builder<RequestConfig> requestConfigs = ImmutableList.builder();

    for (String id : cfg.getSubsections(section)) {
      try {
        RequestConfig.Builder requestConfig = RequestConfig.builder(cfg, section, id);
        requestConfig.requestTypes(parseRequestTypes(cfg, section, id));
        requestConfig.requestUriPatterns(parseRequestUriPatterns(cfg, section, id));
        requestConfig.excludedRequestUriPatterns(parseExcludedRequestUriPatterns(cfg, section, id));
        requestConfig.accountIds(parseAccounts(cfg, section, id));
        requestConfig.projectPatterns(parseProjectPatterns(cfg, section, id));
        requestConfigs.add(requestConfig.build());
      } catch (ConfigInvalidException e) {
        logger.atWarning().log("Ignoring invalid %s configuration:\n %s", section, e.getMessage());
      }
    }

    return requestConfigs.build();
  }

  private static ImmutableSet<String> parseRequestTypes(Config cfg, String section, String id) {
    return ImmutableSet.copyOf(cfg.getStringList(section, id, "requestType"));
  }

  private static ImmutableSet<Pattern> parseRequestUriPatterns(
      Config cfg, String section, String id) throws ConfigInvalidException {
    return parsePatterns(cfg, section, id, "requestUriPattern");
  }

  private static ImmutableSet<Pattern> parseExcludedRequestUriPatterns(
      Config cfg, String section, String id) throws ConfigInvalidException {
    return parsePatterns(cfg, section, id, "excludedRequestUriPattern");
  }

  private static ImmutableSet<Account.Id> parseAccounts(Config cfg, String section, String id)
      throws ConfigInvalidException {
    ImmutableSet.Builder<Account.Id> accountIds = ImmutableSet.builder();
    String[] accounts = cfg.getStringList(section, id, "account");
    for (String account : accounts) {
      Optional<Account.Id> accountId = Account.Id.tryParse(account);
      if (!accountId.isPresent()) {
        throw new ConfigInvalidException(
            String.format(
                "Invalid request config ('%s.%s.account = %s'): invalid account ID",
                section, id, account));
      }
      accountIds.add(accountId.get());
    }
    return accountIds.build();
  }

  private static ImmutableSet<Pattern> parseProjectPatterns(Config cfg, String section, String id)
      throws ConfigInvalidException {
    return parsePatterns(cfg, section, id, "projectPattern");
  }

  private static ImmutableSet<Pattern> parsePatterns(
      Config cfg, String section, String id, String name) throws ConfigInvalidException {
    ImmutableSet.Builder<Pattern> patterns = ImmutableSet.builder();
    String[] patternRegExs = cfg.getStringList(section, id, name);
    for (String patternRegEx : patternRegExs) {
      try {
        patterns.add(Pattern.compile(patternRegEx));
      } catch (PatternSyntaxException e) {
        throw new ConfigInvalidException(
            String.format(
                "Invalid request config ('%s.%s.%s = %s'): %s",
                section, id, name, patternRegEx, e.getMessage()));
      }
    }
    return patterns.build();
  }

  /** the config from which this request config was read */
  abstract Config cfg();

  /** the section from which this request config was read */
  abstract String section();

  /** ID of the config, also the subsection from which this request config was read */
  abstract String id();

  /** request types that should be matched */
  abstract ImmutableSet<String> requestTypes();

  /** pattern matching request URIs */
  abstract ImmutableSet<Pattern> requestUriPatterns();

  /** pattern matching request URIs to be excluded */
  abstract ImmutableSet<Pattern> excludedRequestUriPatterns();

  /** accounts IDs matching calling user */
  abstract ImmutableSet<Account.Id> accountIds();

  /** pattern matching projects names */
  abstract ImmutableSet<Pattern> projectPatterns();

  private static Builder builder(Config cfg, String section, String id) {
    return new AutoValue_RequestConfig.Builder().cfg(cfg).section(section).id(id);
  }

  /**
   * Whether this request config matches a given request.
   *
   * @param requestInfo request info
   * @return whether this request config matches
   */
  boolean matches(RequestInfo requestInfo) {
    // If in the request config request types are set and none of them matches, then the request is
    // not matched.
    if (!requestTypes().isEmpty()
        && requestTypes().stream()
            .noneMatch(type -> type.equalsIgnoreCase(requestInfo.requestType()))) {
      return false;
    }

    // If in the request config request URI patterns are set and none of them matches, then the
    // request is not matched.
    if (!requestUriPatterns().isEmpty()) {
      if (!requestInfo.requestUri().isPresent()) {
        // The request has no request URI, hence it cannot match a request URI pattern.
        return false;
      }

      if (requestUriPatterns().stream()
          .noneMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
        return false;
      }
    }

    // If the request URI matches an excluded request URI pattern, then the request is not matched.
    if (requestInfo.requestUri().isPresent()
        && excludedRequestUriPatterns().stream()
            .anyMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
      return false;
    }

    // If in the request config accounts are set and none of them matches, then the request is not
    // matched.
    if (!accountIds().isEmpty()) {
      try {
        if (accountIds().stream()
            .noneMatch(id -> id.equals(requestInfo.callingUser().getAccountId()))) {
          return false;
        }
      } catch (UnsupportedOperationException e) {
        // The calling user is not logged in, hence it cannot match an account.
        return false;
      }
    }

    // If in the request config project patterns are set and none of them matches, then the request
    // is not matched.
    if (!projectPatterns().isEmpty()) {
      if (!requestInfo.project().isPresent()) {
        // The request is not for a project, hence it cannot match a project pattern.
        return false;
      }

      if (projectPatterns().stream()
          .noneMatch(p -> p.matcher(requestInfo.project().get().get()).matches())) {
        return false;
      }
    }

    // For any match criteria (request type, request URI pattern, account, project pattern) that
    // was specified in the request config, at least one of the configured value matched the
    // request.
    return true;
  }

  @AutoValue.Builder
  abstract static class Builder {
    abstract Builder cfg(Config cfg);

    abstract Builder section(String section);

    abstract Builder id(String id);

    abstract Builder requestTypes(ImmutableSet<String> requestTypes);

    abstract Builder requestUriPatterns(ImmutableSet<Pattern> requestUriPatterns);

    abstract Builder excludedRequestUriPatterns(ImmutableSet<Pattern> excludedRequestUriPatterns);

    abstract Builder accountIds(ImmutableSet<Account.Id> accountIds);

    abstract Builder projectPatterns(ImmutableSet<Pattern> projectPatterns);

    abstract RequestConfig build();
  }
}
