// Copyright 2014 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.base.Preconditions.checkNotNull;

import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.actions.SpawnResult.MetadataLog;
import com.google.devtools.build.lib.buildeventstream.BuildEvent;
import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
import com.google.devtools.build.lib.buildeventstream.BuildEventContext;
import com.google.devtools.build.lib.buildeventstream.BuildEventIdUtil;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId;
import com.google.devtools.build.lib.buildeventstream.BuildEventWithConfiguration;
import com.google.devtools.build.lib.buildeventstream.GenericBuildEvent;
import com.google.devtools.build.lib.buildeventstream.NullConfiguration;
import com.google.devtools.build.lib.buildeventstream.PathConverter;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.protobuf.Timestamp;
import java.time.Instant;
import java.util.Collection;
import javax.annotation.Nullable;

/**
 * This event is fired during the build, when an action is executed. It contains information about
 * the action: the Action itself, and the output file names its stdout and stderr are recorded in.
 */
public final class ActionExecutedEvent implements BuildEventWithConfiguration {
  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

  private final PathFragment actionId;
  private final Action action;
  @Nullable private final ActionExecutionException exception;
  private final Path primaryOutput;
  private final Artifact outputArtifact;
  @Nullable private final FileArtifactValue primaryOutputMetadata;
  private final Path stdout;
  private final Path stderr;
  private final ImmutableList<MetadataLog> actionMetadataLogs;
  private final ErrorTiming timing;

  /** Timestamp of the action starting; if no timestamp is available will be {@code null}. */
  @Nullable private final Instant startTime;

  /** Timestamp of the action finishing; if no timestamp is available will be {@code null}. */
  @Nullable private final Instant endTime;

  public ActionExecutedEvent(
      PathFragment actionId,
      Action action,
      @Nullable ActionExecutionException exception,
      Path primaryOutput,
      Artifact outputArtifact,
      @Nullable FileArtifactValue primaryOutputMetadata,
      Path stdout,
      Path stderr,
      ImmutableList<MetadataLog> actionMetadataLogs,
      ErrorTiming timing,
      @Nullable Instant startTime,
      @Nullable Instant endTime) {
    this.actionId = actionId;
    this.action = action;
    this.exception = exception;
    this.primaryOutput = primaryOutput;
    this.outputArtifact = outputArtifact;
    this.primaryOutputMetadata = primaryOutputMetadata;
    this.stdout = stdout;
    this.stderr = stderr;
    this.timing = timing;
    this.actionMetadataLogs = checkNotNull(actionMetadataLogs, this);
    this.startTime = startTime;
    this.endTime = endTime;
    Preconditions.checkState(
        (this.exception == null) == (this.timing == ErrorTiming.NO_ERROR), this);
    Preconditions.checkState(
        (this.exception == null) != (this.primaryOutputMetadata == null), this);
  }

  public Action getAction() {
    return action;
  }

  // null if action succeeded
  public ActionExecutionException getException() {
    return exception;
  }

  public ErrorTiming errorTiming() {
    return timing;
  }

  @Nullable
  public String getStdout() {
    if (stdout == null) {
      return null;
    }
    return stdout.toString();
  }

  @Nullable
  public String getStderr() {
    if (stderr == null) {
      return null;
    }
    return stderr.toString();
  }

  public ImmutableList<MetadataLog> getActionMetadataLogs() {
    return actionMetadataLogs;
  }

  @Nullable
  public FileArtifactValue getPrimaryOutputMetadata() {
    return primaryOutputMetadata;
  }

  @Override
  public BuildEventId getEventId() {
    if (action.getOwner() == null) {
      return BuildEventIdUtil.actionCompleted(actionId);
    } else {
      return BuildEventIdUtil.actionCompleted(
          actionId, action.getOwner().getLabel(), action.getOwner().getConfigurationChecksum());
    }
  }

  @Override
  public Collection<BuildEventId> getChildrenEvents() {
    return ImmutableList.of();
  }

  @Override
  public Collection<BuildEvent> getConfigurations() {
    if (action.getOwner() != null) {
      BuildEvent configuration = action.getOwner().getBuildConfigurationEvent();
      if (configuration == null) {
        configuration = NullConfiguration.INSTANCE;
      }
      return ImmutableList.of(configuration);
    } else {
      return ImmutableList.of();
    }
  }

  @Override
  public Collection<LocalFile> referencedLocalFiles() {
    ImmutableList.Builder<LocalFile> localFiles = ImmutableList.builder();
    // TODO(b/199940216): thread file metadata through here when possible.
    if (stdout != null) {
      localFiles.add(new LocalFile(stdout, LocalFileType.STDOUT, /* artifactMetadata= */ null));
    }
    if (stderr != null) {
      localFiles.add(new LocalFile(stderr, LocalFileType.STDERR, /* artifactMetadata= */ null));
    }
    for (MetadataLog actionMetadataLog : actionMetadataLogs) {
      localFiles.add(
          new LocalFile(
              actionMetadataLog.getFilePath(), LocalFileType.LOG, /* artifactMetadata= */ null));
    }
    if (exception == null) {
      localFiles.add(
          new LocalFile(
              primaryOutput,
              LocalFileType.forArtifact(outputArtifact, primaryOutputMetadata),
              primaryOutputMetadata));
    }
    return localFiles.build();
  }

  @Override
  public BuildEventStreamProtos.BuildEvent asStreamProto(BuildEventContext converters)
      throws InterruptedException {
    PathConverter pathConverter = converters.pathConverter();
    BuildEventStreamProtos.ActionExecuted.Builder actionBuilder =
        BuildEventStreamProtos.ActionExecuted.newBuilder()
            .setSuccess(getException() == null)
            .setType(action.getMnemonic());
    if (startTime != null) {
      actionBuilder.setStartTime(timestampProto(startTime));
      if (endTime != null) {
        actionBuilder.setEndTime(timestampProto(endTime));
      }
    }

    if (exception != null) {
      // TODO(b/150405553): This statement seems to be confused. The exit_code field of
      //  ActionExecuted is documented as "The exit code of the action, if it is available."
      //  However, the value returned by exception.getExitCode().getNumericExitCode() is intended as
      //  an exit code that this Bazel invocation might return to the user.
      actionBuilder.setExitCode(exception.getExitCode().getNumericExitCode());
      FailureDetails.FailureDetail failureDetail =
          exception.getDetailedExitCode().getFailureDetail();
      if (failureDetail != null) {
        actionBuilder.setFailureDetail(failureDetail);
      }
    }
    if (stdout != null) {
      String uri = pathConverter.apply(stdout);
      if (uri != null) {
        actionBuilder.setStdout(
            BuildEventStreamProtos.File.newBuilder().setName("stdout").setUri(uri).build());
      }
    }
    if (stderr != null) {
      String uri = pathConverter.apply(stderr);
      if (uri != null) {
        actionBuilder.setStderr(
            BuildEventStreamProtos.File.newBuilder().setName("stderr").setUri(uri).build());
      }
    }
    if (action.getOwner() != null && action.getOwner().getLabel() != null) {
      actionBuilder.setLabel(action.getOwner().getLabel().toString());
    }
    if (action.getOwner() != null) {
      BuildEvent configuration = action.getOwner().getBuildConfigurationEvent();
      if (configuration == null) {
        configuration = NullConfiguration.INSTANCE;
      }
      actionBuilder.setConfiguration(configuration.getEventId().getConfiguration());
    }
    for (MetadataLog actionMetadataLog : actionMetadataLogs) {
      String uri = pathConverter.apply(actionMetadataLog.getFilePath());
      if (uri != null) {
        actionBuilder.addActionMetadataLogs(
            BuildEventStreamProtos.File.newBuilder()
                .setName(actionMetadataLog.getName())
                .setUri(uri)
                .build());
      }
    }
    if (exception == null) {
      String uri = pathConverter.apply(primaryOutput);
      if (uri != null) {
        actionBuilder.setPrimaryOutput(
            BuildEventStreamProtos.File.newBuilder().setUri(uri).build());
      }
    }
    try {
      if (action instanceof CommandAction commandAction) {
        actionBuilder.addAllCommandLine(commandAction.getArguments());
      }
    } catch (CommandLineExpansionException e) {
      // Command-line not available, so just not report it
      logger.atInfo().withCause(e).log("Could not compute commandline of reported action");
    }
    return GenericBuildEvent.protoChaining(this).setAction(actionBuilder.build()).build();
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("exception", exception)
        .add("timing", timing)
        .add("stdout", stdout)
        .add("stderr", stderr)
        .add("action", action)
        .add("primaryOutput", primaryOutput)
        .add("outputArtifact", outputArtifact)
        .add("primaryOutputMetadata", primaryOutputMetadata)
        .add("startTime", startTime)
        .add("endTime", endTime)
        .toString();
  }

  /** When an error occurred that aborted action execution, if any. */
  public enum ErrorTiming {
    NO_ERROR,
    BEFORE_EXECUTION,
    AFTER_EXECUTION
  }

  private static Timestamp timestampProto(Instant time) {
    return Timestamp.newBuilder()
        .setSeconds(time.getEpochSecond())
        .setNanos(time.getNano())
        .build();
  }
}
