/*
 * 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.phoenix.schema.task;

import edu.umd.cs.findbugs.annotations.SuppressWarnings;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Mutation;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.client.coprocessor.Batch;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.coprocessor.generated.MetaDataProtos.MetaDataResponse;
import org.apache.phoenix.coprocessor.generated.TaskMetaDataProtos.TaskMetaDataService;
import org.apache.phoenix.coprocessorclient.MetaDataProtocol.MetaDataMutationResult;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.transform.SystemTransformRecord;
import org.apache.phoenix.util.ClientUtil;
import org.apache.phoenix.util.EnvironmentEdgeManager;
import org.apache.phoenix.util.QueryUtil;
import org.apache.phoenix.util.SchemaUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.phoenix.thirdparty.com.google.common.base.Strings;

@SuppressWarnings(value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING",
    justification = "Not possible to avoid")
public class Task {
  public static final Logger LOGGER = LoggerFactory.getLogger(Task.class);

  protected static List<Mutation> executeStatementAndGetTaskMutations(PhoenixConnection conn,
    PreparedStatement stmt) throws SQLException {
    stmt.execute();
    // retrieve mutations for SYSTEM.TASK upsert query
    Iterator<Pair<byte[], List<Mutation>>> iterator = conn.getMutationState().toMutations();
    return iterator.next().getSecond();
  }

  protected static PreparedStatement setValuesToAddTaskPS(PreparedStatement stmt,
    PTable.TaskType taskType, String tenantId, String schemaName, String tableName,
    String taskStatus, String data, Integer priority, Timestamp startTs, Timestamp endTs)
    throws SQLException {
    stmt.setByte(1, taskType.getSerializedValue());
    if (tenantId != null) {
      stmt.setString(2, tenantId);
    } else {
      stmt.setNull(2, Types.VARCHAR);
    }
    if (schemaName != null) {
      stmt.setString(3, schemaName);
    } else {
      stmt.setNull(3, Types.VARCHAR);
    }
    stmt.setString(4, tableName);
    if (taskStatus != null) {
      stmt.setString(5, taskStatus);
    } else {
      stmt.setString(5, PTable.TaskStatus.CREATED.toString());
    }
    if (priority != null) {
      stmt.setInt(6, priority);
    } else {
      byte defaultPri = 4;
      stmt.setInt(6, defaultPri);
    }
    if (startTs == null) {
      startTs = new Timestamp(EnvironmentEdgeManager.currentTimeMillis());
    }
    stmt.setTimestamp(7, startTs);
    if (endTs != null) {
      stmt.setTimestamp(8, endTs);
    } else {
      if (taskStatus != null && taskStatus.equals(PTable.TaskStatus.COMPLETED.toString())) {
        endTs = new Timestamp(EnvironmentEdgeManager.currentTimeMillis());
        stmt.setTimestamp(8, endTs);
      } else {
        stmt.setNull(8, Types.TIMESTAMP);
      }
    }
    if (data != null) {
      stmt.setString(9, data);
    } else {
      stmt.setNull(9, Types.VARCHAR);
    }
    return stmt;
  }

  protected static PreparedStatement addTaskAndGetStatement(SystemTaskParams systemTaskParams,
    PhoenixConnection connection) throws IOException {
    PreparedStatement stmt;
    try {
      stmt = connection.prepareStatement("UPSERT INTO " + PhoenixDatabaseMetaData.SYSTEM_TASK_NAME
        + " ( " + PhoenixDatabaseMetaData.TASK_TYPE + ", " + PhoenixDatabaseMetaData.TENANT_ID
        + ", " + PhoenixDatabaseMetaData.TABLE_SCHEM + ", " + PhoenixDatabaseMetaData.TABLE_NAME
        + ", " + PhoenixDatabaseMetaData.TASK_STATUS + ", " + PhoenixDatabaseMetaData.TASK_PRIORITY
        + ", " + PhoenixDatabaseMetaData.TASK_TS + ", " + PhoenixDatabaseMetaData.TASK_END_TS + ", "
        + PhoenixDatabaseMetaData.TASK_DATA + " ) VALUES(?,?,?,?,?,?,?,?,?)");
      stmt = setValuesToAddTaskPS(stmt, systemTaskParams.getTaskType(),
        systemTaskParams.getTenantId(), systemTaskParams.getSchemaName(),
        systemTaskParams.getTableName(), systemTaskParams.getTaskStatus(),
        systemTaskParams.getData(), systemTaskParams.getPriority(), systemTaskParams.getStartTs(),
        systemTaskParams.getEndTs());
      LOGGER.info(
        "Adding task type: {} , tableName: {} , taskStatus: {}" + " , startTs: {} , endTs: {}",
        systemTaskParams.getTaskType(), systemTaskParams.getTableName(),
        systemTaskParams.getTaskStatus(), systemTaskParams.getStartTs(),
        systemTaskParams.getEndTs());
    } catch (SQLException e) {
      throw new IOException(e);
    }
    return stmt;
  }

  public static List<Mutation> getMutationsForAddTask(SystemTaskParams systemTaskParams)
    throws IOException, SQLException {
    PhoenixConnection curConn = systemTaskParams.getConn();
    Configuration conf = curConn.getQueryServices().getConfiguration();
    // create new connection as we do not want to mix up mutationState
    // with existing connection
    try (PhoenixConnection newConnection = QueryUtil
      .getConnectionOnServer(curConn.getClientInfo(), conf).unwrap(PhoenixConnection.class)) {
      try (PreparedStatement statement = addTaskAndGetStatement(systemTaskParams, newConnection)) {
        return executeStatementAndGetTaskMutations(newConnection, statement);
      }
    }
  }

  /**
   * Invoke SYSTEM.TASK metadata coprocessor endpoint
   * @param connection Phoenix Connection
   * @param rowKey     key corresponding to SYSTEM.TASK mutation
   * @param callable   used to invoke the coprocessor endpoint to upsert records in SYSTEM.TASK
   * @return result of invoking the coprocessor endpoint
   * @throws SQLException If something goes wrong while executing co
   */
  public static MetaDataMutationResult taskMetaDataCoprocessorExec(
    final PhoenixConnection connection, final byte[] rowKey,
    final Batch.Call<TaskMetaDataService, MetaDataResponse> callable) throws SQLException {
    TableName tableName = SchemaUtil.getPhysicalName(PhoenixDatabaseMetaData.SYSTEM_TASK_NAME_BYTES,
      connection.getQueryServices().getProps());
    try (Table table = connection.getQueryServices().getTable(tableName.getName())) {
      final Map<byte[], MetaDataResponse> results =
        table.coprocessorService(TaskMetaDataService.class, rowKey, rowKey, callable);
      assert results.size() == 1;
      MetaDataResponse result = results.values().iterator().next();
      return MetaDataMutationResult.constructFromProto(result);
    } catch (IOException e) {
      throw ClientUtil.parseServerException(e);
    } catch (Throwable t) {
      throw new SQLException(t);
    }
  }

  private static List<TaskRecord> populateTasks(Connection connection, String taskQuery)
    throws SQLException {
    List<TaskRecord> result = new ArrayList<>();
    try (PreparedStatement taskStatement = connection.prepareStatement(taskQuery);
      ResultSet rs = taskStatement.executeQuery()) {
      while (rs.next()) {
        // delete child views only if the parent table is deleted from the system catalog
        TaskRecord taskRecord = parseResult(rs);
        result.add(taskRecord);
      }
    }
    return result;
  }

  public static List<TaskRecord> queryTaskTable(Connection connection, Timestamp ts, String schema,
    String tableName, PTable.TaskType taskType, String tenantId, String indexName)
    throws SQLException {
    String taskQuery = "SELECT " + PhoenixDatabaseMetaData.TASK_TS + ", "
      + PhoenixDatabaseMetaData.TENANT_ID + ", " + PhoenixDatabaseMetaData.TABLE_SCHEM + ", "
      + PhoenixDatabaseMetaData.TABLE_NAME + ", " + PhoenixDatabaseMetaData.TASK_STATUS + ", "
      + PhoenixDatabaseMetaData.TASK_TYPE + ", " + PhoenixDatabaseMetaData.TASK_PRIORITY + ", "
      + PhoenixDatabaseMetaData.TASK_DATA + " FROM " + PhoenixDatabaseMetaData.SYSTEM_TASK_NAME;
    taskQuery += " WHERE " + PhoenixDatabaseMetaData.TABLE_NAME + " ='" + tableName + "' AND "
      + PhoenixDatabaseMetaData.TASK_TYPE + "=" + taskType.getSerializedValue();
    if (!Strings.isNullOrEmpty(tenantId)) {
      taskQuery += " AND " + PhoenixDatabaseMetaData.TENANT_ID + "='" + tenantId + "' ";
    }

    if (!Strings.isNullOrEmpty(schema)) {
      taskQuery += " AND " + PhoenixDatabaseMetaData.TABLE_SCHEM + "='" + schema + "' ";
    }

    if (!Strings.isNullOrEmpty(indexName)) {
      taskQuery += " AND " + PhoenixDatabaseMetaData.TASK_DATA + " LIKE '%" + indexName + "%'";
    }

    List<TaskRecord> taskRecords = populateTasks(connection, taskQuery);
    List<TaskRecord> result = new ArrayList<TaskRecord>();
    if (ts != null) {
      // Adding TASK_TS to the where clause did not work. It returns empty when directly querying
      // with the timestamp.
      for (TaskRecord tr : taskRecords) {
        if (tr.getTimeStamp().equals(ts)) {
          result.add(tr);
        }
      }
    } else {
      result = taskRecords;
    }

    return result;
  }

  public static List<TaskRecord> queryTaskTable(Connection connection, String[] excludedTaskStatus)
    throws SQLException {
    String taskQuery = "SELECT " + PhoenixDatabaseMetaData.TASK_TS + ", "
      + PhoenixDatabaseMetaData.TENANT_ID + ", " + PhoenixDatabaseMetaData.TABLE_SCHEM + ", "
      + PhoenixDatabaseMetaData.TABLE_NAME + ", " + PhoenixDatabaseMetaData.TASK_STATUS + ", "
      + PhoenixDatabaseMetaData.TASK_TYPE + ", " + PhoenixDatabaseMetaData.TASK_PRIORITY + ", "
      + PhoenixDatabaseMetaData.TASK_DATA + " FROM " + PhoenixDatabaseMetaData.SYSTEM_TASK_NAME;
    if (excludedTaskStatus != null && excludedTaskStatus.length > 0) {
      taskQuery += " WHERE " + PhoenixDatabaseMetaData.TASK_STATUS + " IS NULL OR "
        + PhoenixDatabaseMetaData.TASK_STATUS + " NOT IN (";
      String[] values = new String[excludedTaskStatus.length];
      for (int i = 0; i < excludedTaskStatus.length; i++) {
        values[i] = String.format("'%s'", excludedTaskStatus[i].trim());
      }

      // Delimit with comma
      taskQuery += String.join(",", values);
      taskQuery += ")";
    }

    return populateTasks(connection, taskQuery);
  }

  public static TaskRecord parseResult(ResultSet rs) throws SQLException {
    TaskRecord taskRecord = new TaskRecord();
    taskRecord.setTimeStamp(rs.getTimestamp(PhoenixDatabaseMetaData.TASK_TS));
    taskRecord.setTenantId(rs.getString(PhoenixDatabaseMetaData.TENANT_ID));
    taskRecord.setTenantIdBytes(rs.getBytes(PhoenixDatabaseMetaData.TENANT_ID));
    taskRecord.setSchemaName(rs.getString(PhoenixDatabaseMetaData.TABLE_SCHEM));
    taskRecord.setSchemaNameBytes(rs.getBytes(PhoenixDatabaseMetaData.TABLE_SCHEM));
    taskRecord.setTableName(rs.getString(PhoenixDatabaseMetaData.TABLE_NAME));
    taskRecord.setTableNameBytes(rs.getBytes(PhoenixDatabaseMetaData.TABLE_NAME));
    taskRecord.setStatus(rs.getString(PhoenixDatabaseMetaData.TASK_STATUS));
    taskRecord.setTaskType(
      PTable.TaskType.fromSerializedValue(rs.getByte(PhoenixDatabaseMetaData.TASK_TYPE)));
    taskRecord.setPriority(rs.getInt(PhoenixDatabaseMetaData.TASK_PRIORITY));
    taskRecord.setData(rs.getString(PhoenixDatabaseMetaData.TASK_DATA));
    return taskRecord;
  }

  public static class TaskRecord {
    private String tenantId;
    private Timestamp timeStamp;
    private byte[] tenantIdBytes;
    private String schemaName = null;
    private byte[] schemaNameBytes;
    private String tableName = null;
    private byte[] tableNameBytes;

    private PTable.TaskType taskType;
    private String status;
    private int priority;
    private String data;

    public String getTenantId() {
      return tenantId;
    }

    public void setTenantId(String tenantId) {
      this.tenantId = tenantId;
    }

    public Timestamp getTimeStamp() {
      return timeStamp;
    }

    public void setTimeStamp(Timestamp timeStamp) {
      this.timeStamp = timeStamp;
    }

    public byte[] getTenantIdBytes() {
      return tenantIdBytes;
    }

    public void setTenantIdBytes(byte[] tenantIdBytes) {
      this.tenantIdBytes = tenantIdBytes;
    }

    public String getSchemaName() {
      return schemaName;
    }

    public void setSchemaName(String schemaName) {
      this.schemaName = schemaName;
    }

    public byte[] getSchemaNameBytes() {
      return schemaNameBytes;
    }

    public void setSchemaNameBytes(byte[] schemaNameBytes) {
      this.schemaNameBytes = schemaNameBytes;
    }

    public String getTableName() {
      return tableName;
    }

    public void setTableName(String tableName) {
      this.tableName = tableName;
    }

    public byte[] getTableNameBytes() {
      return tableNameBytes;
    }

    public void setTableNameBytes(byte[] tableNameBytes) {
      this.tableNameBytes = tableNameBytes;
    }

    public String getData() {
      if (data == null) {
        return "";
      }
      return data;
    }

    public int getPriority() {
      return priority;
    }

    public void setPriority(int priority) {
      this.priority = priority;
    }

    public void setData(String data) {
      this.data = data;
    }

    public String getStatus() {
      return status;
    }

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

    public PTable.TaskType getTaskType() {
      return taskType;
    }

    public void setTaskType(PTable.TaskType taskType) {
      this.taskType = taskType;
    }

    public boolean isMatchingTask(SystemTransformRecord transformRecord) {
      if (getTaskType() != PTable.TaskType.TRANSFORM_MONITOR) {
        return false;
      }
      if (
        StringUtils.equals(transformRecord.getLogicalTableName(), getTableName())
          && StringUtils.equals(transformRecord.getTenantId(), getTenantId())
          && StringUtils.equals(transformRecord.getSchemaName(), getSchemaName())
      ) {
        return true;
      }
      return false;
    }
  }
}
