/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.ambari.server.orm.entities;

import static org.apache.commons.lang.StringUtils.defaultString;

import java.util.Arrays;

import jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinColumns;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.TableGenerator;

import org.apache.ambari.server.Role;
import org.apache.ambari.server.RoleCommand;
import org.apache.ambari.server.actionmanager.HostRoleStatus;
import org.apache.commons.lang.ArrayUtils;

@Entity
@Table(name = "host_role_command"
       , indexes = {
           @Index(name = "idx_hrc_request_id", columnList = "request_id")
         , @Index(name = "idx_hrc_status_role", columnList = "status, role")
       })
@TableGenerator(name = "host_role_command_id_generator",
    table = "ambari_sequences", pkColumnName = "sequence_name", valueColumnName = "sequence_value"
    , pkColumnValue = "host_role_command_id_seq"
    , initialValue = 1
)
@NamedQueries({
    @NamedQuery(
        name = "HostRoleCommandEntity.findTaskIdsByRequestStageIds",
        query = "SELECT command.taskId FROM HostRoleCommandEntity command WHERE command.stageId = :stageId AND command.requestId = :requestId"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findCountByCommandStatuses",
        query = "SELECT COUNT(command.taskId) FROM HostRoleCommandEntity command WHERE command.status IN :statuses"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findByRequestIdAndStatuses",
        query = "SELECT task FROM HostRoleCommandEntity task WHERE task.requestId=:requestId AND task.status IN :statuses ORDER BY task.taskId ASC"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findTasksByStatusesOrderByIdDesc",
        query = "SELECT task FROM HostRoleCommandEntity task WHERE task.requestId = :requestId AND task.status IN :statuses ORDER BY task.taskId DESC"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findNumTasksAlreadyRanInStage",
        query = "SELECT COUNT(task.taskId) FROM HostRoleCommandEntity task WHERE task.requestId = :requestId AND task.taskId > :taskId AND task.stageId > :stageId AND task.status NOT IN :statuses"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findByCommandStatuses",
        query = "SELECT command FROM HostRoleCommandEntity command WHERE command.status IN :statuses ORDER BY command.requestId, command.stageId"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findByHostId",
        query = "SELECT command FROM HostRoleCommandEntity command WHERE command.hostId=:hostId"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findByHostRole",
        query = "SELECT command FROM HostRoleCommandEntity command WHERE command.hostEntity.hostName=:hostName AND command.requestId=:requestId AND command.stageId=:stageId AND command.role=:role ORDER BY command.taskId"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findByHostRoleNullHost",
        query = "SELECT command FROM HostRoleCommandEntity command WHERE command.hostEntity IS NULL AND command.requestId=:requestId AND command.stageId=:stageId AND command.role=:role"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findByStatusBetweenStages",
        query = "SELECT command FROM HostRoleCommandEntity command WHERE command.requestId = :requestId AND command.stageId >= :minStageId AND command.stageId <= :maxStageId AND command.status = :status"),
    @NamedQuery(
        name = "HostRoleCommandEntity.updateAutoSkipExcludeRoleCommand",
        query = "UPDATE HostRoleCommandEntity command SET command.autoSkipOnFailure = :autoSkipOnFailure WHERE command.requestId = :requestId AND command.roleCommand <> :roleCommand"),
    @NamedQuery(
        name = "HostRoleCommandEntity.updateAutoSkipForRoleCommand",
        query = "UPDATE HostRoleCommandEntity command SET command.autoSkipOnFailure = :autoSkipOnFailure WHERE command.requestId = :requestId AND command.roleCommand = :roleCommand"),
    @NamedQuery(
        name = "HostRoleCommandEntity.removeByTaskIds",
        query = "DELETE FROM HostRoleCommandEntity command WHERE command.taskId IN :taskIds"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findHostsByCommandStatus",
        query = "SELECT DISTINCT(host.hostName) FROM HostRoleCommandEntity command, HostEntity host WHERE (command.requestId >= :iLowestRequestIdInProgress AND command.requestId <= :iHighestRequestIdInProgress) AND command.status IN :statuses AND command.hostId = host.hostId AND host.hostName IS NOT NULL"),
    @NamedQuery(
        name = "HostRoleCommandEntity.getBlockingHostsForRequest",
        query = "SELECT DISTINCT(host.hostName) FROM HostRoleCommandEntity command, HostEntity host WHERE command.requestId >= :lowerRequestIdInclusive AND command.requestId < :upperRequestIdExclusive AND command.status IN :statuses AND command.isBackgroundCommand=0 AND command.hostId = host.hostId AND host.hostName IS NOT NULL"),
    @NamedQuery(
        name = "HostRoleCommandEntity.findLatestServiceChecksByRole",
        query = "SELECT NEW org.apache.ambari.server.orm.dao.HostRoleCommandDAO.LastServiceCheckDTO(command.role, MAX(command.endTime)) FROM HostRoleCommandEntity command WHERE command.roleCommand = :roleCommand AND command.endTime > 0 AND command.stage.clusterId = :clusterId GROUP BY command.role ORDER BY command.role ASC"),
    @NamedQuery(
      name = "HostRoleCommandEntity.findByRequestId",
      query = "SELECT command FROM HostRoleCommandEntity command WHERE command.requestId = :requestId ORDER BY command.taskId")
})
public class HostRoleCommandEntity {

  private static int MAX_COMMAND_DETAIL_LENGTH = 250;

  @Column(name = "task_id")
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE, generator = "host_role_command_id_generator")
  private Long taskId;

  @Column(name = "request_id", insertable = false, updatable = false, nullable = false)
  @Basic
  private Long requestId;

  @Column(name = "stage_id", insertable = false, updatable = false, nullable = false)
  @Basic
  private Long stageId;

  @Column(name = "host_id", insertable = false, updatable = false, nullable = true)
  @Basic
  private Long hostId;

  @Column(name = "role")
  private String role;

  @Column(name = "event", length = 32000)
  @Basic
  @Lob
  private String event = "";

  @Column(name = "exitcode", nullable = false)
  @Basic
  private Integer exitcode = 0;

  @Column(name = "status", nullable = false)
  @Enumerated(EnumType.STRING)
  private HostRoleStatus status = HostRoleStatus.PENDING;

  @Column(name = "std_error")
  @Lob
  @Basic
  private byte[] stdError = new byte[0];

  @Column(name = "std_out")
  @Lob
  @Basic
  private byte[] stdOut = new byte[0];

  @Column(name = "output_log")
  @Basic
  private String outputLog = null;

  @Column(name = "error_log")
  @Basic
  private String errorLog = null;


  @Column(name = "structured_out")
  @Lob
  @Basic
  private byte[] structuredOut = new byte[0];

  @Basic
  @Column(name = "start_time", nullable = false)
  private Long startTime = -1L;

  /**
   * Because the startTime is allowed to be overwritten, introduced a new column for the original start time.
   */
  @Basic
  @Column(name = "original_start_time", nullable = false)
  private Long originalStartTime = -1L;

  @Basic
  @Column(name = "end_time", nullable = false)
  private Long endTime = -1L;

  @Basic
  @Column(name = "last_attempt_time", nullable = false)
  private Long lastAttemptTime = -1L;

  @Basic
  @Column(name = "attempt_count", nullable = false)
  private Short attemptCount = 0;

  @Column(name = "retry_allowed", nullable = false)
  private Integer retryAllowed = Integer.valueOf(0);

  /**
   * If the command fails and is skippable, then this will instruct the
   * scheduler to skip the command.
   */
  @Column(name = "auto_skip_on_failure", nullable = false)
  private Integer autoSkipOnFailure = Integer.valueOf(0);

  // This is really command type as well as name
  @Column(name = "role_command")
  @Enumerated(EnumType.STRING)
  private RoleCommand roleCommand;

  // A readable description of the command
  @Column(name = "command_detail")
  @Basic
  private String commandDetail;

  // An optional property that can be used for setting the displayName for operations window
  @Column(name = "ops_display_name")
  @Basic
  private String opsDisplayName;

  // When command type id CUSTOM_COMMAND and CUSTOM_ACTION this is the name
  @Column(name = "custom_command_name")
  @Basic
  private String customCommandName;

  @OneToOne(mappedBy = "hostRoleCommand", cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
  private ExecutionCommandEntity executionCommand;

  @ManyToOne(cascade = {CascadeType.MERGE})
  @JoinColumns({@JoinColumn(name = "request_id", referencedColumnName = "request_id", nullable = false), @JoinColumn(name = "stage_id", referencedColumnName = "stage_id", nullable = false)})
  private StageEntity stage;

  @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH})
  @JoinColumn(name = "host_id", referencedColumnName = "host_id", nullable = true)
  private HostEntity hostEntity;

  @OneToOne(mappedBy = "hostRoleCommandEntity", cascade = CascadeType.REMOVE)
  private TopologyLogicalTaskEntity topologyLogicalTaskEntity;

  @Basic
  @Column(name = "is_background", nullable = false)
  private short isBackgroundCommand = 0;

  public Long getTaskId() {
    return taskId;
  }

  public void setTaskId(Long taskId) {
    this.taskId = taskId;
  }

  public Long getRequestId() {
    return requestId;
  }

  public void setRequestId(Long requestId) {
    this.requestId = requestId;
  }

  public Long getStageId() {
    return stageId;
  }

  public void setStageId(Long stageId) {
    this.stageId = stageId;
  }

  public String getHostName() {
    return hostEntity != null ? hostEntity.getHostName() : null;
  }

  public Long getHostId() {
    return hostEntity != null ? hostEntity.getHostId() : null;
  }

  public Role getRole() {
    return Role.valueOf(role);
  }

  public void setRole(Role role) {
    this.role = role.name();
  }

  public String getEvent() {
    return defaultString(event);
  }

  public void setEvent(String event) {
    this.event = event;
  }

  public Integer getExitcode() {
    return exitcode;
  }

  public void setExitcode(Integer exitcode) {
    this.exitcode = exitcode;
  }

  public HostRoleStatus getStatus() {
    return status;
  }

  public void setStatus(HostRoleStatus status) {
    this.status = status;
  }

  public byte[] getStdError() {
    return ArrayUtils.nullToEmpty(stdError);
  }

  public void setStdError(byte[] stdError) {
    this.stdError = stdError;
  }

  public byte[] getStdOut() {
    return ArrayUtils.nullToEmpty(stdOut);
  }

  public void setStdOut(byte[] stdOut) {
    this.stdOut = stdOut;
  }

  public String getOutputLog() { return outputLog; }

  public void setOutputLog(String outputLog) { this.outputLog = outputLog; }

  public String getErrorLog() { return errorLog; }

  public void setErrorLog(String errorLog) { this.errorLog = errorLog; }

  public Long getStartTime() {
    return startTime;
  }

  public void setStartTime(Long startTime) {
    this.startTime = startTime;
  }

  /**
   * Get the original time the command was first scheduled on the agent. This value is never overwritten.
   * @return Original start time
   */
  public Long getOriginalStartTime() {
    return originalStartTime;
  }

  /**
   * Set the original start time when the command is first scheduled. This value is never overwritten.
   * @param originalStartTime Original start time
   */
  public void setOriginalStartTime(Long originalStartTime) {
    this.originalStartTime = originalStartTime;
  }

  public Long getLastAttemptTime() {
    return lastAttemptTime;
  }

  public void setLastAttemptTime(Long lastAttemptTime) {
    this.lastAttemptTime = lastAttemptTime;
  }

  public Short getAttemptCount() {
    return attemptCount;
  }

  public void setAttemptCount(Short attemptCount) {
    this.attemptCount = attemptCount;
  }

  public RoleCommand getRoleCommand() {
    return roleCommand;
  }

  public void setRoleCommand(RoleCommand roleCommand) {
    this.roleCommand = roleCommand;
  }

  public byte[] getStructuredOut() {
    return structuredOut;
  }

  public void setStructuredOut(byte[] structuredOut) {
    this.structuredOut = structuredOut;
  }

  public Long getEndTime() {
    return endTime;
  }

  public void setEndTime(Long endTime) {
    this.endTime = endTime;
  }

  public String getCommandDetail() {
    return commandDetail;
  }

  public void setCommandDetail(String commandDetail) {
    String truncatedCommandDetail = commandDetail;
    if (commandDetail != null) {
      if (commandDetail.length() > MAX_COMMAND_DETAIL_LENGTH) {
        truncatedCommandDetail = commandDetail.substring(0, MAX_COMMAND_DETAIL_LENGTH) + "...";
      }
    }
    this.commandDetail = truncatedCommandDetail;
  }

  public String getCustomCommandName() {
    return customCommandName;
  }

  public void setCustomCommandName(String customCommandName) {
    this.customCommandName = customCommandName;
  }

  public String getOpsDisplayName() {
    return opsDisplayName;
  }

  public void setOpsDisplayName(String opsDisplayName) {
    this.opsDisplayName = opsDisplayName;
  }

  

  /**
   * Determine whether this task should hold for retry when an error occurs.
   *
   * @return {@code true} if this task should hold for retry when an error occurs
   */
  public boolean isRetryAllowed() {
    return retryAllowed != 0;
  }

  /**
   * Sets whether this task should hold for retry when an error occurs.
   *
   * @param enabled  {@code true} if this task should hold for retry when an error occurs
   */
  public void setRetryAllowed(boolean enabled) {
    retryAllowed = enabled ? 1 : 0;
  }

  /**
   * Gets whether commands which fail and are retryable are automatically
   * skipped and marked with {@link HostRoleStatus#SKIPPED_FAILED}.
   *
   * @return
   */
  public boolean isFailureAutoSkipped() {
    return autoSkipOnFailure != 0;
  }

  /**
   * Sets whether commands which fail and are retryable are automatically
   * skipped and marked with {@link HostRoleStatus#SKIPPED_FAILED}.
   *
   * @param skipFailures
   */
  public void setAutoSkipOnFailure(boolean skipFailures) {
    autoSkipOnFailure = skipFailures ? 1 : 0;
  }

  /**
   * Sets whether this is a command is a background command and will not block
   * other commands.
   *
   * @param runInBackground
   *          {@code true} if this is a background command, {@code false}
   *          otherwise.
   */
  public void setBackgroundCommand(boolean runInBackground) {
    isBackgroundCommand = (short) (runInBackground ? 1 : 0);
  }

  /**
   * Gets whether this command runs in the background and will not block other
   * commands.
   */
  public boolean isBackgroundCommand() {
    return isBackgroundCommand == 0 ? false : true;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    HostRoleCommandEntity that = (HostRoleCommandEntity) o;

    if (attemptCount != null ? !attemptCount.equals(that.attemptCount) : that.attemptCount != null) {
      return false;
    }
    if (event != null ? !event.equals(that.event) : that.event != null) {
      return false;
    }
    if (exitcode != null ? !exitcode.equals(that.exitcode) : that.exitcode != null) {
      return false;
    }
    if (hostEntity != null ? !hostEntity.equals(that.hostEntity) : that.hostEntity != null) {
      return false;
    }
    if (lastAttemptTime != null ? !lastAttemptTime.equals(that.lastAttemptTime) : that.lastAttemptTime != null) {
      return false;
    }
    if (requestId != null ? !requestId.equals(that.requestId) : that.requestId != null) {
      return false;
    }
    if (role != null ? !role.equals(that.role) : that.role != null) {
      return false;
    }
    if (stageId != null ? !stageId.equals(that.stageId) : that.stageId != null) {
      return false;
    }
    if (startTime != null ? !startTime.equals(that.startTime) : that.startTime != null) {
      return false;
    }
    if (originalStartTime != null ? !originalStartTime.equals(that.originalStartTime) : that.originalStartTime != null) {
      return false;
    }
    if (status != null ? !status.equals(that.status) : that.status != null) {
      return false;
    }
    if (stdError != null ? !Arrays.equals(stdError, that.stdError) : that.stdError != null) {
      return false;
    }
    if (stdOut != null ? !Arrays.equals(stdOut, that.stdOut) : that.stdOut != null) {
      return false;
    }
    if (outputLog != null ? !outputLog.equals(that.outputLog) : that.outputLog != null) {
      return false;
    }
    if (errorLog != null ? !errorLog.equals(that.errorLog) : that.errorLog != null) {
      return false;
    }
    if (taskId != null ? !taskId.equals(that.taskId) : that.taskId != null) {
      return false;
    }
    if (structuredOut != null ? !Arrays.equals(structuredOut, that.structuredOut) : that.structuredOut != null) {
      return false;
    }
    if (endTime != null ? !endTime.equals(that.endTime) : that.endTime != null) {
      return false;
    }

    return true;
  }

  @Override
  public int hashCode() {
    int result = taskId != null ? taskId.hashCode() : 0;
    result = 31 * result + (requestId != null ? requestId.hashCode() : 0);
    result = 31 * result + (stageId != null ? stageId.hashCode() : 0);
    result = 31 * result + (hostEntity != null ? hostEntity.hashCode() : 0);
    result = 31 * result + (role != null ? role.hashCode() : 0);
    result = 31 * result + (event != null ? event.hashCode() : 0);
    result = 31 * result + (exitcode != null ? exitcode.hashCode() : 0);
    result = 31 * result + (status != null ? status.hashCode() : 0);
    result = 31 * result + (stdError != null ? Arrays.hashCode(stdError) : 0);
    result = 31 * result + (stdOut != null ? Arrays.hashCode(stdOut) : 0);
    result = 31 * result + (outputLog != null ? outputLog.hashCode() : 0);
    result = 31 * result + (errorLog != null ? errorLog.hashCode() : 0);
    result = 31 * result + (startTime != null ? startTime.hashCode() : 0);
    result = 31 * result + (originalStartTime != null ? originalStartTime.hashCode() : 0);
    result = 31 * result + (lastAttemptTime != null ? lastAttemptTime.hashCode() : 0);
    result = 31 * result + (attemptCount != null ? attemptCount.hashCode() : 0);
    result = 31 * result + (endTime != null ? endTime.hashCode() : 0);
    result = 31 * result + (structuredOut != null ? Arrays.hashCode(structuredOut) : 0);
    return result;
  }

  public ExecutionCommandEntity getExecutionCommand() {
    return executionCommand;
  }

  public void setExecutionCommand(ExecutionCommandEntity executionCommandsByTaskId) {
    executionCommand = executionCommandsByTaskId;
  }

  public StageEntity getStage() {
    return stage;
  }

  /**
   * Sets the associated {@link StageEntity} for this command. If the
   * {@link StageEntity} has been persisted, then this will also set the
   * commands stage and request ID fields.
   *
   * @param stage
   */
  public void setStage(StageEntity stage) {
    this.stage = stage;

    // ensure that the IDs are also set since they may not be retrieved from JPA
    // when this entity is cached
    if (null != stage) {
      if (null == stageId) {
        stageId = stage.getStageId();
      }

      if (null == requestId) {
        requestId = stage.getRequestId();
      }
    }
  }

  public HostEntity getHostEntity() {
    return hostEntity;
  }

  public void setHostEntity(HostEntity hostEntity) {
    this.hostEntity = hostEntity;
  }

  public TopologyLogicalTaskEntity getTopologyLogicalTaskEntity() {
    return topologyLogicalTaskEntity;
  }

  public void setTopologyLogicalTaskEntity(TopologyLogicalTaskEntity topologyLogicalTaskEntity) {
    this.topologyLogicalTaskEntity = topologyLogicalTaskEntity;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    StringBuilder buffer = new StringBuilder("HostRoleCommandEntity{ ");
    buffer.append("taskId").append(taskId);
    buffer.append(", stageId=").append(stageId);
    buffer.append(", requestId=").append(requestId);
    buffer.append(", role=").append(role);
    buffer.append(", roleCommand=").append(roleCommand);
    buffer.append(", exitcode=").append(exitcode);
    buffer.append("}");
    return buffer.toString();
  }
}
