// Copyright 2023 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.remote;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.remote.RemoteScrubbing.Config;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.protobuf.TextFormat;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.Nullable;

/**
 * The {@link Scrubber} implements scrubbing of remote cache keys.
 *
 * <p>See the documentation for the {@code --experimental_remote_scrub_config} flag for more
 * information.
 */
public class Scrubber {

  /** An error that occurred while parsing the scrubbing configuration. */
  public static class ConfigParseException extends Exception {

    private ConfigParseException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  private final ImmutableList<SpawnScrubber> spawnScrubbers;

  @VisibleForTesting
  Scrubber(Config configProto) {
    ArrayList<SpawnScrubber> spawnScrubbers = new ArrayList<>();
    for (Config.Rule ruleProto : configProto.getRulesList()) {
      spawnScrubbers.add(new SpawnScrubber(ruleProto));
    }
    // Reverse the order so that later rules supersede earlier ones.
    Collections.reverse(spawnScrubbers);
    this.spawnScrubbers = ImmutableList.copyOf(spawnScrubbers);
  }

  /**
   * Constructs a {@link Scrubber} from the given configuration file, which must contain a {@link
   * Config} protocol buffer in text format.
   */
  public static Scrubber parse(String configPath) throws ConfigParseException {
    try (BufferedReader reader = Files.newBufferedReader(Paths.get(configPath))) {
      var builder = Config.newBuilder();
      TextFormat.getParser().merge(reader, builder);
      return new Scrubber(builder.build());
    } catch (IOException e) {
      throw new ConfigParseException(e.getMessage(), e);
    } catch (PatternSyntaxException e) {
      throw new ConfigParseException(
          String.format("in regex '%s': %s", e.getPattern(), e.getMessage()), e);
    }
  }

  /**
   * Returns a {@link SpawnScrubber} suitable for a {@link Spawn}, or {@code null} if the spawn does
   * not need to be scrubbed.
   */
  @Nullable
  public SpawnScrubber forSpawn(Spawn spawn) {
    for (SpawnScrubber spawnScrubber : spawnScrubbers) {
      if (spawnScrubber.matches(spawn)) {
        return spawnScrubber;
      }
    }
    return null;
  }

  /**
   * Encapsulates a set of transformations required to scrub the remote cache key for a set of
   * spawns.
   */
  public static class SpawnScrubber {

    private final Pattern mnemonicPattern;
    private final Pattern labelPattern;
    private final Pattern kindPattern;
    private final boolean matchTools;

    private final ImmutableList<Pattern> omittedInputPatterns;
    private final ImmutableMap<Pattern, String> argReplacements;
    private final String salt;

    private SpawnScrubber(Config.Rule ruleProto) {
      Config.Matcher matcherProto = ruleProto.getMatcher();
      this.mnemonicPattern = Pattern.compile(emptyToAll(matcherProto.getMnemonic()));
      this.labelPattern = Pattern.compile(emptyToAll(matcherProto.getLabel()));
      this.kindPattern = Pattern.compile(emptyToAll(matcherProto.getKind()));
      this.matchTools = matcherProto.getMatchTools();

      Config.Transform transformProto = ruleProto.getTransform();
      this.omittedInputPatterns =
          transformProto.getOmittedInputsList().stream()
              .map(Pattern::compile)
              .collect(toImmutableList());
      this.argReplacements =
          transformProto.getArgReplacementsList().stream()
              .collect(toImmutableMap(r -> Pattern.compile(r.getSource()), r -> r.getTarget()));
      this.salt = ruleProto.getTransform().getSalt();
    }

    private String emptyToAll(String s) {
      return s.isEmpty() ? ".*" : s;
    }

    /** Whether this scrubber applies to the given {@link Spawn}. */
    private boolean matches(Spawn spawn) {
      String mnemonic = spawn.getMnemonic();
      ActionOwner actionOwner = spawn.getResourceOwner().getOwner();
      String label = actionOwner.getLabel().getCanonicalForm();
      String kind = actionOwner.getTargetKind();
      boolean isForTool = actionOwner.isBuildConfigurationForTool();

      return (!isForTool || matchTools)
          && mnemonicPattern.matcher(mnemonic).matches()
          && labelPattern.matcher(label).matches()
          && kindPattern.matcher(kind).matches();
    }

    /** Whether an input with the given exec-relative path should be omitted from the cache key. */
    public boolean shouldOmitInput(PathFragment execPath) {
      for (Pattern pattern : omittedInputPatterns) {
        if (pattern.matcher(execPath.getPathString()).matches()) {
          return true;
        }
      }
      return false;
    }

    /** Transforms a command line argument. */
    public String transformArgument(String arg) {
      for (Map.Entry<Pattern, String> entry : argReplacements.entrySet()) {
        Pattern pattern = entry.getKey();
        String replacement = entry.getValue();
        // Don't use Pattern#replaceFirst because it allows references to capture groups.
        Matcher m = pattern.matcher(arg);
        if (m.find()) {
          arg = arg.substring(0, m.start()) + replacement + arg.substring(m.end());
        }
      }
      return arg;
    }

    /** Returns the scrubbing salt. */
    public String getSalt() {
      return salt;
    }
  }
}
