/*
 *
 * Copyright (c) 2013 - 2020 Lijun Liao
 *
 * 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 org.xipki.ca.api.mgmt;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.util.Args;
import org.xipki.util.Base64;
import org.xipki.util.CollectionUtil;
import org.xipki.util.ConfPairs;
import org.xipki.util.FileOrBinary;
import org.xipki.util.FileOrValue;
import org.xipki.util.InvalidConfException;
import org.xipki.util.IoUtil;
import org.xipki.util.StringUtil;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

/**
 * Helper class to convert the CA configuration.
 *
 * @author Lijun Liao
 * @since 2.1.0
 */

public class CaConfs {
  private static final Logger LOG = LoggerFactory.getLogger(CaConfs.class);

  private static final String APP_DIR = "APP_DIR";

  private CaConfs() {
  }

  public static void marshal(CaConfType.CaSystem root, OutputStream out)
      throws InvalidConfException, IOException {
    Args.notNull(root, "root");
    Args.notNull(out, "out");
    root.validate();
    JSON.writeJSONString(out, Charset.forName("UTF8"), root, SerializerFeature.PrettyFormat);
  } // method marshal

  public static InputStream convertFileConfToZip(String confFilename)
      throws IOException, InvalidConfException {
    Args.notNull(confFilename, "confFilename");

    ByteArrayOutputStream bytesStream = new ByteArrayOutputStream(1048576); // initial 1M
    ZipOutputStream zipStream = new ZipOutputStream(bytesStream);
    zipStream.setLevel(Deflater.BEST_SPEED);

    File confFile = new File(confFilename);
    confFile = IoUtil.expandFilepath(confFile);

    InputStream caConfStream = null;
    String baseDir = null;

    try {
      caConfStream = Files.newInputStream(confFile.toPath());
      CaConfType.CaSystem root = JSON.parseObject(caConfStream, CaConfType.CaSystem.class);

      baseDir = root.getBasedir();
      if (StringUtil.isBlank(baseDir)) {
        File confFileParent = confFile.getParentFile();
        baseDir = (confFileParent == null) ? "." : confFileParent.getPath();
      } else if (APP_DIR.equalsIgnoreCase(baseDir)) {
        baseDir = ".";
      }
      // clear the baseDir in ZIP file
      root.setBasedir(null);

      final Map<String, String> properties = new HashMap<>();

      if (root.getProperties() != null) {
        properties.putAll(root.getProperties());
      }

      // Signers
      if (root.getSigners() != null) {
        for (CaConfType.Signer m : root.getSigners()) {
          String name = m.getName();

          if (m.getConf() != null) {
            String conf = convertSignerConf(m.getConf(), properties, baseDir);
            if (conf.length() > 200) {
              String zipEntryName = "files/signer-" + name + ".conf";
              createFileOrValue(zipStream, conf, zipEntryName);
              m.getConf().setFile(zipEntryName);
              m.getConf().setValue(null);
            } else {
              m.getConf().setFile(null);
              m.getConf().setValue(conf);
            }
          }

          if (m.getCert() != null && m.getCert().getFile() != null) {
            String zipEntryName = "files/signer-" + name + ".crt";
            byte[] value = getBinary(m.getConf().getFile(), properties, baseDir);
            createFileOrBinary(zipStream, value, zipEntryName);
            m.getCert().setFile(zipEntryName);
          }
        }
      }

      // Requestors
      if (root.getRequestors() != null) {
        for (CaConfType.Requestor m : root.getRequestors()) {
          String name = m.getName();

          if (m.getConf() != null && m.getConf().getFile() != null) {
            String zipEntryName = "files/requestor-" + name + ".conf";
            String value = getValue(m.getConf().getFile(), properties, baseDir);
            createFileOrValue(zipStream, value, zipEntryName);
            m.getConf().setFile(zipEntryName);
          }

          if (m.getBinaryConf() != null && m.getBinaryConf().getFile() != null) {
            String zipEntryName = "files/requestor-" + name + ".bin";
            byte[] value = getBinary(m.getBinaryConf().getFile(), properties, baseDir);
            createFileOrBinary(zipStream, value, zipEntryName);
            m.getBinaryConf().setFile(zipEntryName);
          }
        }
      }

      // Publishers
      if (root.getPublishers() != null) {
        for (CaConfType.NameTypeConf m : root.getPublishers()) {
          if (m.getConf() != null && m.getConf().getFile() != null) {
            String name = m.getName();
            String zipEntryName = "files/publisher-" + name + ".conf";
            String value = getValue(m.getConf().getFile(), properties, baseDir);
            createFileOrValue(zipStream, value, zipEntryName);
            m.getConf().setFile(zipEntryName);
          }
        }
      }

      // Profiles
      if (root.getProfiles() != null) {
        for (CaConfType.NameTypeConf m : root.getProfiles()) {
          if (m.getConf() != null && m.getConf().getFile() != null) {
            String name = m.getName();
            String zipEntryName = "files/certprofile-" + name + ".conf";
            String value = getValue(m.getConf().getFile(), properties, baseDir);
            createFileOrValue(zipStream, value, zipEntryName);
            m.getConf().setFile(zipEntryName);
          }
        }
      }

      // CAs
      if (root.getCas() != null) {
        for (CaConfType.Ca m : root.getCas()) {
          if (m.getCaInfo() != null) {
            String name = m.getName();
            CaConfType.CaInfo ci = m.getCaInfo();

            // SignerInfo
            if (ci.getSignerConf() != null) {
              FileOrValue fv = ci.getSignerConf();
              String conf = convertSignerConf(fv, properties, baseDir);
              if (conf.length() > 200) {
                String zipEntryName = "files/ca-" + name + "-signer.conf";
                createFileOrValue(zipStream, conf, zipEntryName);
                fv.setFile(zipEntryName);
                fv.setValue(null);
              } else {
                fv.setFile(null);
                fv.setValue(conf);
              }
            }

            // DHPoc Control
            if (ci.getDhpocControl() != null) {
              FileOrValue fv = ci.getDhpocControl();
              String conf = convertSignerConf(fv, properties, baseDir);
              if (conf.length() > 200) {
                String zipEntryName = "files/ca-" + name + "-dhpoc.conf";
                createFileOrValue(zipStream, conf, zipEntryName);
                fv.setFile(zipEntryName);
                fv.setValue(null);
              } else {
                fv.setFile(null);
                fv.setValue(conf);
              }
            }

            // Cert and Certchain
            if (ci.getGenSelfIssued() == null) {
              if (ci.getCert() != null && ci.getCert().getFile() != null) {
                String zipEntryName = "files/ca-" + name + ".crt";
                byte[] value = getBinary(ci.getCert().getFile(), properties, baseDir);
                createFileOrBinary(zipStream, value, zipEntryName);
                ci.getCert().setFile(zipEntryName);
              }

              if (CollectionUtil.isNotEmpty(ci.getCertchain())) {
                for (int i = 0; i < ci.getCertchain().size(); i++) {
                  FileOrBinary fi = ci.getCertchain().get(i);
                  if (fi.getFile() != null) {
                    String zipEntryName = "files/cacertchain-" + name + "-" + i + ".crt";
                    byte[] value = getBinary(fi.getFile(), properties, baseDir);
                    createFileOrBinary(zipStream, value, zipEntryName);
                    fi.setFile(zipEntryName);
                  }
                }
              }
            } else {
              if (ci.getCert() != null) {
                throw new InvalidConfException("cert of CA " + name + " may not be set");
              }

              FileOrBinary csrFb = ci.getGenSelfIssued().getCsr();
              if (csrFb != null && csrFb.getFile() != null) {
                String zipEntryName = "files/ca-" + name + "-csr.p10";
                byte[] value = getBinary(csrFb.getFile(), properties, baseDir);
                createFileOrBinary(zipStream, value, zipEntryName);
                csrFb.setFile(zipEntryName);
              }
            }
          }
        }
      }

      // add the CAConf XML file
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      try {
        marshal(root, bout);
      } finally {
        bout.flush();
      }

      zipStream.putNextEntry(new ZipEntry("caconf.json"));
      try {
        zipStream.write(bout.toByteArray());
      } finally {
        zipStream.closeEntry();
      }
    } finally {
      if (caConfStream != null) {
        try {
          caConfStream.close();
        } catch (IOException ex) {
          LOG.info("could not clonse caConfStream", ex.getMessage());
        }
      }

      zipStream.close();
      bytesStream.flush();
    }

    return new ByteArrayInputStream(bytesStream.toByteArray());
  } // method convertFileConfToZip

  private static void createFileOrValue(ZipOutputStream zipStream, String content, String fileName)
      throws IOException {
    ZipEntry certZipEntry = new ZipEntry(fileName);
    zipStream.putNextEntry(certZipEntry);
    try {
      zipStream.write(StringUtil.toUtf8Bytes(content));
    } finally {
      zipStream.closeEntry();
    }
  } // method createFileOrValue

  private static void createFileOrBinary(ZipOutputStream zipStream, byte[] content, String fileName)
      throws IOException {
    ZipEntry certZipEntry = new ZipEntry(fileName);
    zipStream.putNextEntry(certZipEntry);
    try {
      zipStream.write(content);
    } finally {
      zipStream.closeEntry();
    }
  } // method createFileOrBinary

  private static String getValue(String fileName, Map<String, String> properties, String baseDir)
      throws IOException {
    byte[] binary = getBinary(fileName, properties, baseDir);
    return new String(binary, "UTF-8");
  } // method getValue

  private static byte[] getBinary(String fileName, Map<String, String> properties, String baseDir)
      throws IOException {
    fileName = expandConf(fileName, properties);

    InputStream is = Files.newInputStream(Paths.get(resolveFilePath(fileName, baseDir)));

    return IoUtil.read(is);
  } // method getBinary

  private static String expandConf(String confStr, Map<String, String> properties) {
    if (confStr == null || !confStr.contains("${") || confStr.indexOf('}') == -1) {
      return confStr;
    }

    for (String name : properties.keySet()) {
      String placeHolder = "${" + name + "}";
      while (confStr.contains(placeHolder)) {
        confStr = confStr.replace(placeHolder, properties.get(name));
      }
    }

    return confStr;
  } // method expandConf

  private static String resolveFilePath(String filePath, String baseDir) {
    File file = new File(filePath);
    return file.isAbsolute() ? filePath : new File(baseDir, filePath).getPath();
  } // method resolveFilePath

  private static String convertSignerConf(FileOrValue confFv, Map<String, String> properties,
      String baseDir) throws IOException {

    String conf;
    if (confFv.getValue() != null) {
      conf = confFv.getValue();
    } else {
      conf = getValue(confFv.getFile(), properties, baseDir);
    }

    conf = expandConf(conf, properties);
    if (!conf.contains("file:")) {
      return conf;
    }

    ConfPairs confPairs = new ConfPairs(conf);
    boolean changed = false;

    for (String name : confPairs.names()) {
      String value = confPairs.value(name);
      if (!value.startsWith("file:")) {
        continue;
      }

      changed = true;
      String fileName = value.substring("file:".length());
      byte[] binValue = getBinary(fileName, properties, baseDir);
      confPairs.putPair(name, "base64:" + Base64.encodeToString(binValue));
    }

    return changed ? confPairs.getEncoded() : conf;
  } // method convertSignerConf

}