// Copyright 2022 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.bazel.bzlmod;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.skyframe.PrecomputedValue;
import java.util.List;
import java.util.Optional;

/** Utility class to parse and evaluate yanked version specifications and exceptions. */
public final class YankedVersionsUtil {

  public static final PrecomputedValue.Precomputed<List<String>> ALLOWED_YANKED_VERSIONS =
      new PrecomputedValue.Precomputed<>("allowed_yanked_versions");
  public static final String BZLMOD_ALLOWED_YANKED_VERSIONS_ENV = "BZLMOD_ALLOW_YANKED_VERSIONS";

  /**
   * Parse a set of allowed yanked version from command line flag (--allowed_yanked_versions) and
   * environment variable (ALLOWED_YANKED_VERSIONS). If `all` is specified, return Optional.empty();
   * otherwise returns the set of parsed modulel key.
   */
  static Optional<ImmutableSet<ModuleKey>> parseAllowedYankedVersions(
      String allowedYankedVersionsFromEnv, List<String> allowedYankedVersionsFromFlag)
      throws ExternalDepsException {
    ImmutableSet.Builder<ModuleKey> allowedYankedVersionBuilder = new ImmutableSet.Builder<>();
    if (allowedYankedVersionsFromEnv != null) {
      if (parseModuleKeysFromString(
          allowedYankedVersionsFromEnv,
          allowedYankedVersionBuilder,
          String.format(
              "environment variable %s=%s",
              BZLMOD_ALLOWED_YANKED_VERSIONS_ENV, allowedYankedVersionsFromEnv))) {
        return Optional.empty();
      }
    }
    for (String allowedYankedVersions : allowedYankedVersionsFromFlag) {
      if (parseModuleKeysFromString(
          allowedYankedVersions,
          allowedYankedVersionBuilder,
          String.format("command line flag --allow_yanked_versions=%s", allowedYankedVersions))) {
        return Optional.empty();
      }
    }
    return Optional.of(allowedYankedVersionBuilder.build());
  }

  /**
   * Parse of a comma-separated list of module version(s) of the form '<module name>@<version>' or
   * 'all' from the string. Returns true if 'all' is present, otherwise returns false.
   */
  private static boolean parseModuleKeysFromString(
      String input, ImmutableSet.Builder<ModuleKey> allowedYankedVersionBuilder, String context)
      throws ExternalDepsException {
    ImmutableList<String> moduleStrs = ImmutableList.copyOf(Splitter.on(',').split(input));

    for (String moduleStr : moduleStrs) {
      if (moduleStr.equals("all")) {
        return true;
      }

      if (moduleStr.isEmpty()) {
        continue;
      }

      String[] pieces = moduleStr.split("@", 2);

      if (pieces.length != 2) {
        throw ExternalDepsException.withMessage(
            FailureDetails.ExternalDeps.Code.VERSION_RESOLUTION_ERROR,
            "Parsing %s failed, module versions must be of the form '<module name>@<version>'",
            context);
      }

      if (!RepositoryName.VALID_MODULE_NAME.matcher(pieces[0]).matches()) {
        throw ExternalDepsException.withMessage(
            FailureDetails.ExternalDeps.Code.VERSION_RESOLUTION_ERROR,
            "Parsing %s failed, invalid module name '%s': valid names must 1) only contain"
                + " lowercase letters (a-z), digits (0-9), dots (.), hyphens (-), and"
                + " underscores (_); 2) begin with a lowercase letter; 3) end with a lowercase"
                + " letter or digit.",
            context,
            pieces[0]);
      }

      Version version;
      try {
        version = Version.parse(pieces[1]);
      } catch (Version.ParseException e) {
        throw ExternalDepsException.withCauseAndMessage(
            FailureDetails.ExternalDeps.Code.VERSION_RESOLUTION_ERROR,
            e,
            "Parsing %s failed, invalid version specified for module: %s",
            context,
            pieces[1]);
      }

      allowedYankedVersionBuilder.add(new ModuleKey(pieces[0], version));
    }
    return false;
  }

  private YankedVersionsUtil() {}
}
