/*
 *  Name:    $Id: EVVValidator.java,v 1.15 2010/03/23 11:22:11 t724w Exp $
 *
 *  Copyright 2009 Bundesamt für Informatik und Telekommunikation
 *
 *  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 ch.admin.bit.edec.evv.ui;

import ch.admin.bit.edec.dsig.Environment;
import ch.admin.bit.edec.dsig.ValidationResult;
import ch.admin.bit.edec.dsig.ValidationResultImpl;
import ch.admin.bit.edec.dsig.builder.X509CRLBuilder;
import ch.admin.bit.edec.dsig.builder.X509CertificateBuilder;
import ch.admin.bit.edec.dsig.context.ProviderSelector;
import ch.admin.bit.edec.dsig.context.ProviderSelectorImpl;
import ch.admin.bit.edec.dsig.context.SystemContext;
import ch.admin.bit.edec.dsig.context.SystemContextImpl;
import ch.admin.bit.edec.dsig.out.ConsoleOutputter;
import ch.admin.bit.edec.dsig.out.FileOutputter;
import ch.admin.bit.edec.dsig.out.MultipleOutputter;
import ch.admin.bit.edec.dsig.out.OutputterStrategy;
import ch.admin.bit.edec.dsig.util.XMLUtil;
import ch.admin.bit.edec.dsig.validator.X509CertificateValidator;
import ch.admin.bit.edec.dsig.validator.XMLSignatureValidator;
import org.apache.commons.cli.*;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import java.io.File;
import java.io.FileNotFoundException;
import java.security.cert.CertificateException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.text.ParseException;

import static java.lang.System.exit;

/**
 * eVV Validator console UI.
 * <p>
 * eVV validator console application. This application is used to validate the
 * xml signature on eVV XML files and display the output.
 *
 * @author t724w (mru)
 */
public class EVVValidator {

  private static final String HELP_FILE_TXT = "/help.txt";

  private List<OutputterStrategy> outputterStrategies = new ArrayList<OutputterStrategy>();
  private static final String SUFIX_XML = ".xml";
  private static boolean showSystemInfo = true;
  private static EVVValidator evvValidator;

  /**
   * Start eVV Validator
   *
   * @param args program parameters
   */
  public static void main(String[] args) {
    evvValidator = new EVVValidator();

    // create the parser
    CommandLineParser parser = new DefaultParser();
    Options options = evvValidator.getCommandLineOptions();

    String inputCA = null;
    String inputCRL = null;
    String inputFile = null;
    String inputDate = null;
    String inputDir = null;
    String outputDir = null;
    boolean move = false;
    boolean showDebugInfos = false;

    try {
      // parse the command line arguments
      CommandLine line = parser.parse(options, args);

      if (null == line.getOptions()) {
        showHelp(options);
        exit(1);
      }

      if (line.hasOption("h")) {
        showHelp(options);
        exit(0);
      }

      inputCA = line.getOptionValue("ca");
      inputCRL = line.getOptionValue("crl");
      inputFile = line.getOptionValue("file");

      if (line.hasOption("move")) {
        move = true;
      }

      if (line.hasOption("debug")) {
        showDebugInfos = true;
      }

      if (line.hasOption("date")) {
        inputDate = line.getOptionValue("date");
      }

      if (line.hasOption("input-dir")) {
        inputDir = line.getOptionValue("input-dir");
      }

      if (line.hasOption("output-dir")) {
        outputDir = line.getOptionValue("output-dir");
      }
    }
    catch (org.apache.commons.cli.ParseException exp) {
      System.err.println("<< Parsing failed.  Reason: " + exp.getMessage() + " >>");
      showHelp(options);
      System.out.println("");
      exit(1);
    }

    // do validation with given input

    if (null != inputDir) {
      evvValidator.validateFromDir(inputDir, inputCA, inputCRL, inputDate, outputDir, move, showDebugInfos);
    }
    else {
      evvValidator.validateFromFile(inputFile, inputCA, inputCRL, inputDate, outputDir, showDebugInfos);
    }

  }

  /**
   * Validate all xml files in a specified directory
   *
   * @param inputDir
   * @param inputCA
   * @param inputCRL
   * @param inputDate
   */
  public void validateFromDir(String inputDir, String inputCA, String inputCRL, String inputDate, String outputDir,
      boolean move, boolean showDebugInfos) {

    File file = new File(inputDir);
    File error = new File(file, "error");
    File ok = new File(file, "ok");

    if (move) {
      ok.mkdir();
      error.mkdir();
    }

    for (File current : file.listFiles()) {
      if (current.getName().toLowerCase().endsWith(SUFIX_XML)) {
        ValidationResult result = validateFromFile(current.getAbsolutePath(), inputCA, inputCRL, inputDate, outputDir, showDebugInfos);

        if (move) {
          move(error, ok, current, result);
        }

      }
    }
  }

  private void move(File error, File ok, File current, ValidationResult result) {
    if (result.isValid()) {
      current.renameTo(new File(ok, current.getName()));
    }
    else {
      current.renameTo(new File(error, current.getName()));
    }
  }

  /**
   * @param inputFile
   * @param inputCA
   * @param inputCRL
   * @param inputDate
   * @param outputDir
   * @throws FileNotFoundException
   */
  public ValidationResult validateFromFile(String inputFile, String inputCA, String inputCRL, String inputDate,
      String outputDir, boolean showDebugInfos) {

    ValidationResultImpl result = new ValidationResultImpl();

    // create system context and output strategy
    SystemContext context = createSystemContext(outputDir, inputFile);
    OutputterStrategy out = context.getOutputterStrategy();

    try {

      out.write("### --- START --- ####");

      if (true == showDebugInfos) {
        printDebugInfo(out);
      }

      out.write("input eVV file: " + inputFile);
      out.write("input CRL file: " + inputCRL);
      out.write("input CA file: " + inputCA);
      out.write("input check date: " + inputDate);
      out.write("output dir: " + outputDir);

      // create xml document from evv file
      Document xmldoc = XMLUtil.read(inputFile);
      // stream is no longer needed

      // extract check date
      Node timestamp = XMLUtil.extractTimeStamp(xmldoc);
      // display evv timestamp. This is the wsu:Timestamp/wsu:Created
      // Element in the soap security header

      if (timestamp == null) {
        throw new CertificateException("Timestamp not found: this is not a valid eVV");
      }

      out.write("timestamp: " + timestamp.getTextContent());

      // get the date on which the certifcate must be valid
      Date checkDate = getCheckDate(inputDate, timestamp);

      // display eVV document info
      Node evvInfo = XMLUtil.extractDocumentInformation(xmldoc);
      if (evvInfo != null) {
        out.write(evvInfo.getChildNodes());
      }
      else {
        // signed soap fault
        out.write("documentInformation: NO CONTENT");
      }

      // display traderdeclaration number
      out.write(XMLUtil.extractTraderDeclarationNumber(xmldoc));

      // display trader reference
      out.write(XMLUtil.extractTraderReference(xmldoc));

      // display out tin's
      out.write((XMLUtil.extractDeclaratTIN(xmldoc)));

      // extract x509 token from evv file
      Node x509token = XMLUtil.extractX509Token(xmldoc);

      if (x509token == null) {
        throw new CertificateException("X509 Certificate not found: this is not a valid eVV");
      }

      X509CertificateBuilder builder = new X509CertificateBuilder(context);

      // create certificate from x509 token
      X509Certificate x509 = builder.createCertificate(x509token.getTextContent());
      out.write("certificate", x509);

      // create ca certificate from file
      X509Certificate ca = builder.createCertificateFromPath(inputCA);
      out.write("CA", ca);

      // x509 validator which trust a given ca
      X509CertificateValidator x509validator = new X509CertificateValidator(context, ca);
      // set date
      x509validator.setDate(checkDate);

      // read in crl when specified
      if (inputCRL != null && inputCRL.length() != 0) {
        X509CRLBuilder crlBuilder = new X509CRLBuilder(context);
        X509CRL crl = crlBuilder.createCRLFromPath(inputCRL);
        out.write("CRL", crl);
        // set CRL
        x509validator.setX509crl(crl);

      }
      else {
        out.write("CRL: NOT SET");
      }

      out.write("check certificate date: " + checkDate);
      // validate cert path with ca
      boolean certValid = x509validator.isValid(x509);

      String statusCert = certValid ? "OK" : "NOT OK";
      out.write("check certificate: " + statusCert);

      // extract siganture element from xml document
      Node dsSignature = XMLUtil.extractDSignature(xmldoc);

      // validate signature with given public key
      XMLSignatureValidator sigValidator = new XMLSignatureValidator(context, x509.getPublicKey());
      boolean sigValid = sigValidator.isValid(dsSignature);

      String statusSign = sigValid ? "OK" : "NOT OK";
      out.write("check signature: " + statusSign);

      if (certValid && sigValid) {
        result.setValid(true);
      }
      else {
        result.setValid(false);
        result.setMessage("certificate: " + statusCert + ", signature: " + statusSign);
      }

    }
    catch (Throwable e) {
      out.write("ERROR", e);
      result.setValid(false);
      result.setException(e);
      result.setMessage(e.getMessage());
    }
    finally {
      out.write("### --- END --- ###");

      out.close();

    }

    return result;

  }

  private void printDebugInfo(OutputterStrategy out) {
    if (showSystemInfo) {
      out.write("system jre version: " + System.getProperty("java.version"));
      out.write("system jre vendor: " + System.getProperty("java.vendor"));
      out.write("system jre home: " + System.getProperty("java.home"));
      out.write("system os: " + System.getProperty("os.name"));
      out.write("system os version: " + System.getProperty("os.version"));
      out.write("system java endorsed dirs: " + System.getProperty("java.endorsed.dirs"));

      out.write("system java user dir: " + System.getProperty("user.dir"));

      out.write("system user home: " + System.getProperty("user.home"));

    }
  }

  public void addOutputterStrategy(OutputterStrategy strategy) {
    outputterStrategies.add(strategy);
  }

  /**
   * Get the checkDate
   *
   * @param inputDate
   * @param timestamp
   * @return
   * @throws ParseException
   */
  private Date getCheckDate(String inputDate, Node timestamp) throws ParseException {

    if (inputDate == null) {
      // get date from timestamp when no input date was specified
      return getDateFromTimestampNode(timestamp);
    }
    else {
      // get date from input string
      return getDateFromString(inputDate);
    }

  }

  /**
   * Parse the date from a given string. The String must have the fromat
   * yyyy.MM.dd-HH:mm:ss or yyyy.MM.dd
   *
   * @param str a date as string
   * @return a date
   * @throws ParseException
   */
  private Date getDateFromString(String str) throws ParseException {

    // return current date when input string was null
    if (str == null) {

      return new Date();
    }

    SimpleDateFormat sdf = null;

    // check format
    if (str.matches(".*-.*")) {
      // format with time
      sdf = new SimpleDateFormat("yyyy.MM.dd-HH:mm:ss");
    }
    else {
      // format without time
      sdf = new SimpleDateFormat("yyyy.MM.dd");
    }

    return sdf.parse(str);

  }

  /**
   * Get the date from the wsu:Timestamp/wsu:Created element
   *
   * @param timestamp
   * @return
   * @throws ParseException
   */
  private Date getDateFromTimestampNode(Node timestamp) throws ParseException {

    // return current date when input string was null
    if (timestamp == null) {
      return new Date();
    }

    Date date = null;
    // format with miliseconds
    try {
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S'Z'");
      date = sdf.parse(timestamp.getTextContent());

      // fallback strategy --> format without miliseconds
    }
    catch (ParseException pex) {
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
      date = sdf.parse(timestamp.getTextContent());
    }

    return date;

  }

  /**
   * Create the system context
   *
   * @param outDir
   * @param inputFile
   * @return
   */
  private SystemContext createSystemContext(String outDir, String inputFile) {
    SystemContextImpl context = new SystemContextImpl();

    MultipleOutputter outputter = new MultipleOutputter();
    outputter.addOutputter(new ConsoleOutputter());

    for (OutputterStrategy outputterStrategy : outputterStrategies) {
      outputter.addOutputter(outputterStrategy);
    }

    context.setOutputterStrategy(outputter);

    // add fileoutputter when out dir was specified
    if (outDir != null) {

      File logDir = new File(outDir);

      if (!logDir.exists()) {
        logDir.mkdir();
      }

      File file = new File(inputFile);
      String name = file.getName();
      // out file name is the name of the eVV File with the suffix txt.
      File outFile = new File(outDir + File.separator + name + ".txt");

      FileOutputter fileOutputter;
      try {
        fileOutputter = new FileOutputter(outFile);
        outputter.addOutputter(fileOutputter);
      }
      catch (FileNotFoundException e) {
        // write error to system.out
        outputter.write("Output dir not found", e);
      }
    }

    // initialize security provider
    ProviderSelector selector = new ProviderSelectorImpl();
    context.setProviderSelector(selector);

    return context;

  }

  private static void showHelp(Options options) {
    System.out.println("e-dec eVV Validator");
    System.out.println("===================");
    System.out.println("");

    // automatically generate the help statement
    HelpFormatter formatter = new HelpFormatter();
    formatter.printHelp("EVVValidator", options);
  }

  private Options getCommandLineOptions() {
    Option optionHelp = new Option("h", "help", false,"print this message");
    Option optionMove = new Option("m", "move", false,"Move");
    Option optionDebug = new Option("d", "debug", false,"Show debug infos");

    Option optionCA = Option.builder().longOpt("ca").required(true).hasArg(true).argName("FILENAME").build();
    Option optionCRL = Option.builder().longOpt("crl").required(true).hasArg(true).argName("FILENAME").build();
    Option optionFile = Option.builder().longOpt("file").required(true).hasArg(true).argName("FILENAME").build();
    Option optionDate = Option.builder().longOpt("date").required(false).hasArg(true).argName("DATE").build();
    Option optionDir = Option.builder().longOpt("input-dir").required(false).hasArg(true).argName("DIR").build();
    Option optionOut = Option.builder().longOpt("output-dir").required(false).hasArg(true).argName("DIR").build();
    //Option optionCRL = Option.builder("crl").required(true).longOpt("CRL file (certificate revocation list)").build();

    Options options = new Options();
    options.addOption(optionCA);
    options.addOption(optionCRL);
    options.addOption(optionDate);
    options.addOption(optionDir);
    options.addOption(optionFile);
    options.addOption(optionHelp);
    options.addOption(optionMove);
    options.addOption(optionOut);
    options.addOption(optionDebug);

    return options;
  }

}
