// Copyright 2019 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.runtime.commands;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import com.google.common.base.Verify;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.analysis.config.Fragment;
import com.google.devtools.build.lib.analysis.config.FragmentClassSet;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.analysis.config.FragmentRegistry;
import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.commands.ConfigCommand.ConfigOptions;
import com.google.devtools.build.lib.runtime.commands.ConfigCommandOutputFormatter.JsonOutputFormatter;
import com.google.devtools.build.lib.runtime.commands.ConfigCommandOutputFormatter.TextOutputFormatter;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.ConfigCommand.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
import com.google.devtools.build.lib.skyframe.config.BuildConfigurationKey;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator;
import com.google.devtools.common.options.EnumConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParsingResult;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;

/** Handles the 'config' command on the Blaze command line. */
@Command(
    name = "config",
    builds = true,
    inherits = {BuildCommand.class},
    options = {ConfigOptions.class},
    usesConfigurationOptions = true,
    shortDescription = "Displays details of configurations.",
    allowResidue = true,
    completion = "string",
    hidden = true,
    help = "resource:config.txt")
public class ConfigCommand implements BlazeCommand {
  /** Defines the types of output this command can produce. */
  public enum OutputType {
    TEXT,
    JSON
  }

  /** Options for the "config" command. */
  public static class ConfigOptions extends OptionsBase {
    @Option(
        name = "dump_all",
        defaultValue = "false",
        documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
        effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
        help = "If set, dump all known configurations instead of just the ids.")
    public boolean dumpAll;

    /** Converter for --output. */
    public static class OutputTypeConverter extends EnumConverter<OutputType> {
      public OutputTypeConverter() {
        super(OutputType.class, "output type");
      }
    }

    @Option(
        name = "output",
        converter = OutputTypeConverter.class,
        defaultValue = "text",
        documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
        effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
        help = "Formats the output of displayed results. Can be one of: 'text', 'json'. ")
    public OutputType outputType;
  }

  /**
   * Data structure defining a {@link BuildConfigurationValue} for the purpose of this command's
   * output.
   *
   * <p>Includes all data representing a "configuration" and defines their relative structure and
   * list order.
   *
   * <p>A {@link ConfigCommandOutputFormatter} uses this to lightly format output from a logically
   * consistent core structure.
   */
  protected static class ConfigurationForOutput {
    final String skyKey;
    final String configHash;
    final String mnemonic;
    final boolean isExec;
    final List<FragmentForOutput> fragments;
    final List<FragmentOptionsForOutput> fragmentOptions;

    ConfigurationForOutput(
        String skyKey,
        String configHash,
        String mnemonic,
        boolean isExec,
        List<FragmentForOutput> fragments,
        List<FragmentOptionsForOutput> fragmentOptions) {
      this.skyKey = skyKey;
      this.configHash = configHash;
      this.mnemonic = mnemonic;
      this.isExec = isExec;
      this.fragments = fragments;
      this.fragmentOptions = fragmentOptions;
    }

    @Nullable
    public FragmentOptionsForOutput fragment(String fragmentName) {
      return this.fragmentOptions.stream()
          .filter(fo -> fo.name.equals(fragmentName))
          .findFirst()
          .orElse(null);
    }

    public Set<String> fragmentOptionNames() {
      return this.fragmentOptions.stream().map(fragment -> fragment.name).collect(toImmutableSet());
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof ConfigurationForOutput) {
        ConfigurationForOutput other = (ConfigurationForOutput) o;
        return other.skyKey.equals(skyKey)
            && other.configHash.equals(configHash)
            && other.fragments.equals(fragments)
            && other.fragmentOptions.equals(fragmentOptions);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(skyKey, configHash, fragments, fragmentOptions);
    }

    String checksum() {
      return configHash;
    }
  }

  /**
   * Data structure defining a {@link Fragment} for the purpose of this command's output.
   *
   * <p>{@link Fragment} is a Java object representation of a domain-specific "piece" of
   * configuration (like "C++-related configuration"). It depends on one or more {@link
   * FragmentOptions}, which are the <code>--flag=value</code> pairs that key configurations.
   *
   * <p>See {@link FragmentOptionsForOutput} and {@link ConfigurationForOutput} for further details.
   */
  protected static class FragmentForOutput {
    final String name;
    // We store the name of the associated FragmentOptions instead of FragmentOptionsForOutput
    // objects because multiple fragments may use the same FragmentOptions and we don't want to list
    // it multiple times.
    final List<String> fragmentOptions;

    FragmentForOutput(String name, List<String> fragmentOptions) {
      this.name = name;
      this.fragmentOptions = fragmentOptions;
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof FragmentForOutput) {
        FragmentForOutput other = (FragmentForOutput) o;
        return other.name.equals(name) && other.fragmentOptions.equals(fragmentOptions);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(name, fragmentOptions);
    }
  }

  /**
   * Data structure defining a {@link FragmentOptions} from the point of this command's output.
   *
   * <p>See {@link FragmentForOutput} and {@link ConfigurationForOutput} for further details.
   */
  protected static class FragmentOptionsForOutput {
    final String name;
    final Map<String, String> options;

    FragmentOptionsForOutput(String name, Map<String, String> options) {
      this.name = name;
      this.options = options;
    }

    public Set<String> optionNames() {
      return this.options.keySet();
    }

    public String getOption(String optionName) {
      return this.options.get(optionName);
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof FragmentOptionsForOutput) {
        FragmentOptionsForOutput other = (FragmentOptionsForOutput) o;
        return other.name.equals(name) && other.options.equals(options);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(name, options);
    }
  }

  /**
   * Data structure defining the difference between two {@link BuildConfigurationValue}s from the
   * point of this command's output.
   *
   * <p>See {@link ConfigurationForOutput} for further details.
   */
  protected static class ConfigurationDiffForOutput {
    final String configHash1;
    final String configHash2;
    final List<FragmentDiffForOutput> fragmentsDiff;

    ConfigurationDiffForOutput(
        String configHash1, String configHash2, List<FragmentDiffForOutput> fragmentsDiff) {
      this.configHash1 = configHash1;
      this.configHash2 = configHash2;
      this.fragmentsDiff = fragmentsDiff;
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof ConfigurationDiffForOutput) {
        ConfigurationDiffForOutput other = (ConfigurationDiffForOutput) o;
        return other.configHash1.equals(configHash1)
            && other.configHash2.equals(configHash2)
            && other.fragmentsDiff.equals(fragmentsDiff);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(configHash1, configHash2, fragmentsDiff);
    }
  }

  /**
   * Data structure defining the difference between two {@link BuildConfigurationValue}s for a given
   * {@link FragmentOptions}from the point of this command's output.
   *
   * <p>See {@link ConfigurationForOutput} for further details.
   */
  protected static class FragmentDiffForOutput {
    final String name;
    final Map<String, Pair<String, String>> optionsDiff;

    FragmentDiffForOutput(String name, Map<String, Pair<String, String>> optionsDiff) {
      this.name = name;
      this.optionsDiff = optionsDiff;
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof FragmentDiffForOutput) {
        FragmentDiffForOutput other = (FragmentDiffForOutput) o;
        return other.name.equals(name) && other.optionsDiff.equals(optionsDiff);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(name, optionsDiff);
    }
  }

  /**
   * Main entry point into the <code>blaze config</code> command.
   *
   * <p>Its purpose is to parse all options, figure out what variation of the command that implies,
   * run the right logic, and return the right exit code.
   */
  @Override
  public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
    ImmutableSortedMap<BuildConfigurationKey, BuildConfigurationValue> configurations =
        findConfigurations(env);
    if (configurations.isEmpty()) {
      String message =
          "No configurations found. This can happen if the 'config' subcommand is used after "
              + "files, including their metadata, have changed since the last invocation of "
              + "another subcommand. Try running a 'build' or 'cquery' directly followed by "
              + "'config'.";
      env.getReporter().handle(Event.error(message));
      return createFailureResult(message, Code.CONFIGURATION_NOT_FOUND);
    }

    try (PrintWriter writer =
        new PrintWriter(
            new OutputStreamWriter(env.getReporter().getOutErr().getOutputStream(), UTF_8))) {

      ConfigOptions configCommandOptions = options.getOptions(ConfigOptions.class);
      ConfigCommandOutputFormatter outputFormatter =
          configCommandOptions.outputType == OutputType.TEXT
              ? new TextOutputFormatter(writer)
              : new JsonOutputFormatter(writer);
      ImmutableSortedMap<
              Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
          fragmentDefs =
              getFragmentDefs(env.getRuntime().getRuleClassProvider().getFragmentRegistry());

      if (options.getResidue().isEmpty()) {
        if (configCommandOptions.dumpAll) {
          return reportAllConfigurations(outputFormatter, forOutput(configurations, fragmentDefs));
        } else {
          return reportConfigurationIds(outputFormatter, forOutput(configurations, fragmentDefs));
        }
      } else if (options.getResidue().size() == 1) {
        String configHash = options.getResidue().get(0);
        return reportSingleConfiguration(
            outputFormatter, env, forOutput(configurations, fragmentDefs), configHash);
      } else if (options.getResidue().size() == 2) {
        String configHash1 = options.getResidue().get(0);
        String configHash2 = options.getResidue().get(1);
        return reportConfigurationDiff(
            forOutput(configurations, fragmentDefs),
            configHash1,
            configHash2,
            outputFormatter,
            env);
      } else {
        String message = "Too many config ids.";
        env.getReporter().handle(Event.error(message));
        return createFailureResult(message, Code.TOO_MANY_CONFIG_IDS);
      }
    }
  }

  /**
   * Returns all {@link BuildConfigurationValue}s in Skyframe as a map from their {@link
   * BuildConfigurationKey} to instance.
   */
  private static ImmutableSortedMap<BuildConfigurationKey, BuildConfigurationValue>
      findConfigurations(CommandEnvironment env) {
    InMemoryMemoizingEvaluator evaluator =
        (InMemoryMemoizingEvaluator)
            env.getRuntime().getWorkspace().getSkyframeExecutor().getEvaluator();
    return evaluator.getDoneValues().entrySet().stream()
        .filter(e -> SkyFunctions.BUILD_CONFIGURATION.equals(e.getKey().functionName()))
        .collect(
            toImmutableSortedMap(
                comparing(e -> e.getOptions().checksum()),
                e -> (BuildConfigurationKey) e.getKey(),
                e -> (BuildConfigurationValue) e.getValue()));
  }

  /**
   * Returns the {@link Fragment}s and the {@link FragmentOptions} they require from Blaze's
   * runtime.
   *
   * <p>These are the fragments that Blaze "knows about", not necessarily the fragments in a {@link
   * BuildConfigurationValue}. Trimming, in particular, strips fragments out of actual
   * configurations. It's safe to assume untrimmed configuration have all fragments listed here.
   */
  private static ImmutableSortedMap<
          Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
      getFragmentDefs(FragmentRegistry fragmentRegistry) {
    return fragmentRegistry.getAllFragments().stream()
        .collect(
            toImmutableSortedMap(
                FragmentClassSet.LEXICAL_FRAGMENT_SORTER,
                fragment -> fragment,
                fragment ->
                    ImmutableSortedSet.copyOf(
                        Comparator.comparing(Class::getName), Fragment.requiredOptions(fragment))));
  }

  /**
   * Converts {@link #findConfigurations}'s output into a list of {@link ConfigurationForOutput}
   * instances.
   */
  private static ImmutableSortedSet<ConfigurationForOutput> forOutput(
      ImmutableSortedMap<BuildConfigurationKey, BuildConfigurationValue> asSkyKeyMap,
      ImmutableSortedMap<
              Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
          fragmentDefs) {
    ImmutableSortedSet.Builder<ConfigurationForOutput> ans =
        ImmutableSortedSet.orderedBy(comparing(e -> e.configHash));
    for (Map.Entry<BuildConfigurationKey, BuildConfigurationValue> entry : asSkyKeyMap.entrySet()) {
      BuildConfigurationKey key = entry.getKey();
      BuildConfigurationValue config = entry.getValue();
      ans.add(getConfigurationForOutput(key, config.checksum(), config, fragmentDefs));
    }
    return ans.build();
  }

  /** Constructs a {@link ConfigurationForOutput} from the given input daata. */
  private static ConfigurationForOutput getConfigurationForOutput(
      BuildConfigurationKey skyKey,
      String configHash,
      BuildConfigurationValue config,
      ImmutableSortedMap<
              Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
          fragmentDefs) {

    ImmutableSortedSet.Builder<FragmentForOutput> fragments =
        ImmutableSortedSet.orderedBy(comparing(e -> e.name));
    for (Map.Entry<Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
        entry : fragmentDefs.entrySet()) {
      fragments.add(
          new FragmentForOutput(
              entry.getKey().getName(),
              entry.getValue().stream().map(Class::getName).collect(toList())));
    }
    fragmentDefs.entrySet().stream()
        .filter(entry -> config.hasFragment(entry.getKey()))
        .forEach(
            entry ->
                fragments.add(
                    new FragmentForOutput(
                        entry.getKey().getName(),
                        entry.getValue().stream().map(Class::getName).collect(toList()))));

    ImmutableSortedSet.Builder<FragmentOptionsForOutput> fragmentOptions =
        ImmutableSortedSet.orderedBy(comparing(e -> e.name));
    config.getOptions().getFragmentClasses().stream()
        .map(optionsClass -> config.getOptions().get(optionsClass))
        .forEach(
            fragmentOptionsInstance ->
                fragmentOptions.add(
                    new FragmentOptionsForOutput(
                        fragmentOptionsInstance.getClass().getName(),
                        getOrderedNativeOptions(fragmentOptionsInstance))));
    fragmentOptions.add(
        new FragmentOptionsForOutput(
            UserDefinedFragment.DESCRIPTIVE_NAME, getOrderedUserDefinedOptions(config)));

    return new ConfigurationForOutput(
        skyKey.toString(),
        configHash,
        config.getMnemonic(),
        config.isExecConfiguration(),
        fragments.build().asList(),
        fragmentOptions.build().asList());
  }

  /**
   * Returns the configuration matching a hash prefix.
   *
   * @param configurations collection of configurations to search
   * @param configPrefix prefix or exact value of the matching configuration's hash
   * @throws InvalidConfigurationException if not exactly one configuration matches
   */
  private static ConfigurationForOutput getConfiguration(
      Collection<ConfigurationForOutput> configurations, String configPrefix)
      throws InvalidConfigurationException {
    ImmutableList<ConfigurationForOutput> matches =
        configurations.stream()
            .filter(config -> doesConfigMatch(config, configPrefix))
            .collect(toImmutableList());
    if (matches.isEmpty()) {
      throw new InvalidConfigurationException(
          String.format("No configuration found with ID prefix %s", configPrefix));
    } else if (matches.size() > 1) {
      throw new InvalidConfigurationException(
          String.format(
              "Configuration identifier '%s' is ambiguous.\n"
                  + "'%s' is a prefix of multiple configurations:\n "
                  + matches.stream().map(ConfigurationForOutput::checksum).collect(joining("\n "))
                  + "\n\n"
                  + "Use a sufficient prefix to uniquely identify one configuration.",
              configPrefix,
              configPrefix));
    }
    return Iterables.getOnlyElement(matches);
  }

  private static boolean doesConfigMatch(ConfigurationForOutput config, String configPrefix) {
    return config.checksum().startsWith(configPrefix);
  }

  /**
   * Returns a {@link FragmentOptions}'s native option settings in canonical order.
   *
   * <p>While actual option values are objects, we serialize them to strings to prevent command
   * output from interpreting them more deeply than we want for simple "name=value" output.
   */
  private static ImmutableSortedMap<String, String> getOrderedNativeOptions(
      FragmentOptions options) {
    return options.asMap().entrySet().stream()
        // While technically part of CoreOptions, --define is practically a user-definable flag so
        // we include it in the user-defined fragment for clarity. See getOrderedUserDefinedOptions.
        .filter(
            entry ->
                !(options.getClass().equals(CoreOptions.class) && entry.getKey().equals("define")))
        .collect(
            toImmutableSortedMap(
                Ordering.natural(), Map.Entry::getKey, e -> String.valueOf(e.getValue())));
  }

  /**
   * Returns a configuration's user-definable settings in canonical order.
   *
   * <p>While actual option values are objects, we serialize them to strings to prevent command
   * output from interpreting them more deeply than we want for simple "name=value" output.
   */
  private static ImmutableSortedMap<String, String> getOrderedUserDefinedOptions(
      BuildConfigurationValue config) {
    ImmutableSortedMap.Builder<String, String> ans = ImmutableSortedMap.naturalOrder();

    // Starlark-defined options:
    for (Map.Entry<Label, Object> entry : config.getOptions().getStarlarkOptions().entrySet()) {
      ans.put(entry.getKey().toString(), String.valueOf(entry.getValue()));
    }

    // --define:
    for (Map.Entry<String, String> entry :
        config
            .getOptions()
            .get(CoreOptions.class)
            .getNormalizedCommandLineBuildVariables()
            .entrySet()) {
      ans.put("--define:" + entry.getKey(), Verify.verifyNotNull(entry.getValue()));
    }
    return ans.buildOrThrow();
  }

  /**
   * Reports the result of <code>blaze config --dump_all</code> and returns the appropriate command
   * exit code.
   */
  private static BlazeCommandResult reportAllConfigurations(
      ConfigCommandOutputFormatter writer,
      ImmutableSortedSet<ConfigurationForOutput> configurations) {
    writer.writeConfigurations(configurations);
    return BlazeCommandResult.success();
  }

  /**
   * Reports the result of <code>blaze config</code> and returns the appropriate command exit code.
   */
  private static BlazeCommandResult reportConfigurationIds(
      ConfigCommandOutputFormatter writer,
      ImmutableSortedSet<ConfigurationForOutput> configurations) {
    writer.writeConfigurationIDs(configurations);
    return BlazeCommandResult.success();
  }

  /**
   * Reports the result of <code>blaze config <configHash></code> and returns the appropriate
   * command exit code.
   */
  private static BlazeCommandResult reportSingleConfiguration(
      ConfigCommandOutputFormatter writer,
      CommandEnvironment env,
      ImmutableSortedSet<ConfigurationForOutput> allConfigurations,
      String configHash) {
    env.getReporter().handle(Event.info(String.format("Displaying config with id %s", configHash)));
    try {
      writer.writeConfiguration(getConfiguration(allConfigurations, configHash));
      return BlazeCommandResult.success();
    } catch (InvalidConfigurationException e) {
      env.getReporter().handle(Event.error(e.getMessage()));
      return createFailureResult(e.getMessage(), Code.CONFIGURATION_NOT_FOUND);
    }
  }

  /**
   * Reports the result of <code>blaze config <configHash1> <configHash2></code> and returns the
   * appropriate command exit code.
   */
  private static BlazeCommandResult reportConfigurationDiff(
      ImmutableSortedSet<ConfigurationForOutput> allConfigs,
      String configHash1,
      String configHash2,
      ConfigCommandOutputFormatter writer,
      CommandEnvironment env) {
    env.getReporter()
        .handle(
            Event.info(
                String.format(
                    "Displaying diff between configs" + " %s and" + " %s",
                    configHash1, configHash2)));
    try {
      ConfigurationForOutput config1 = getConfiguration(allConfigs, configHash1);
      ConfigurationForOutput config2 = getConfiguration(allConfigs, configHash2);
      Table<String, String, Pair<Object, Object>> diffs = diffConfigurations(config1, config2);
      writer.writeConfigurationDiff(getConfigurationDiffForOutput(configHash1, configHash2, diffs));
      return BlazeCommandResult.success();
    } catch (InvalidConfigurationException e) {
      env.getReporter().handle(Event.error(e.getMessage()));
      return createFailureResult(e.getMessage(), Code.CONFIGURATION_NOT_FOUND);
    }
  }

  /**
   * Starlark options don't have configuration fragments. This is just to keep their output
   * consistent with native options, i.e. to include "user-defined" section in the output list.
   */
  private static class UserDefinedFragment extends FragmentOptions {
    static final String DESCRIPTIVE_NAME = "user-defined";
    // Intentionally empty: we read the actual options directly from BuildOptions.
  }

  private static Table<String, String, Pair<Object, Object>> diffConfigurations(
      ConfigurationForOutput config1, ConfigurationForOutput config2) {
    Table<String, String, Pair<Object, Object>> diffs = HashBasedTable.create();

    for (String fragmentName :
        Sets.union(config1.fragmentOptionNames(), config2.fragmentOptionNames())) {
      FragmentOptionsForOutput options1 = config1.fragment(fragmentName);
      FragmentOptionsForOutput options2 = config2.fragment(fragmentName);
      diffs.row(fragmentName).putAll(diffOptions(options1, options2));
    }
    return diffs;
  }

  private static Map<String, Pair<Object, Object>> diffOptions(
      @Nullable FragmentOptionsForOutput options1, @Nullable FragmentOptionsForOutput options2) {
    Set<String> optionNames1 = options1 == null ? ImmutableSet.of() : options1.optionNames();
    Set<String> optionNames2 = options2 == null ? ImmutableSet.of() : options2.optionNames();
    Map<String, Pair<Object, Object>> diffs = new HashMap<>();

    for (String optionName : Sets.union(optionNames1, optionNames2)) {
      String value1 = options1 == null ? null : options1.getOption(optionName);
      String value2 = options2 == null ? null : options2.getOption(optionName);

      if (!Objects.equals(value1, value2)) {
        diffs.put(optionName, Pair.of(value1, value2));
      }
    }

    return diffs;
  }

  private static ConfigurationDiffForOutput getConfigurationDiffForOutput(
      String configHash1, String configHash2, Table<String, String, Pair<Object, Object>> diffs) {
    ImmutableSortedSet.Builder<FragmentDiffForOutput> fragmentDiffs =
        ImmutableSortedSet.orderedBy(comparing(e -> e.name));
    diffs
        .rowKeySet()
        .forEach(
            fragmentName -> {
              ImmutableSortedMap<String, Pair<String, String>> sortedOptionDiffs =
                  diffs.row(fragmentName).entrySet().stream()
                      .collect(
                          toImmutableSortedMap(
                              Ordering.natural(),
                              Map.Entry::getKey,
                              e -> toNullableStringPair(e.getValue())));
              fragmentDiffs.add(new FragmentDiffForOutput(fragmentName, sortedOptionDiffs));
            });
    return new ConfigurationDiffForOutput(configHash1, configHash2, fragmentDiffs.build().asList());
  }

  private static Pair<String, String> toNullableStringPair(Pair<Object, Object> pair) {
    return Pair.of(String.valueOf(pair.first), String.valueOf(pair.second));
  }

  private static BlazeCommandResult createFailureResult(String message, Code detailedCode) {
    return BlazeCommandResult.failureDetail(
        FailureDetail.newBuilder()
            .setMessage(message)
            .setConfigCommand(FailureDetails.ConfigCommand.newBuilder().setCode(detailedCode))
            .build());
  }
}
