/*
 * 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.util;

import static java.text.MessageFormat.format;

import com.scytl.slogger.SecureLoggerException;
import com.scytl.slogger.crypto.SecureLoggerCryptographyFactory;
import com.scytl.slogger.crypto.SecureLoggerCryptographyProvider;
import com.scytl.slogger.policy.LogFileNamePolicy;
import com.scytl.slogger.policy.SequentialFileNamePolicy;
import com.scytl.slogger.validator.EventValidatorException;
import com.scytl.slogger.validator.LogFilePatternReceiver;
import com.scytl.slogger.validator.ValidatorHelper;
import com.scytl.slogger.validator.hmac.encrypted.signed.PublicValidatorFactory;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.Iterator;

public final class LogValidator {

  private static final String VERIFIER_PATTERN = "v";

  private static final String PATTERN = "p";

  private static final String LOG_FILE = "l";

  private static final String ENCODING = "e";

  private static final String CERTIFICATE = "c";

  private static final String OUTPUT_FOLDER = "o";

  private static final String LOG_FILE_NAME_POLICY_CLASS = "n";

  private static final String CRYPTOGRAPHY_PROVIDER_CLASS = "m";

  public static final String OK_FILE = "ok.log";

  public static final String WARN_FILE = "warn.log";

  public static final String ERROR_FILE = "error.log";


  private LogValidator() {}

  /**
   * Returns the log files.
   *
   * @param args the arguments
   * @return the log files
   * @throws SecureLoggerException failed to get the log files.
   */
  public static Iterator<File> getLogFiles(final String[] args) throws SecureLoggerException {
    // create Options object
    Options options = getLogFileOptions();
    CommandLineParser clp = new PosixParser();
    CommandLine cmd = null;
    try {
      cmd = clp.parse(options, args);
    } catch (ParseException e) {
      // automatically generate the help statement
      HelpFormatter formatter = new HelpFormatter();
      formatter.printHelp("LogValidator", options);
      throw new SecureLoggerException(e);
    }
    LogFileNamePolicy policy;
    String logFile = cmd.getOptionValue(LOG_FILE);
    if (cmd.hasOption(LOG_FILE_NAME_POLICY_CLASS)) {
      policy = verifyLogFileNamePolicy(cmd.getOptionValue(LOG_FILE_NAME_POLICY_CLASS), logFile);
    } else {
      policy = new SequentialFileNamePolicy(new File(logFile));
    }

    return policy.logFiles();
  }

  /**
   * The main method.
   *
   * @param args the command line arguments.
   */
  public static void main(final String[] args) {

    // create Options object
    Options options = mainOptions();

    try {
      CommandLineParser clp = new PosixParser();
      CommandLine cmd = clp.parse(options, args);
      
      if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
        Security.addProvider(new BouncyCastleProvider());
      }

      // Check if we have a plugin or use the default implementation
      SecureLoggerCryptographyProvider provider;
      if (cmd.hasOption(CRYPTOGRAPHY_PROVIDER_CLASS)) {
        provider = new SecureLoggerCryptographyFactory()
            .build(cmd.getOptionValue(CRYPTOGRAPHY_PROVIDER_CLASS));
      } else {
        provider = new SecureLoggerCryptographyFactory().build();
      }

      PublicKey publicKey =
          readPublicKeyFromCertificateFile(cmd.getOptionValue(CERTIFICATE), provider);
      String outputFolder = cmd.getOptionValue(OUTPUT_FOLDER);
      verifyOutputFolder(outputFolder);

      LogFileNamePolicy policy;
      String logFile = cmd.getOptionValue(LOG_FILE);
      if (cmd.hasOption(LOG_FILE_NAME_POLICY_CLASS)) {
        policy = verifyLogFileNamePolicy(cmd.getOptionValue(LOG_FILE_NAME_POLICY_CLASS), logFile);
      } else {
        policy = new SequentialFileNamePolicy(new File(logFile));
      }

      String okFile = new File(outputFolder, OK_FILE).getAbsolutePath();
      String warnFile = new File(outputFolder, WARN_FILE).getAbsolutePath();
      String errorFile = new File(outputFolder, ERROR_FILE).getAbsolutePath();

      try (BufferedWriter okWriter = new BufferedWriter(new FileWriter(okFile));
          BufferedWriter warnWriter = new BufferedWriter(new FileWriter(warnFile));
          BufferedWriter errorWriter = new BufferedWriter(new FileWriter(errorFile));) {
        validateLog(cmd, publicKey, logFile, okWriter, warnWriter, errorWriter, policy, provider);
      }

      System.err.println("The log has been verified. See output files: ");
      System.err.println("    Secure traces: " + okFile);
      System.err.println("    Error traces: " + errorFile);
      System.err.println("    Warning traces: " + warnFile);
    } catch (ParseException e) {
      // automatically generate the help statement
      HelpFormatter formatter = new HelpFormatter();
      formatter.printHelp("LogValidator", options);
    } catch (GeneralSecurityException | EventValidatorException | IOException e) {
      throw new IllegalStateException("Failed to validate the log.", e);
    }
  }

  private static Options getLogFileOptions() {
    Options options = new Options();

    Option logFile = new Option(LOG_FILE, "file", true, "Log4j File");
    logFile.setRequired(true);
    options.addOption(logFile);

    Option logFileNamePolicyClass =
        new Option(LOG_FILE_NAME_POLICY_CLASS, "policy", true, "Log File Name Policy Class");
    options.addOption(logFileNamePolicyClass);

    return options;
  }

  private static Options mainOptions() {
    Options options = new Options();

    Option logFile = new Option(LOG_FILE, "file", true, "Log4j File");
    logFile.setRequired(true);
    options.addOption(logFile);

    Option encoding = new Option(ENCODING, "encoding", true, "Log Encoding");
    options.addOption(encoding);

    Option logPattern = new Option(PATTERN, "pattern", true, "Log Pattern");
    logPattern.setRequired(true);
    options.addOption(logPattern);

    Option verifierPattern = new Option(VERIFIER_PATTERN, "verifier", true, "Verifier Pattern");
    verifierPattern.setRequired(true);
    options.addOption(verifierPattern);

    Option certificate = new Option(CERTIFICATE, "cert", true, "Certificate File");
    certificate.setRequired(true);
    options.addOption(certificate);

    Option outputFolder = new Option(OUTPUT_FOLDER, "output", true, "Output Folder");
    outputFolder.setRequired(true);
    options.addOption(outputFolder);

    Option logFileNamePolicyClass =
        new Option(LOG_FILE_NAME_POLICY_CLASS, "policy", true, "Log File Name Policy Class");
    options.addOption(logFileNamePolicyClass);

    Option cryptographyProviderClass =
        new Option(CRYPTOGRAPHY_PROVIDER_CLASS, "provider", true, "Cryptography Provider Class");
    options.addOption(cryptographyProviderClass);

    return options;
  }

  private static PublicKey readPublicKeyFromCertificateFile(final String certificateValue,
      final SecureLoggerCryptographyProvider provider)
      throws GeneralSecurityException, FileNotFoundException, IOException {
    Certificate certificate;
    try (InputStream stream = new FileInputStream(certificateValue)) {
      certificate = provider.readCertificate(stream);
    }
    return certificate.getPublicKey();
  }

  private static void validateLog(final CommandLine cmd, final PublicKey publicKey,
      final String logFile, final BufferedWriter okWriter, final BufferedWriter warnWriter,
      final BufferedWriter errorWriter, final LogFileNamePolicy policy,
      final SecureLoggerCryptographyProvider provider) throws EventValidatorException {
    PublicValidatorFactory factory =
        new PublicValidatorFactory(publicKey, okWriter, warnWriter, errorWriter, provider);
    ValidatorHelper.validateLog(policy, cmd.getOptionValue(PATTERN),
        cmd.getOptionValue(VERIFIER_PATTERN), factory.createValidator(),
        cmd.getOptionValue(ENCODING), LogFilePatternReceiver.DEFAULT_TIMESTAMP_FORMAT);
  }

  private static LogFileNamePolicy verifyLogFileNamePolicy(final String policyClass,
      final String logFile) {
    File file = new File(logFile);
    try {
      Class<?> logFileNamePolicyClass = Class.forName(policyClass);
      Constructor<?> constructor = logFileNamePolicyClass.getConstructor(File.class);
      return (LogFileNamePolicy) constructor.newInstance(file);
    } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException
        | IllegalAccessException | InvocationTargetException e) {
      throw new IllegalArgumentException(format("Invalid policy class ''{0}''.", policyClass), e);
    }
  }

  private static void verifyOutputFolder(final String outputFolder) {
    File folder = new File(outputFolder);
    if (!folder.exists() || !folder.isDirectory()) {
      throw new IllegalArgumentException(format("Invalid output folder ''{0}''.", outputFolder));
    }
  }
}
