/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt'
 * which is part of this source code package
 */

package com.scytl.slogger.validator.hmac.encrypted;

import static java.text.MessageFormat.format;

import com.scytl.slogger.crypto.SecureLoggerCryptographyProvider;
import com.scytl.slogger.event.SecureLoggingEvent;
import com.scytl.slogger.event.SecureMessageProperties;
import com.scytl.slogger.event.SecurePatternLayout;
import com.scytl.slogger.validator.EventValidatorException;
import com.scytl.slogger.validator.hmac.HMacEventValidator;

import org.apache.log4j.Layout;

import java.io.BufferedWriter;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Base64;
import java.util.List;
import java.util.Map;

import javax.crypto.SecretKey;

/**
 * Public encrypted HMAC event validator.
 */
public class PublicEncryptedHMacEventValidator extends HMacEventValidator {

  /**
   * Error writing to the file message.
   */
  protected static final String ERROR_WHILE_WRITING_THE_FILE = "Error while writing the file";

  /**
   * The current block.
   */
  protected SecureLogBlock currentBlock = new SecureLogBlock();

  private final BufferedWriter okWriter;

  private final BufferedWriter errorWriter;

  private final BufferedWriter warnWriter;

  /**
   * Constructor.
   *
   * @param okWriter the OK writer
   * @param warnWriter the warning writer
   * @param errorWriter the error writer
   * @param cryptographyProvider the cryptography provider.
   */
  public PublicEncryptedHMacEventValidator(final BufferedWriter okWriter,
      final BufferedWriter warnWriter, final BufferedWriter errorWriter,
      final SecureLoggerCryptographyProvider cryptographyProvider) {
    super(cryptographyProvider);
    this.errorWriter = errorWriter;
    this.okWriter = okWriter;
    this.warnWriter = warnWriter;
  }

  @Override
  public void stopValidation(final SecurePatternLayout layout) throws EventValidatorException {
    try {
      for (SecureLoggingEvent secureEvent : currentBlock.logEvents()) {
        EventValidatorException exception =
            new EventValidatorException("HMac Session Key not found", secureEvent, layout);
        currentBlock.addProcessedLogEvent(secureEvent, exception);
      }

      saveCurrentBlock(layout);
    } catch (IOException e) {
      throw new EventValidatorException(ERROR_WHILE_WRITING_THE_FILE, e);
    }
  }

  @Override
  public void validateEvent(final SecureLoggingEvent event, final SecurePatternLayout layout)
      throws EventValidatorException {
    Map<String, String> properties = event.getSecureMessage().properties();
    if (properties.containsKey(SecureMessageProperties.LIBERATED_SESSION_KEY)) {
      verifyEventBlock(event, layout);
    }

    try {
      if (properties.containsKey(SecureMessageProperties.ENCRYPTED_SESSION_KEY)) {
        saveCurrentBlock(layout);
      }
      currentBlock.loadLogEvent(event);
    } catch (IOException e) {
      throw new EventValidatorException(ERROR_WHILE_WRITING_THE_FILE, e);
    }

  }

  @Override
  protected void initializeHMac(final SecureLoggingEvent event, final Layout layout)
      throws EventValidatorException {}

  /**
   * Saves the current block.
   *
   * @param layout the layout
   * @throws IOException I/O error occurred.
   */
  protected void saveCurrentBlock(final SecurePatternLayout layout) throws IOException {
    currentBlock.saveProcessedBlock(okWriter, errorWriter, warnWriter, layout);
    currentBlock = new SecureLogBlock();
  }

  private Integer findNumberLogLines(SecureLoggingEvent event) throws EventValidatorException {
    Map<String, String> properties = event.getSecureMessage().properties();
    String source = properties.get(SecureMessageProperties.NUMBER_LOG_LINES);
    if (source == null) {
      return null;
    }
    try {
      return Integer.valueOf(source);
    } catch (NumberFormatException e) {
      throw new EventValidatorException(format("Invalid log lines number: {0}", source));
    }
  }

  private Long findTimerLogMilliseconds(SecureLoggingEvent event) throws EventValidatorException {
    Map<String, String> properties = event.getSecureMessage().properties();
    String source = properties.get(SecureMessageProperties.TIMER_LOG_MILLISECONDS);
    if (source == null) {
      return null;
    }
    try {
      return Long.valueOf(source);
    } catch (NumberFormatException e) {
      throw new EventValidatorException(format("Invalid timer log milliseconds: {0}", source));
    }
  }

  private Long findTimestamp(SecureLoggingEvent event) throws EventValidatorException {
    Map<String, String> properties = event.getSecureMessage().properties();
    String source = properties.get(SecureMessageProperties.TIMESTAMP);
    if (source == null) {
      return null;
    }
    try {
      return Long.valueOf(source);
    } catch (NumberFormatException e) {
      throw new EventValidatorException(format("Invalid timestamp: {0}", source));
    }
  }

  private SecretKey getLiberatedSessionKey(SecureLoggingEvent event)
      throws EventValidatorException {
    Map<String, String> properties = event.getSecureMessage().properties();
    String source = properties.get(SecureMessageProperties.LIBERATED_SESSION_KEY);
    byte[] bytes = Base64.getDecoder().decode(source);
    SecretKey key;
    try {
      key = getCryptographyProvider().generateSymmetricKey(bytes);
    } catch (GeneralSecurityException e) {
      throw new EventValidatorException(e);
    }
    return key;
  }

  private long getTimestamp(SecureLoggingEvent event) throws EventValidatorException {
    Long value = findTimestamp(event);
    if (value == null) {
      throw new EventValidatorException("Timestamp is missing");
    }
    return value.longValue();
  }

  private void verifyEventBlock(final SecureLoggingEvent closingCheckpoint,
      final SecurePatternLayout layout) throws EventValidatorException {
    List<SecureLoggingEvent> events = currentBlock.logEvents();
    if (events.isEmpty()) {
      return;
    }

    SecretKey key = getLiberatedSessionKey(closingCheckpoint);
    try {
      loadHMacSecretKey(key);
    } catch (GeneralSecurityException e) {
      throw new EventValidatorException(e);
    }

    SecureLoggingEvent openingCheckpoint = events.get(0);
    Integer numberLogLines = findNumberLogLines(openingCheckpoint);
    Long timerLogMilliseconds = findTimerLogMilliseconds(openingCheckpoint);
    Long openingTimestamp = findTimestamp(openingCheckpoint);

    int count = 0;
    for (SecureLoggingEvent event : events) {
      try {
        super.validateEvent(event, layout);
        if (event != openingCheckpoint && numberLogLines != null && timerLogMilliseconds != null
            && openingTimestamp != null) {
          if (count > numberLogLines) {
            throw new EventValidatorException("Block has too many events", event, layout);
          }
          long timestamp = getTimestamp(event);
          if (timestamp - openingTimestamp > timerLogMilliseconds) {
            throw new EventValidatorException("Block has late events", event, layout);
          }
        }
        currentBlock.addProcessedLogEvent(event);
      } catch (EventValidatorException e) {
        currentBlock.addProcessedLogEvent(event, e);
      }
      count++;
    }
  }
}
