/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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.apache.geode.management.internal.cli.commands;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;

import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;

import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.management.cli.CliMetaData;
import org.apache.geode.management.cli.GfshCommand;
import org.apache.geode.management.internal.cli.domain.StackTracesPerMember;
import org.apache.geode.management.internal.cli.functions.GetStackTracesFunction;
import org.apache.geode.management.internal.cli.result.model.InfoResultModel;
import org.apache.geode.management.internal.cli.result.model.ResultModel;
import org.apache.geode.management.internal.i18n.CliStrings;
import org.apache.geode.management.internal.security.ResourceOperation;
import org.apache.geode.security.ResourcePermission;

public class ExportStackTraceCommand extends GfshCommand {
  private final GetStackTracesFunction getStackTracesFunction = new GetStackTracesFunction();

  private final DateTimeFormatter formatter =
      DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS")
          .withZone(ZoneId.systemDefault());

  /**
   * Current implementation supports writing it to a locator/server side file and returning the
   * location of the file
   */
  @ShellMethod(value = CliStrings.EXPORT_STACKTRACE__HELP, key = CliStrings.EXPORT_STACKTRACE)
  @CliMetaData(relatedTopic = {CliStrings.TOPIC_GEODE_DEBUG_UTIL})
  @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
      operation = ResourcePermission.Operation.READ)
  public ResultModel exportStackTrace(@ShellOption(value = {CliStrings.MEMBER, CliStrings.MEMBERS},
      help = CliStrings.EXPORT_STACKTRACE__HELP) String[] memberNameOrId,

      @ShellOption(value = {CliStrings.GROUP, CliStrings.GROUPS},
          help = CliStrings.GROUP) String[] group,

      @ShellOption(value = CliStrings.EXPORT_STACKTRACE__FILE,
          help = CliStrings.EXPORT_STACKTRACE__FILE__HELP) String fileName,

      @ShellOption(value = CliStrings.EXPORT_STACKTRACE__FAIL__IF__FILE__PRESENT,
          defaultValue = "false",
          help = CliStrings.EXPORT_STACKTRACE__FAIL__IF__FILE__PRESENT__HELP) boolean failIfFilePresent)
      throws IOException {

    if (fileName == null) {
      StringBuilder filePrefix = new StringBuilder("stacktrace");
      fileName = filePrefix.append("_").append(System.currentTimeMillis()).toString();
    }
    final File outFile = new File(fileName);

    if (outFile.exists() && failIfFilePresent) {
      return ResultModel.createError(CliStrings
          .format(CliStrings.EXPORT_STACKTRACE__ERROR__FILE__PRESENT, outFile.getCanonicalPath()));
    }

    Map<String, byte[]> dumps = new HashMap<>();
    Set<DistributedMember> targetMembers = getMembersIncludingLocators(group, memberNameOrId);

    ResultModel result = new ResultModel();
    InfoResultModel resultData = result.addInfo();

    ResultCollector<?, ?> rc = executeFunction(getStackTracesFunction, null, targetMembers);
    @SuppressWarnings("unchecked")
    ArrayList<Object> resultList = (ArrayList<Object>) rc.getResult();

    for (Object resultObj : resultList) {
      if (resultObj instanceof StackTracesPerMember) {
        StackTracesPerMember stackTracePerMember = (StackTracesPerMember) resultObj;
        dumps.put(getHeaderMessage(stackTracePerMember),
            stackTracePerMember.getStackTraces());
      }
    }

    InternalDistributedSystem ads = ((InternalCache) getCache()).getInternalDistributedSystem();
    String filePath = writeStacksToFile(dumps, fileName);
    resultData.addLine(CliStrings.format(CliStrings.EXPORT_STACKTRACE__SUCCESS, filePath));
    resultData.addLine(CliStrings.EXPORT_STACKTRACE__HOST + ads.getDistributedMember().getHost());

    return result;

  }

  String getHeaderMessage(StackTracesPerMember stackTracesPerMember) {
    String headerMessage = stackTracesPerMember.getMemberNameOrId();

    Instant timestamp = stackTracesPerMember.getTimestamp();
    if (timestamp != null) {
      headerMessage += " at " + formatter.format(timestamp);
    }
    return headerMessage;
  }

  /***
   * Writes the Stack traces member-wise to a text file
   *
   * @param dumps - Map containing key : member , value : zipped stack traces
   * @param fileName - Name of the file to which the stack-traces are written to
   * @return Canonical path of the file which contains the stack-traces
   */
  public String writeStacksToFile(Map<String, byte[]> dumps, String fileName) throws IOException {
    String filePath;
    PrintWriter ps;
    File outputFile;

    outputFile = new File(fileName);
    try (OutputStream os = new FileOutputStream(outputFile)) {
      ps = new PrintWriter(os);

      for (Map.Entry<String, byte[]> entry : dumps.entrySet()) {
        ps.append(CliStrings.STACK_TRACE_FOR_MEMBER).append(entry.getKey()).append(" ***")
            .append(System.lineSeparator());
        ps.flush();
        GZIPInputStream zipIn = new GZIPInputStream(new ByteArrayInputStream(entry.getValue()));
        BufferedInputStream bin = new BufferedInputStream(zipIn);
        byte[] buffer = new byte[10000];
        int count;
        while ((count = bin.read(buffer)) != -1) {
          os.write(buffer, 0, count);
        }
        ps.append('\n');
      }
      ps.flush();
      filePath = outputFile.getCanonicalPath();
    }

    return filePath;
  }
}
