/*
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.zookeeper.server;

import static org.apache.zookeeper.server.persistence.FileSnap.SNAPSHOT_FILE_PREFIX;
import com.fasterxml.jackson.core.io.JsonStringEncoder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.jute.BinaryInputArchive;
import org.apache.jute.InputArchive;
import org.apache.yetus.audience.InterfaceAudience;
import org.apache.zookeeper.ZKUtil;
import org.apache.zookeeper.data.StatPersisted;
import org.apache.zookeeper.server.persistence.FileSnap;
import org.apache.zookeeper.server.persistence.SnapStream;
import org.apache.zookeeper.server.persistence.Util;
import org.apache.zookeeper.util.ServiceUtils;

/**
 * Dump a snapshot file to stdout.
 *
 * For JSON format, followed https://dev.yorhel.nl/ncdu/jsonfmt
 */
@InterfaceAudience.Public
public class SnapshotFormatter {

    // per-znode counter so ncdu treats each as a unique object
    private static Integer INODE_IDX = 1000;

    /**
     * USAGE: SnapshotFormatter snapshot_file or the ready-made script: zkSnapShotToolkit.sh
     */
    public static void main(String[] args) throws Exception {
        String snapshotFile = null;
        boolean dumpData = false;
        boolean dumpJson = false;

        int i;
        for (i = 0; i < args.length; i++) {
            if (args[i].equals("-d")) {
                dumpData = true;
            } else if (args[i].equals("-json")) {
                dumpJson = true;
            } else {
                snapshotFile = args[i];
                i++;
                break;
            }
        }
        if (args.length != i || snapshotFile == null) {
            System.err.println("USAGE: SnapshotFormatter [-d|-json] snapshot_file");
            System.err.println("       -d dump the data for each znode");
            System.err.println("       -json dump znode info in json format");
            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
            return;
        }

        String error = ZKUtil.validateFileInput(snapshotFile);
        if (null != error) {
            System.err.println(error);
            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
        }

        if (dumpData && dumpJson) {
            System.err.println("Cannot specify both data dump (-d) and json mode (-json) in same call");
            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
        }

        new SnapshotFormatter().run(snapshotFile, dumpData, dumpJson);
    }

    public void run(String snapshotFileName, boolean dumpData, boolean dumpJson) throws IOException {
        File snapshotFile = new File(snapshotFileName);
        try (InputStream is = SnapStream.getInputStream(snapshotFile)) {
            InputArchive ia = BinaryInputArchive.getArchive(is);

            FileSnap fileSnap = new FileSnap(null);

            DataTree dataTree = new DataTree();
            Map<Long, Integer> sessions = new HashMap<Long, Integer>();

            fileSnap.deserialize(dataTree, sessions, ia);
            long fileNameZxid = Util.getZxidFromName(snapshotFile.getName(), SNAPSHOT_FILE_PREFIX);

            if (dumpJson) {
                printSnapshotJson(dataTree);
            } else {
                printDetails(dataTree, sessions, dumpData, fileNameZxid);
            }
        }
    }

    private void printDetails(DataTree dataTree, Map<Long, Integer> sessions, boolean dumpData, long fileNameZxid) {
        long dtZxid = printZnodeDetails(dataTree, dumpData);
        printSessionDetails(dataTree, sessions);
        DataTree.ZxidDigest targetZxidDigest = dataTree.getDigestFromLoadedSnapshot();
        if (targetZxidDigest != null) {
            System.out.println(String.format("Target zxid digest is: %s, %s",
                    Long.toHexString(targetZxidDigest.zxid), targetZxidDigest.digest));
        }
        System.out.println(String.format("----%nLast zxid: 0x%s", Long.toHexString(Math.max(fileNameZxid, dtZxid))));
    }

    private long printZnodeDetails(DataTree dataTree, boolean dumpData) {
        System.out.println(String.format("ZNode Details (count=%d):", dataTree.getNodeCount()));

        final long zxid = printZnode(dataTree, "/", dumpData);
        System.out.println("----");
        return zxid;
    }

    private long printZnode(DataTree dataTree, String name, boolean dumpData) {
        System.out.println("----");
        DataNode n = dataTree.getNode(name);
        Set<String> children;
        long zxid;
        synchronized (n) { // keep findbugs happy
            System.out.println(name);
            printStat(n.stat);
            zxid = Math.max(n.stat.getMzxid(), n.stat.getPzxid());
            if (dumpData) {
                System.out.println("  data = " + (n.data == null ? "" : Base64.getEncoder().encodeToString(n.data)));
            } else {
                System.out.println("  dataLength = " + (n.data == null ? 0 : n.data.length));
            }
            children = n.getChildren();
        }
        if (children != null) {
            for (String child : children) {
                long cxid = printZnode(dataTree, name + (name.equals("/") ? "" : "/") + child, dumpData);
                zxid = Math.max(zxid, cxid);
            }
        }
        return zxid;
    }

    private void printSessionDetails(DataTree dataTree, Map<Long, Integer> sessions) {
        System.out.println("Session Details (sid, timeout, ephemeralCount):");
        for (Map.Entry<Long, Integer> e : sessions.entrySet()) {
            long sid = e.getKey();
            System.out.println(String.format("%#016x, %d, %d", sid, e.getValue(), dataTree.getEphemerals(sid).size()));
        }
    }

    private void printStat(StatPersisted stat) {
        printHex("cZxid", stat.getCzxid());
        System.out.println("  ctime = " + new Date(stat.getCtime()).toString());
        printHex("mZxid", stat.getMzxid());
        System.out.println("  mtime = " + new Date(stat.getMtime()).toString());
        printHex("pZxid", stat.getPzxid());
        System.out.println("  cversion = " + stat.getCversion());
        System.out.println("  dataVersion = " + stat.getVersion());
        System.out.println("  aclVersion = " + stat.getAversion());
        printHex("ephemeralOwner", stat.getEphemeralOwner());
    }

    private void printHex(String prefix, long value) {
        System.out.println(String.format("  %s = %#016x", prefix, value));
    }

    private void printSnapshotJson(final DataTree dataTree) {
        JsonStringEncoder encoder = JsonStringEncoder.getInstance();
        System.out.printf(
            "[1,0,{\"progname\":\"SnapshotFormatter.java\",\"progver\":\"0.01\",\"timestamp\":%d}",
            System.currentTimeMillis());
        printZnodeJson(dataTree, "/", encoder);
        System.out.print("]");
    }

    private void printZnodeJson(final DataTree dataTree, final String fullPath, JsonStringEncoder encoder) {


        final DataNode n = dataTree.getNode(fullPath);

        if (null == n) {
            System.err.println("DataTree Node for " + fullPath + " doesn't exist");
            return;
        }

        final String name = fullPath.equals("/")
            ? fullPath
            : fullPath.substring(fullPath.lastIndexOf("/") + 1);

        System.out.print(",");

        int dataLen;
        synchronized (n) { // keep findbugs happy
            dataLen = (n.data == null) ? 0 : n.data.length;
        }
        StringBuilder nodeSB = new StringBuilder();
        nodeSB.append("{");
        nodeSB.append("\"name\":\"").append(encoder.quoteAsString(name)).append("\"").append(",");
        nodeSB.append("\"asize\":").append(dataLen).append(",");
        nodeSB.append("\"dsize\":").append(dataLen).append(",");
        nodeSB.append("\"dev\":").append(0).append(",");
        nodeSB.append("\"ino\":").append(++INODE_IDX);
        nodeSB.append("}");

        Set<String> children;
        synchronized (n) { // keep findbugs happy
            children = n.getChildren();
        }
        if (children != null && children.size() > 0) {
            System.out.print("[" + nodeSB);
            for (String child : children) {
                printZnodeJson(dataTree, fullPath + (fullPath.equals("/") ? "" : "/") + child, encoder);
            }
            System.out.print("]");
        } else {
            System.out.print(nodeSB);
        }
    }

}
