/*
 * 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 com.google.common.base.Strings;
import org.apache.hadoop.hbase.ipc.RpcCall;
import org.apache.hadoop.hbase.ipc.RpcUtil;
import org.apache.hadoop.hbase.security.User;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.util.EnvironmentEdgeManager;

import java.io.IOException;
import java.security.PrivilegedExceptionAction;
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.List;

public class Task {
    private static void mutateSystemTaskTable(PhoenixConnection conn, PreparedStatement stmt, boolean accessCheckEnabled)
            throws IOException {
        // we need to mutate SYSTEM.TASK with HBase/login user if access is enabled.
        if (accessCheckEnabled) {
            User.runAsLoginUser(new PrivilegedExceptionAction<Void>() {
                @Override
                public Void run() throws Exception {
                    final RpcCall rpcContext = RpcUtil.getRpcContext();
                    // setting RPC context as null so that user can be reset
                    try {
                        RpcUtil.setRpcContext(null);
                        stmt.execute();
                        conn.commit();
                    } catch (SQLException e) {
                        throw new IOException(e);
                    } finally {
                        // setting RPC context back to original context of the RPC
                        RpcUtil.setRpcContext(rpcContext);
                    }
                    return null;
                }
            });
        }
        else {
            try {
                stmt.execute();
                conn.commit();
            } catch (SQLException e) {
                throw new IOException(e);
            }
        }
    }

    private 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;
    }

    public static void addTask(PhoenixConnection conn, PTable.TaskType taskType, String tenantId, String schemaName,
            String tableName, String taskStatus, String data, Integer priority, Timestamp startTs, Timestamp endTs,
            boolean accessCheckEnabled)
            throws IOException {
        PreparedStatement stmt;
        try {
            stmt = conn.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, taskType, tenantId, schemaName, tableName, taskStatus, data, priority, startTs, endTs);
        } catch (SQLException e) {
            throw new IOException(e);
        }
        mutateSystemTaskTable(conn, stmt, accessCheckEnabled);
    }

    public static void deleteTask(PhoenixConnection conn, PTable.TaskType taskType, Timestamp ts, String tenantId,
            String schemaName, String tableName, boolean accessCheckEnabled) throws IOException {
        PreparedStatement stmt = null;
        try {
            stmt = conn.prepareStatement("DELETE FROM " +
                    PhoenixDatabaseMetaData.SYSTEM_TASK_NAME +
                    " WHERE " + PhoenixDatabaseMetaData.TASK_TYPE + " = ? AND " +
                    PhoenixDatabaseMetaData.TASK_TS + " = ? AND " +
                    PhoenixDatabaseMetaData.TENANT_ID + (tenantId == null ? " IS NULL " : " = '" + tenantId + "'") + " AND " +
                    PhoenixDatabaseMetaData.TABLE_SCHEM + (schemaName == null ? " IS NULL " : " = '" + schemaName + "'") + " AND " +
                    PhoenixDatabaseMetaData.TABLE_NAME + " = ?");
            stmt.setByte(1, taskType.getSerializedValue());
            stmt.setTimestamp(2, ts);
            stmt.setString(3, tableName);
        } catch (SQLException e) {
            throw new IOException(e);
        }
        mutateSystemTaskTable(conn, stmt, accessCheckEnabled);
    }

    private static List<TaskRecord> populateTasks(Connection connection, String taskQuery)
            throws SQLException {
        PreparedStatement taskStatement = connection.prepareStatement(taskQuery);
        ResultSet rs = taskStatement.executeQuery();

        List<TaskRecord> result = new ArrayList<>();
        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;
        }

    }
}