/*
 * 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
 *
 *   https://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.accumulo.hadoopImpl.mapreduce;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import org.apache.accumulo.core.client.AccumuloClient;
import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.AccumuloSecurityException;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.MultiTableBatchWriter;
import org.apache.accumulo.core.client.MutationsRejectedException;
import org.apache.accumulo.core.client.TableExistsException;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.security.SecurityErrorCode;
import org.apache.accumulo.core.data.ColumnUpdate;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.security.ColumnVisibility;
import org.apache.accumulo.hadoop.mapreduce.AccumuloOutputFormat;
import org.apache.accumulo.hadoopImpl.mapreduce.lib.OutputConfigurator;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A base class to be used to create {@link RecordWriter} instances that write to Accumulo.
 */
public class AccumuloRecordWriter extends RecordWriter<Text,Mutation> {
  // class to serialize configuration under in the job
  private static final Class<AccumuloOutputFormat> CLASS = AccumuloOutputFormat.class;
  private static final Logger log = LoggerFactory.getLogger(AccumuloRecordWriter.class);
  private MultiTableBatchWriter mtbw = null;
  private final HashMap<Text,BatchWriter> bws;
  private final Text defaultTableName;

  private final boolean simulate;
  private final boolean createTables;

  private long mutCount = 0;
  private long valCount = 0;

  private AccumuloClient client;

  public AccumuloRecordWriter(TaskAttemptContext context) {
    Configuration conf = context.getConfiguration();
    this.simulate = OutputConfigurator.getSimulationMode(CLASS, conf);
    this.createTables = OutputConfigurator.canCreateTables(CLASS, conf);

    if (simulate) {
      log.info("Simulating output only. No writes to tables will occur");
    }

    this.bws = new HashMap<>();

    String tname = OutputConfigurator.getDefaultTableName(CLASS, conf);
    this.defaultTableName = (tname == null) ? null : new Text(tname);

    if (!simulate) {
      this.client = OutputConfigurator.createClient(CLASS, conf);
      mtbw = client.createMultiTableBatchWriter();
    }
  }

  /**
   * Push a mutation into a table. If table is null, the defaultTable will be used. If
   * OutputFormatBuilder#createTables() is set, the table will be created if it does not exist. The
   * table name must only contain alphanumerics and underscore.
   */
  @Override
  public void write(Text table, Mutation mutation) throws IOException {
    if (table == null || table.getLength() == 0) {
      table = this.defaultTableName;
    }

    if (!simulate && table == null) {
      throw new IOException("No table or default table specified. Try simulation mode next time");
    }

    ++mutCount;
    valCount += mutation.size();
    printMutation(table, mutation);

    if (simulate) {
      return;
    }

    if (!bws.containsKey(table)) {
      try {
        addTable(table);
      } catch (AccumuloSecurityException | AccumuloException e) {
        log.error("Could not add table '" + table + "'", e);
        throw new IOException(e);
      }
    }

    try {
      bws.get(table).addMutation(mutation);
    } catch (MutationsRejectedException e) {
      throw new IOException(e);
    }
  }

  public void addTable(Text tableName) throws AccumuloException, AccumuloSecurityException {
    if (simulate) {
      log.info("Simulating adding table: " + tableName);
      return;
    }

    log.debug("Adding table: " + tableName);
    BatchWriter bw = null;
    String table = tableName.toString();

    if (createTables && !client.tableOperations().exists(table)) {
      try {
        client.tableOperations().create(table);
      } catch (AccumuloSecurityException e) {
        log.error("Accumulo security violation creating " + table, e);
        throw e;
      } catch (TableExistsException e) {
        // Shouldn't happen
      }
    }

    try {
      bw = mtbw.getBatchWriter(table);
    } catch (TableNotFoundException e) {
      log.error("Accumulo table " + table + " doesn't exist and cannot be created.", e);
      throw new AccumuloException(e);
    } catch (AccumuloException | AccumuloSecurityException e) {
      throw e;
    }

    if (bw != null) {
      bws.put(tableName, bw);
    }
  }

  private int printMutation(Text table, Mutation m) {
    if (log.isTraceEnabled()) {
      log.trace(String.format("Table %s row key: %s", table, hexDump(m.getRow())));
      for (ColumnUpdate cu : m.getUpdates()) {
        log.trace(String.format("Table %s column: %s:%s", table, hexDump(cu.getColumnFamily()),
            hexDump(cu.getColumnQualifier())));
        log.trace(String.format("Table %s security: %s", table,
            new ColumnVisibility(cu.getColumnVisibility()).toString()));
        log.trace(String.format("Table %s value: %s", table, hexDump(cu.getValue())));
      }
    }
    return m.getUpdates().size();
  }

  private String hexDump(byte[] ba) {
    StringBuilder sb = new StringBuilder();
    for (byte b : ba) {
      if ((b > 0x20) && (b < 0x7e)) {
        sb.append((char) b);
      } else {
        sb.append(String.format("x%02x", b));
      }
    }
    return sb.toString();
  }

  @Override
  public void close(TaskAttemptContext attempt) throws IOException {
    log.debug("mutations written: " + mutCount + ", values written: " + valCount);
    if (simulate) {
      return;
    }

    try {
      mtbw.close();
    } catch (MutationsRejectedException e) {
      if (!e.getSecurityErrorCodes().isEmpty()) {
        HashMap<String,Set<SecurityErrorCode>> tables = new HashMap<>();
        e.getSecurityErrorCodes().forEach((tabletId, codes) -> tables
            .computeIfAbsent(tabletId.getTable().canonical(), k -> new HashSet<>()).addAll(codes));
        log.error("Not authorized to write to tables : " + tables);
      }

      if (!e.getConstraintViolationSummaries().isEmpty()) {
        log.error("Constraint violations : " + e.getConstraintViolationSummaries().size());
      }
      throw new IOException(e);
    } finally {
      client.close();
    }
  }
}
