/**
 * Copyright © 2017 Jeremy Custenborder ([email protected])
 *
 * 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.github.jcustenborder.kafka.connect.cdc.mssql;

import com.github.jcustenborder.kafka.connect.cdc.Change;
import com.github.jcustenborder.kafka.connect.cdc.TableMetadataProvider;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.connect.data.Date;
import org.apache.kafka.connect.data.Schema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

class MsSqlChange implements Change {
  private static final Logger log = LoggerFactory.getLogger(MsSqlChange.class);
  Map<String, String> metadata;
  Map<String, Object> sourcePartition;
  Map<String, Object> sourceOffset;
  String databaseName;
  String schemaName;
  String tableName;
  long timestamp;
  ChangeType changeType;
  List<ColumnValue> keyColumns;
  List<ColumnValue> valueColumns;

  public static Builder builder() {
    return new Builder();
  }

  public static Map<String, Object> offset(long changeVersion) {
    return ImmutableMap.of(
        "sys_change_version", (Object) changeVersion
    );
  }

  public static Long offset(Map<String, Object> sourceOffset) {
    Preconditions.checkNotNull(sourceOffset, "sourceOffset cannot be null.");
    Long changeVersion = (Long) sourceOffset.get("sys_change_version");
    Preconditions.checkNotNull(changeVersion, "sourceOffset[\"sys_change_version\"] cannot be null.");
    return changeVersion;
  }

  @Override
  public Map<String, String> metadata() {
    return this.metadata;
  }

  @Override
  public Map<String, Object> sourcePartition() {
    return this.sourcePartition;
  }

  @Override
  public Map<String, Object> sourceOffset() {
    return this.sourceOffset;
  }

  @Override
  public String databaseName() {
    return this.databaseName;
  }

  @Override
  public String schemaName() {
    return this.schemaName;
  }

  @Override
  public String tableName() {
    return this.tableName;
  }

  @Override
  public List<ColumnValue> keyColumns() {
    return this.keyColumns;
  }

  @Override
  public List<ColumnValue> valueColumns() {
    return this.valueColumns;
  }

  @Override
  public ChangeType changeType() {
    return this.changeType;
  }

  @Override
  public long timestamp() {
    return this.timestamp;
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(MsSqlChange.class)
        .omitNullValues()
        .add("databaseName", this.databaseName)
        .add("schemaName", this.schemaName)
        .add("tableName", this.tableName)
        .add("changeType", this.changeType)
        .add("timestamp", this.timestamp)
        .add("metadata", this.metadata)
        .add("sourcePartition", this.sourcePartition)
        .add("sourceOffset", this.sourceOffset)
        .add("keyColumns", this.keyColumns)
        .add("valueColumns", this.valueColumns)
        .toString();
  }

  static class MsSqlColumnValue implements ColumnValue {

    final String columnName;
    final Schema schema;
    final Object value;

    MsSqlColumnValue(String columnName, Schema schema, Object value) {
      this.columnName = columnName;
      this.schema = schema;
      this.value = value;
    }


    @Override
    public String columnName() {
      return this.columnName;
    }

    @Override
    public Schema schema() {
      return this.schema;
    }

    @Override
    public Object value() {
      return this.value;
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(MsSqlColumnValue.class)
          .omitNullValues()
          .add("columnName", this.columnName)
          .add("schema", this.schema)
          .add("value", this.value)
          .toString();
    }
  }

  static class Builder {
    static Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

    public MsSqlChange build(TableMetadataProvider.TableMetadata tableMetadata, ResultSet resultSet, Time time) throws SQLException {
      MsSqlChange change = new MsSqlChange();
      change.timestamp = time.milliseconds();
      change.databaseName = tableMetadata.databaseName();
      change.schemaName = tableMetadata.schemaName();
      change.tableName = tableMetadata.tableName();

      final long sysChangeVersion = resultSet.getLong("__metadata_sys_change_version");
      final long sysChangeCreationVersion = resultSet.getLong("__metadata_sys_change_creation_version");
      final String changeOperation = resultSet.getString("__metadata_sys_change_operation");

      change.metadata = ImmutableMap.of(
          "sys_change_operation", changeOperation,
          "sys_change_creation_version", String.valueOf(sysChangeCreationVersion),
          "sys_change_version", String.valueOf(sysChangeVersion)
      );

      switch (changeOperation) {
        case "I":
          change.changeType = ChangeType.INSERT;
          break;
        case "U":
          change.changeType = ChangeType.UPDATE;
          break;
        case "D":
          change.changeType = ChangeType.DELETE;
          break;
        default:
          throw new UnsupportedOperationException(
              String.format("Unsupported sys_change_operation of '%s'", changeOperation)
          );
      }
      log.trace("build() - changeType = {}", change.changeType);

      change.keyColumns = new ArrayList<>(tableMetadata.keyColumns().size());
      change.valueColumns = new ArrayList<>(tableMetadata.columnSchemas().size());

      for (Map.Entry<String, Schema> kvp : tableMetadata.columnSchemas().entrySet()) {
        String columnName = kvp.getKey();
        Schema schema = kvp.getValue();
        Object value;
        if (Schema.Type.INT8 == schema.type()) {
          // Really lame Microsoft. A tiny int is stored as a single byte with a value of 0-255.
          // Explain how this should be returned as a short?
          value = resultSet.getByte(columnName);
        } else if (Schema.Type.INT32 == schema.type() &&
            Date.LOGICAL_NAME.equals(schema.name())) {
          value = new java.util.Date(
              resultSet.getDate(columnName, calendar).getTime()
          );
        } else if (Schema.Type.INT32 == schema.type() &&
            org.apache.kafka.connect.data.Time.LOGICAL_NAME.equals(schema.name())) {
          value = new java.util.Date(
              resultSet.getTime(columnName, calendar).getTime()
          );
        } else {
          value = resultSet.getObject(columnName);
        }

        log.trace("build() - columnName = '{}' value = '{}'", columnName, value);
        MsSqlColumnValue columnValue = new MsSqlColumnValue(columnName, schema, value);
        change.valueColumns.add(columnValue);
        if (tableMetadata.keyColumns().contains(columnName)) {
          change.keyColumns.add(columnValue);
        }
      }

      return change;
    }
  }
}