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

import static java.util.stream.Collectors.toList;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.Logger;

import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.management.internal.configuration.utils.ZipUtils;

/**
 * this LogExporter only finds the .log and .gfs files under in the same directory of the base files
 * it doesn't use the base file's filename patterns to find the related logs/stats yet.
 */
public class LogExporter {
  private static final Logger LOGGER = LogService.getLogger();

  private final LogFilter logFilter;
  private final File baseLogFile;
  private final File baseStatsFile;

  /**
   * @param logFilter the filter that's used to check if we need to accept the file or the logLine
   * @param baseLogFile if not null, we will export the logs in that directory
   * @param baseStatsFile if not null, we will export stats in that directory
   */
  public LogExporter(LogFilter logFilter, File baseLogFile, File baseStatsFile) {
    assert logFilter != null;
    this.logFilter = logFilter;
    this.baseLogFile = baseLogFile;
    this.baseStatsFile = baseStatsFile;
  }

  /**
   *
   * @return Path to the zip file that has all the filtered files, null if no files are selected to
   *         export.
   */
  public Path export() throws IOException {
    Path tempDirectory = Files.createTempDirectory("exportLogs");

    if (baseLogFile != null) {
      for (Path logFile : findLogFiles(baseLogFile.toPath().getParent())) {
        Path filteredLogFile = tempDirectory.resolve(logFile.getFileName());
        writeFilteredLogFile(logFile, filteredLogFile);
      }
    }

    if (baseStatsFile != null) {
      for (Path statFile : findStatFiles(baseStatsFile.toPath().getParent())) {
        Files.copy(statFile, tempDirectory.resolve(statFile.getFileName()));
      }
    }

    Path zipFile = null;
    if (tempDirectory.toFile().listFiles().length > 0) {
      zipFile = Files.createTempFile("logExport", ".zip");
      ZipUtils.zipDirectory(tempDirectory, zipFile);
      LOGGER.info("Zipped files to: " + zipFile);
    }

    FileUtils.deleteDirectory(tempDirectory.toFile());

    return zipFile;
  }

  protected void writeFilteredLogFile(Path originalLogFile, Path filteredLogFile)
      throws IOException {
    logFilter.startNewFile();

    try (BufferedReader reader = new BufferedReader(new FileReader(originalLogFile.toFile()))) {
      try (BufferedWriter writer = new BufferedWriter(new FileWriter(filteredLogFile.toFile()))) {

        String line;
        while ((line = reader.readLine()) != null) {
          LogFilter.LineFilterResult result = logFilter.acceptsLine(line);

          if (result == LogFilter.LineFilterResult.REMAINDER_OF_FILE_REJECTED) {
            break;
          }

          if (result == LogFilter.LineFilterResult.LINE_ACCEPTED) {
            writeLine(line, writer);
          }
        }
      }
    }
  }

  private void writeLine(String line, BufferedWriter writer) {
    try {
      writer.write(line);
      writer.newLine();
    } catch (IOException e) {
      throw new RuntimeException("Unable to write to log file", e);
    }
  }

  /**
   * @return combined size of stat archives and filtered log files in bytes
   */
  public long estimateFilteredSize() throws IOException {
    long filteredSize = 0;
    if (baseLogFile != null) {
      for (Path logFile : findLogFiles(baseLogFile.toPath().getParent())) {
        filteredSize += filterAndSize(logFile);
      }
    }

    if (baseStatsFile != null) {
      for (Path statFile : findStatFiles(baseStatsFile.toPath().getParent())) {
        filteredSize += statFile.toFile().length();
      }
    }

    return filteredSize;
  }

  /**
   * @return size of file in bytes
   */
  private long filterAndSize(Path originalLogFile) throws IOException {
    long size = 0;
    logFilter.startNewFile();

    try (BufferedReader reader = new BufferedReader(new FileReader(originalLogFile.toFile()))) {
      String line;
      while ((line = reader.readLine()) != null) {
        LogFilter.LineFilterResult result = logFilter.acceptsLine(line);

        if (result == LogFilter.LineFilterResult.REMAINDER_OF_FILE_REJECTED) {
          break;
        }
        if (result == LogFilter.LineFilterResult.LINE_ACCEPTED) {
          size += line.length() + System.lineSeparator().length();
        }
      }
    }
    return size;
  }

  List<Path> findLogFiles(Path workingDir) throws IOException {
    Predicate<Path> logFileSelector = (Path file) -> file.toString().toLowerCase().contains(".log");
    return findFiles(workingDir, logFileSelector);
  }


  List<Path> findStatFiles(Path workingDir) throws IOException {
    Predicate<Path> statFileSelector =
        (Path file) -> file.toString().toLowerCase().endsWith(".gfs");
    return findFiles(workingDir, statFileSelector);
  }

  private List<Path> findFiles(Path workingDir, Predicate<Path> fileSelector) throws IOException {
    if (!workingDir.toFile().isDirectory()) {
      return Collections.emptyList();
    }
    try (Stream<Path> stream = Files.list(workingDir)) {
      return stream
          .filter(Files::isRegularFile)
          .filter(fileSelector)
          .filter(logFilter::acceptsFile)
          .collect(toList());
    }
  }
}
