package org.apache.helix.tools.commandtools;

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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.zip.GZIPInputStream;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * utility for grep zk transaction/snapshot logs
 * - to grep a pattern by t1 use:
 * zkgrep --zkCfg zkCfg --by t1 --pattern patterns...
 * - to grep a pattern between t1 and t2 use:
 * zkgrep --zkCfg zkCfg --between t1 t2 --pattern patterns...
 * for example, to find fail-over latency between t1 and t2, use:
 * 1) zkgrep --zkCfg zkCfg --by t1 --pattern "/{cluster}/LIVEINSTNCES/" | grep {fail-node}
 * 2) zkgrep --zkCfg zkCfg --between t1 t2 --pattern "closeSession" | grep {fail-node session-id}
 * 3) zkgrep --zkCfg zkCfg --between t1 t2 --pattern "/{cluster}" | grep "CURRENTSTATES" |
 * grep "setData" | tail -1
 * fail-over latency = timestamp difference between 2) and 3)
 */
public class ZkGrep {
  private static Logger LOG = LoggerFactory.getLogger(ZkGrep.class);

  private static final String zkCfg = "zkCfg";
  private static final String pattern = "pattern";
  private static final String by = "by";
  private static final String between = "between";

  public static final String log = "log";
  public static final String snapshot = "snapshot";

  private static final String gzSuffix = ".gz";

  @SuppressWarnings("static-access")
  private static Options constructCommandLineOptions() {
    Option zkCfgOption =
        OptionBuilder.hasArgs(1).isRequired(false).withLongOpt(zkCfg).withArgName("zoo.cfg")
            .withDescription("provide zoo.cfg").create();

    Option patternOption =
        OptionBuilder.hasArgs().isRequired(true).withLongOpt(pattern)
            .withArgName("grep-patterns...").withDescription("provide patterns (required)")
            .create();

    Option betweenOption =
        OptionBuilder.hasArgs(2).isRequired(false).withLongOpt(between)
            .withArgName("t1 t2 (timestamp in ms or yyMMdd_hhmmss_SSS)")
            .withDescription("grep between t1 and t2").create();

    Option byOption =
        OptionBuilder.hasArgs(1).isRequired(false).withLongOpt(by)
            .withArgName("t (timestamp in ms or yyMMdd_hhmmss_SSS)").withDescription("grep by t")
            .create();

    OptionGroup group = new OptionGroup();
    group.setRequired(true);
    group.addOption(betweenOption);
    group.addOption(byOption);

    Options options = new Options();
    options.addOption(zkCfgOption);
    options.addOption(patternOption);
    options.addOptionGroup(group);
    return options;
  }

  /**
   * get zk transaction log dir and zk snapshot log dir
   * @param zkCfgFile
   * @return String[0]: zk-transaction-log-dir, String[1]: zk-snapshot-dir
   */
  static String[] getZkDataDirs(String zkCfgFile) {
    String[] zkDirs = new String[2];

    FileInputStream fis = null;
    BufferedReader br = null;
    try {
      fis = new FileInputStream(zkCfgFile);
      br = new BufferedReader(new InputStreamReader(fis));

      String line;
      while ((line = br.readLine()) != null) {
        String key = "dataDir=";
        if (line.startsWith(key)) {
          zkDirs[1] = zkDirs[0] = line.substring(key.length()) + "/version-2";
        }

        key = "dataLogDir=";
        if (line.startsWith(key)) {
          zkDirs[0] = line.substring(key.length()) + "/version-2";
        }
      }
    } catch (Exception e) {
      LOG.error("exception in read file: " + zkCfgFile, e);
    } finally {
      try {
        if (br != null) {
          br.close();
        }

        if (fis != null) {
          fis.close();
        }

      } catch (Exception e) {
        LOG.error("exception in closing file: " + zkCfgFile, e);
      }
    }

    return zkDirs;
  }

  // debug
  static void printFiles(File[] files) {
    System.out.println("START print");
    for (int i = 0; i < files.length; i++) {
      File file = files[i];
      System.out.println(file.getName() + ", " + file.lastModified());
    }
    System.out.println("END print");
  }

  /**
   * get files under dir in order of last modified time
   * @param dir
   * @param pattern
   * @return
   */
  static File[] getSortedFiles(String dirPath, final String pattern) {
    File dir = new File(dirPath);
    File[] files = dir.listFiles(new FileFilter() {

      @Override
      public boolean accept(File file) {
        return file.isFile() && (file.getName().indexOf(pattern) != -1);
      }
    });

    Arrays.sort(files, new Comparator<File>() {

      @Override
      public int compare(File o1, File o2) {
        int sign = (int) Math.signum(o1.lastModified() - o2.lastModified());
        return sign;
      }

    });
    return files;
  }

  /**
   * get value for an attribute in a parsed zk log; e.g.
   * "time:1384984016778 session:0x14257d1d17e0004 cxid:0x5 zxid:0x46899 type:error err:-101"
   * given "time" return "1384984016778"
   * @param line
   * @param attribute
   * @return value
   */
  static String getAttributeValue(String line, String attribute) {
    if (line == null) {
      return null;
    }

    if (!attribute.endsWith(":")) {
      attribute = attribute + ":";
    }

    String[] parts = line.split("\\s");
    if (parts != null && parts.length > 0) {
      for (int i = 0; i < parts.length; i++) {
        if (parts[i].startsWith(attribute)) {
          String val = parts[i].substring(attribute.length());
          return val;
        }
      }
    }
    return null;
  }

  static long getTimestamp(String line) {
    String timestamp = getAttributeValue(line, "time");
    return Long.parseLong(timestamp);
  }

  /**
   * parse a time string either in timestamp form or "yyMMdd_hhmmss_SSS" form
   * @param time
   * @return timestamp or -1 on error
   */
  static long parseTimeString(String time) {
    try {
      return Long.parseLong(time);
    } catch (NumberFormatException e) {
      try {
        SimpleDateFormat formatter = new SimpleDateFormat("yyMMdd_hhmmss_SSS");
        Date date = formatter.parse(time);
        return date.getTime();
      } catch (java.text.ParseException ex) {
        LOG.error("fail to parse time string: " + time, e);
      }
    }
    return -1;
  }

  public static void grepZkLog(File zkLog, long start, long end, String... patterns) {
    FileInputStream fis = null;
    BufferedReader br = null;
    try {
      fis = new FileInputStream(zkLog);
      br = new BufferedReader(new InputStreamReader(fis));

      String line;
      while ((line = br.readLine()) != null) {
        try {
          long timestamp = getTimestamp(line);
          if (timestamp > end) {
            break;
          }

          if (timestamp < start) {
            continue;
          }

          boolean match = true;
          for (String pattern : patterns) {
            if (line.indexOf(pattern) == -1) {
              match = false;
              break;
            }
          }

          if (match) {
            System.out.println(line);
          }

        } catch (NumberFormatException e) {
          // ignore
        }
      }
    } catch (Exception e) {
      LOG.error("exception in grep zk-log: " + zkLog, e);
    } finally {
      try {
        if (br != null) {
          br.close();
        }

        if (fis != null) {
          fis.close();
        }

      } catch (Exception e) {
        LOG.error("exception in closing zk-log: " + zkLog, e);
      }
    }
  }

  public static void grepZkLogDir(List<File> parsedZkLogs, long start, long end, String... patterns) {
    for (File file : parsedZkLogs) {
      grepZkLog(file, start, end, patterns);

    }

  }

  public static void grepZkSnapshot(File zkSnapshot, String... patterns) {
    FileInputStream fis = null;
    BufferedReader br = null;
    try {
      fis = new FileInputStream(zkSnapshot);
      br = new BufferedReader(new InputStreamReader(fis));

      String line;
      while ((line = br.readLine()) != null) {
        try {
          boolean match = true;
          for (String pattern : patterns) {
            if (line.indexOf(pattern) == -1) {
              match = false;
              break;
            }
          }

          if (match) {
            System.out.println(line);
          }

        } catch (NumberFormatException e) {
          // ignore
        }
      }
    } catch (Exception e) {
      LOG.error("exception in grep zk-snapshot: " + zkSnapshot, e);
    } finally {
      try {
        if (br != null) {
          br.close();
        }

        if (fis != null) {
          fis.close();
        }

      } catch (Exception e) {
        LOG.error("exception in closing zk-snapshot: " + zkSnapshot, e);
      }
    }
  }

  /**
   * guess zoo.cfg dir
   * @return absolute path to zoo.cfg
   */
  static String guessZkCfgDir() {
    // TODO impl this
    return null;
  }

  public static void printUsage(Options cliOptions) {
    HelpFormatter helpFormatter = new HelpFormatter();
    helpFormatter.setWidth(1000);
    helpFormatter.printHelp("java " + ZkGrep.class.getName(), cliOptions);
  }

  /**
   * parse zk-transaction-logs between start and end, if not already parsed
   * @param zkLogDir
   * @param start
   * @param end
   * @return list of parsed zklogs between start and end, in order of last modified timestamp
   */
  static List<File> parseZkLogs(String zkLogDir, long start, long end) {
    File zkParsedDir = new File(String.format("%s/zklog-parsed", System.getProperty("user.home")));
    File[] zkLogs = getSortedFiles(zkLogDir, log);
    // printFiles(zkDataFiles);
    List<File> parsedZkLogs = new ArrayList<File>();

    boolean stop = false;
    for (File zkLog : zkLogs) {
      if (stop) {
        break;
      }

      if (zkLog.lastModified() < start) {
        continue;
      }

      if (zkLog.lastModified() > end) {
        stop = true;
      }

      try {
        File parsedZkLog = new File(zkParsedDir, stripGzSuffix(zkLog.getName()) + ".parsed");
        if (!parsedZkLog.exists() || parsedZkLog.lastModified() <= zkLog.lastModified()) {

          if (zkLog.getName().endsWith(gzSuffix)) {
            // copy and gunzip it
            FileUtils.copyFileToDirectory(zkLog, zkParsedDir);
            File zkLogGz = new File(zkParsedDir, zkLog.getName());
            File tmpZkLog = gunzip(zkLogGz);

            // parse gunzip file
            ZKLogFormatter
                .main(new String[] { log, tmpZkLog.getAbsolutePath(), parsedZkLog.getAbsolutePath()
                });

            // delete it
            zkLogGz.delete();
            tmpZkLog.delete();
          } else {
            // parse it directly
            ZKLogFormatter.main(new String[] {
                log, zkLog.getAbsolutePath(), parsedZkLog.getAbsolutePath()
            });
          }
        }
        parsedZkLogs.add(parsedZkLog);
      } catch (Exception e) {
        LOG.error("fail to parse zkLog: " + zkLog, e);
      }
    }

    return parsedZkLogs;
  }

  /**
   * Strip off a .gz suffix if any
   * @param filename
   * @return
   */
  static String stripGzSuffix(String filename) {
    if (filename.endsWith(gzSuffix)) {
      return filename.substring(0, filename.length() - gzSuffix.length());
    }
    return filename;
  }

  /**
   * Gunzip a file
   * @param zipFile
   * @return
   */
  static File gunzip(File zipFile) {
    File outputFile = new File(stripGzSuffix(zipFile.getAbsolutePath()));

    byte[] buffer = new byte[1024];

    try {

      GZIPInputStream gzis = new GZIPInputStream(new FileInputStream(zipFile));
      FileOutputStream out = new FileOutputStream(outputFile);

      int len;
      while ((len = gzis.read(buffer)) > 0) {
        out.write(buffer, 0, len);
      }

      gzis.close();
      out.close();

      return outputFile;
    } catch (IOException e) {
      LOG.error("fail to gunzip file: " + zipFile, e);
    }

    return null;
  }

  /**
   * parse the last zk-snapshots by by-time, if not already parsed
   * @param zkSnapshotDir
   * @param byTime
   * @return File array which the first element is the last zk-snapshot by by-time and the second
   *         element is its parsed file
   */
  static File[] parseZkSnapshot(String zkSnapshotDir, long byTime) {
    File[] retFiles = new File[2];
    File zkParsedDir = new File(String.format("%s/zklog-parsed", System.getProperty("user.home")));
    File[] zkSnapshots = getSortedFiles(zkSnapshotDir, snapshot);
    // printFiles(zkDataFiles);
    File lastZkSnapshot = null;
    for (int i = 0; i < zkSnapshots.length; i++) {
      File zkSnapshot = zkSnapshots[i];
      if (zkSnapshot.lastModified() >= byTime) {
        break;
      }
      lastZkSnapshot = zkSnapshot;
      retFiles[0] = lastZkSnapshot;
    }

    try {
      File parsedZkSnapshot =
          new File(zkParsedDir, stripGzSuffix(lastZkSnapshot.getName()) + ".parsed");
      if (!parsedZkSnapshot.exists()
          || parsedZkSnapshot.lastModified() <= lastZkSnapshot.lastModified()) {

        if (lastZkSnapshot.getName().endsWith(gzSuffix)) {
          // copy and gunzip it
          FileUtils.copyFileToDirectory(lastZkSnapshot, zkParsedDir);
          File lastZkSnapshotGz = new File(zkParsedDir, lastZkSnapshot.getName());
          File tmpLastZkSnapshot = gunzip(lastZkSnapshotGz);

          // parse gunzip file
          ZKLogFormatter.main(new String[] {
              snapshot, tmpLastZkSnapshot.getAbsolutePath(), parsedZkSnapshot.getAbsolutePath()
          });

          // delete it
          lastZkSnapshotGz.delete();
          tmpLastZkSnapshot.delete();
        } else {
          // parse it directly
          ZKLogFormatter.main(new String[] {
              snapshot, lastZkSnapshot.getAbsolutePath(), parsedZkSnapshot.getAbsolutePath()
          });
        }

      }
      retFiles[1] = parsedZkSnapshot;
      return retFiles;
    } catch (Exception e) {
      LOG.error("fail to parse zkSnapshot: " + lastZkSnapshot, e);
    }

    return null;
  }

  public static void processCommandLineArgs(String[] cliArgs) {
    CommandLineParser cliParser = new GnuParser();
    Options cliOptions = constructCommandLineOptions();
    CommandLine cmd = null;

    try {
      cmd = cliParser.parse(cliOptions, cliArgs);
    } catch (ParseException pe) {
      System.err.println("CommandLineClient: failed to parse command-line options: " + pe);
      printUsage(cliOptions);
      System.exit(1);
    }

    String zkCfgDirValue = null;
    String zkCfgFile = null;

    if (cmd.hasOption(zkCfg)) {
      zkCfgDirValue = cmd.getOptionValue(zkCfg);
    }

    if (zkCfgDirValue == null) {
      zkCfgDirValue = guessZkCfgDir();
    }

    if (zkCfgDirValue == null) {
      LOG.error("couldn't figure out path to zkCfg file");
      System.exit(1);
    }

    // get zoo.cfg path from cfg-dir
    zkCfgFile = zkCfgDirValue;
    if (!zkCfgFile.endsWith(".cfg")) {
      // append with default zoo.cfg
      zkCfgFile = zkCfgFile + "/zoo.cfg";
    }

    if (!new File(zkCfgFile).exists()) {
      LOG.error("zoo.cfg file doen't exist: " + zkCfgFile);
      System.exit(1);
    }

    String[] patterns = cmd.getOptionValues(pattern);

    String[] zkDataDirs = getZkDataDirs(zkCfgFile);

    // parse zk data files
    if (zkDataDirs == null || zkDataDirs[0] == null || zkDataDirs[1] == null) {
      LOG.error("invalid zkCfgDir: " + zkCfgDirValue);
      System.exit(1);
    }

    File zkParsedDir = new File(String.format("%s/zklog-parsed", System.getProperty("user.home")));
    if (!zkParsedDir.exists()) {
      LOG.info("creating zklog-parsed dir: " + zkParsedDir.getAbsolutePath());
      zkParsedDir.mkdir();
    }

    if (cmd.hasOption(between)) {
      String[] timeStrings = cmd.getOptionValues(between);

      long startTime = parseTimeString(timeStrings[0]);
      if (startTime == -1) {
        LOG.error("invalid start time string: " + timeStrings[0]
            + ", should be either timestamp or yyMMdd_hhmmss_SSS");
        System.exit(1);
      }

      long endTime = parseTimeString(timeStrings[1]);
      if (endTime == -1) {
        LOG.error("invalid end time string: " + timeStrings[1]
            + ", should be either timestamp or yyMMdd_hhmmss_SSS");
        System.exit(1);
      }

      if (startTime > endTime) {
        LOG.warn("empty window: " + startTime + " - " + endTime);
        System.exit(1);
      }
      // zkDataDirs[0] is the transaction log dir
      List<File> parsedZkLogs = parseZkLogs(zkDataDirs[0], startTime, endTime);
      grepZkLogDir(parsedZkLogs, startTime, endTime, patterns);

    } else if (cmd.hasOption(by)) {
      String timeString = cmd.getOptionValue(by);

      long byTime = parseTimeString(timeString);
      if (byTime == -1) {
        LOG.error("invalid by time string: " + timeString
            + ", should be either timestamp or yyMMdd_hhmmss_SSS");
        System.exit(1);
      }

      // zkDataDirs[1] is the snapshot dir
      File[] lastZkSnapshot = parseZkSnapshot(zkDataDirs[1], byTime);

      // lastZkSnapshot[1] is the parsed last snapshot by byTime
      grepZkSnapshot(lastZkSnapshot[1], patterns);

      // need to grep transaction logs between last-modified-time of snapshot and byTime also
      // lastZkSnapshot[0] is the last snapshot by byTime
      long startTime = lastZkSnapshot[0].lastModified();

      // zkDataDirs[0] is the transaction log dir
      List<File> parsedZkLogs = parseZkLogs(zkDataDirs[0], startTime, byTime);
      grepZkLogDir(parsedZkLogs, startTime, byTime, patterns);
    }
  }

  public static void main(String[] args) {
    processCommandLineArgs(args);
  }

}
