// Copyright 2024 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.actions;

import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;

import com.google.common.base.Functions;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.packages.AspectDescriptor;
import com.google.devtools.build.lib.server.FailureDetails.Analysis;
import com.google.devtools.build.lib.server.FailureDetails.Analysis.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.skyframe.AbstractSaneAnalysisException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/**
 * This exception is thrown when a conflict between actions is detected. It contains information
 * about the artifact for which the conflict is found, and data about the two conflicting actions
 * and their owners. Non-final only for {@link WithAspectKeyInfo}.
 */
public sealed class ActionConflictException extends AbstractSaneAnalysisException {
  private final Artifact artifact;
  private final ActionAnalysisMetadata attemptedAction;
  private final boolean isPrefixConflict;

  private static final int MAX_DIFF_ARTIFACTS_TO_REPORT = 5;

  public static ActionConflictException create(
      ActionKeyContext actionKeyContext,
      Artifact artifact,
      ActionAnalysisMetadata previousAction,
      ActionAnalysisMetadata attemptedAction) {
    return new ActionConflictException(
        artifact,
        attemptedAction,
        createDetailedMessage(artifact, actionKeyContext, attemptedAction, previousAction),
        /* isPrefixConflict= */ false);
  }

  public static ActionConflictException create(
      Artifact artifact,
      ActionAnalysisMetadata attemptedAction,
      String message,
      boolean isPrefixConflict) {
    return new ActionConflictException(artifact, attemptedAction, message, isPrefixConflict);
  }

  /**
   * Exception to indicate that one {@link Action} has an output artifact whose path is a prefix of
   * an output of another action. Since the first path cannot be both a directory and a file, this
   * would lead to an error if both actions were executed in the same build.
   */
  public static ActionConflictException createPrefix(
      Artifact firstArtifact,
      Artifact secondArtifact,
      ActionAnalysisMetadata firstAction,
      ActionAnalysisMetadata secondAction) {
    return new ActionConflictException(
        firstArtifact,
        firstAction,
        createPrefixDetailedMessage(
            firstArtifact,
            secondArtifact,
            firstAction.getOwner().getLabel(),
            secondAction.getOwner().getLabel()),
        /* isPrefixConflict= */ true);
  }

  public static ActionConflictException withAspectKeyInfo(
      ActionConflictException e, ActionLookupKey aspectKey) {
    return new WithAspectKeyInfo(e, aspectKey);
  }

  private ActionConflictException(
      Artifact artifact,
      ActionAnalysisMetadata attemptedAction,
      String message,
      boolean isPrefixConflict) {
    super(message);
    this.artifact = artifact;
    this.attemptedAction = attemptedAction;
    this.isPrefixConflict = isPrefixConflict;
  }

  public Artifact getArtifact() {
    return artifact;
  }

  public ActionAnalysisMetadata getAttemptedAction() {
    return attemptedAction;
  }

  private static String createDetailedMessage(
      Artifact artifact,
      ActionKeyContext actionKeyContext,
      ActionAnalysisMetadata a,
      ActionAnalysisMetadata b) {
    return "file '"
        + artifact.prettyPrint()
        + "' is generated by these conflicting actions:\n"
        + debugSuffix(actionKeyContext, a, b);
  }

  private static String createPrefixDetailedMessage(
      Artifact firstArtifact, Artifact secondArtifact, Label firstOwner, Label secondOwner) {
    return String.format(
        "One of the output paths '%s' (belonging to %s) and '%s' (belonging to %s) is a"
            + " prefix of the other. These actions cannot be simultaneously present; please"
            + " rename one of the output files or build just one of them",
        firstArtifact.getExecPath(), firstOwner, secondArtifact.getExecPath(), secondOwner);
  }

  public void reportTo(EventHandler eventListener) {
    eventListener.handle(Event.error(this.getMessage()));
  }

  @Override
  public DetailedExitCode getDetailedExitCode() {
    return DetailedExitCode.of(
        FailureDetail.newBuilder()
            .setMessage(getMessage())
            .setAnalysis(
                Analysis.newBuilder()
                    .setCode(
                        isPrefixConflict ? Code.ARTIFACT_PREFIX_CONFLICT : Code.ACTION_CONFLICT))
            .build());
  }

  private static void addStringDetail(StringBuilder sb, String key, String valueA, String valueB) {
    valueA = valueA != null ? valueA : "(null)";
    valueB = valueB != null ? valueB : "(null)";

    sb.append(key).append(": ").append(valueA);
    if (!valueA.equals(valueB)) {
      sb.append(", ").append(valueB);
    }
    sb.append("\n");
  }

  private static void addListDetail(
      StringBuilder sb, String key, Iterable<Artifact> valueA, Iterable<Artifact> valueB) {
    Set<Artifact> diffA = differenceWithoutOwner(valueA, valueB);
    Set<Artifact> diffB = differenceWithoutOwner(valueB, valueA);

    sb.append(key).append(": ");
    if (diffA.isEmpty() && diffB.isEmpty()) {
      sb.append("are equal\n");
    } else {
      if (!diffA.isEmpty()) {
        sb.append(
            "Attempted action contains artifacts not in previous action (first "
                + MAX_DIFF_ARTIFACTS_TO_REPORT
                + "): \n");
        prettyPrintArtifactDiffs(sb, diffA);
      }

      if (!diffB.isEmpty()) {
        sb.append(
            "Previous action contains artifacts not in attempted action (first "
                + MAX_DIFF_ARTIFACTS_TO_REPORT
                + "): \n");
        prettyPrintArtifactDiffs(sb, diffB);
      }
    }
  }

  /** Returns items in {@code valueA} that are not in {@code valueB}, ignoring the owner. */
  private static Set<Artifact> differenceWithoutOwner(
      Iterable<Artifact> valueA, Iterable<Artifact> valueB) {
    ImmutableSet.Builder<Artifact> diff = new ImmutableSet.Builder<>();

    // Group valueB by exec path for easier checks.
    ImmutableListMultimap<String, Artifact> mapB =
        Streams.stream(valueB)
            .collect(toImmutableListMultimap(Artifact::getExecPathString, Functions.identity()));
    for (Artifact a : valueA) {
      boolean found = false;
      for (Artifact b : mapB.get(a.getExecPathString())) {
        if (a.equalsWithoutOwner(b)) {
          found = true;
          break;
        }
      }
      if (!found) {
        diff.add(a);
      }
    }

    return diff.build();
  }

  /** Pretty print action diffs (at most {@code MAX_DIFF_ARTIFACTS_TO_REPORT} lines). */
  private static void prettyPrintArtifactDiffs(StringBuilder sb, Set<Artifact> diff) {
    for (Artifact artifact : Iterables.limit(diff, MAX_DIFF_ARTIFACTS_TO_REPORT)) {
      sb.append('\t').append(artifact.prettyPrint()).append('\n');
    }
  }

  // See also Actions.canBeShared()
  private static String debugSuffix(
      ActionKeyContext actionKeyContext, ActionAnalysisMetadata a, ActionAnalysisMetadata b) {
    // Note: the error message reveals to users the names of intermediate files that are not
    // documented in the BUILD language.  This error-reporting logic is rather elaborate but it
    // does help to diagnose some tricky situations.
    StringBuilder sb = new StringBuilder();
    ActionOwner aOwner = a.getOwner();
    ActionOwner bOwner = b.getOwner();
    boolean aNull = aOwner == null;
    boolean bNull = bOwner == null;

    addStringDetail(
        sb,
        "Label",
        aNull ? null : Label.print(aOwner.getLabel()),
        bNull ? null : Label.print(bOwner.getLabel()));
    if ((!aNull && !aOwner.getAspectDescriptors().isEmpty())
        || (!bNull && !bOwner.getAspectDescriptors().isEmpty())) {
      addStringDetail(sb, "Aspects", aspectDescriptor(aOwner), aspectDescriptor(bOwner));
    }
    addStringDetail(
        sb,
        "RuleClass",
        aNull ? null : aOwner.getTargetKind(),
        bNull ? null : bOwner.getTargetKind());
    addStringDetail(
        sb,
        "JavaActionClass",
        aNull ? null : a.getClass().toString(),
        bNull ? null : b.getClass().toString());
    addStringDetail(
        sb,
        "Configuration",
        aNull ? null : aOwner.getConfigurationChecksum(),
        bNull ? null : bOwner.getConfigurationChecksum());
    addStringDetail(sb, "Mnemonic", a.getMnemonic(), b.getMnemonic());
    addStringDetail(
        sb, "IsShareable", String.valueOf(a.isShareable()), String.valueOf(b.isShareable()));
    try {
      addStringDetail(
          sb,
          "Action key",
          a.getKey(actionKeyContext, /* inputMetadataProvider= */ null),
          b.getKey(actionKeyContext, /* inputMetadataProvider= */ null));
    } catch (InterruptedException e) {
      // Only for debugging - skip the key and carry on.
      addStringDetail(sb, "Action key", "<elided due to interrupt>", "<elided due to interrupt>");
      Thread.currentThread().interrupt();
    }

    if ((a instanceof ActionExecutionMetadata) && (b instanceof ActionExecutionMetadata)) {
      addStringDetail(
          sb,
          "Progress message",
          ((ActionExecutionMetadata) a).getProgressMessage(),
          ((ActionExecutionMetadata) b).getProgressMessage());
      addStringDetail(
          sb,
          "Action describeKey",
          ((ActionExecutionMetadata) a).describeKey(),
          ((ActionExecutionMetadata) b).describeKey());
    }

    Artifact aPrimaryInput = a.getPrimaryInput();
    Artifact bPrimaryInput = b.getPrimaryInput();
    addStringDetail(
        sb,
        "PrimaryInput",
        aPrimaryInput == null ? null : aPrimaryInput.toString(),
        bPrimaryInput == null ? null : bPrimaryInput.toString());
    addStringDetail(
        sb, "PrimaryOutput", a.getPrimaryOutput().toString(), b.getPrimaryOutput().toString());

    // Only add list details if the primary input of A matches the input of B. Otherwise
    // the above information is enough and list diff detail is not needed.
    if ((aPrimaryInput == null && bPrimaryInput == null)
        || (aPrimaryInput != null
            && bPrimaryInput != null
            && aPrimaryInput.toString().equals(bPrimaryInput.toString()))) {
      Artifact aPrimaryOutput = a.getPrimaryOutput();
      Artifact bPrimaryOutput = b.getPrimaryOutput();
      if (!aPrimaryOutput.equalsWithoutOwner(bPrimaryOutput)) {
        sb.append("Primary outputs are different: ")
            .append(System.identityHashCode(aPrimaryOutput))
            .append(", ")
            .append(System.identityHashCode(bPrimaryOutput))
            .append('\n');
      }
      ArtifactOwner aArtifactOwner = aPrimaryOutput.getArtifactOwner();
      ArtifactOwner bArtifactOwner = bPrimaryOutput.getArtifactOwner();
      addStringDetail(
          sb, "Owner information", aArtifactOwner.toString(), bArtifactOwner.toString());
      addListDetail(
          sb, "MandatoryInputs", a.getMandatoryInputs().toList(), b.getMandatoryInputs().toList());
      addListDetail(sb, "Outputs", a.getOutputs(), b.getOutputs());
    }

    return sb.toString();
  }

  @Nullable
  private static String aspectDescriptor(ActionOwner owner) {
    return owner == null
        ? null
        : owner.getAspectDescriptors().stream()
            .map(AspectDescriptor::getDescription)
            .collect(Collectors.joining(",", "[", "]"));
  }

  @Nullable
  public ActionLookupKey getAspectKey() {
    return null;
  }

  /**
   * For skymeld.
   *
   * <p>We need to forward the AspectKey along so that it's available for the final conflict report.
   */
  private static final class WithAspectKeyInfo extends ActionConflictException {
    private final ActionLookupKey aspectKey;

    private WithAspectKeyInfo(ActionConflictException e, ActionLookupKey aspectKey) {
      super(e.artifact, e.attemptedAction, e.getMessage(), e.isPrefixConflict);
      this.aspectKey = aspectKey;
    }

    @Override
    public ActionLookupKey getAspectKey() {
      return aspectKey;
    }
  }
}
