package play;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.jamonapi.MonitorFactory;
import com.jamonapi.utils.Misc;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.commons.lang.StringUtils;
import play.Play.Mode;
import play.classloading.ApplicationClasses.ApplicationClass;
import play.classloading.enhancers.ContinuationEnhancer;
import play.classloading.enhancers.ControllersEnhancer;
import play.classloading.enhancers.Enhancer;
import play.classloading.enhancers.LocalvariablesNamesEnhancer;
import play.classloading.enhancers.MailerEnhancer;
import play.classloading.enhancers.PropertiesEnhancer;
import play.classloading.enhancers.SigEnhancer;
import play.exceptions.UnexpectedException;
import play.libs.Crypto;
import play.mvc.Http.Header;
import play.mvc.Http.Request;
import play.mvc.Http.Response;

/**
 * Plugin used for core tasks
 */
public class CorePlugin extends PlayPlugin {

    /**
     * Get the application status
     */
    public static String computeApplicationStatus(boolean json) {
        if (json) {
            JsonObject o = new JsonObject();
            for (PlayPlugin plugin : Play.pluginCollection.getEnabledPlugins()) {
                try {
                    JsonObject status = plugin.getJsonStatus();
                    if (status != null) {
                        o.add(plugin.getClass().getName(), status);
                    }
                } catch (Throwable e) {
                    JsonObject error = new JsonObject();
                    error.add("error", new JsonPrimitive(e.getMessage()));
                    o.add(plugin.getClass().getName(), error);
                }
            }
            return o.toString();
        }
        StringBuilder dump = new StringBuilder(16);
        for (PlayPlugin plugin : Play.pluginCollection.getEnabledPlugins()) {
            try {
                String status = plugin.getStatus();
                if (status != null) {
                    dump.append(status);
                    dump.append("\n");
                }
            } catch (Throwable e) {
                dump.append(plugin.getClass().getName()).append(".getStatus() has failed (").append(e.getMessage()).append(")");
            }
        }
        return dump.toString();
    }

    /**
     * Intercept /@status and check that the Authorization header is valid. 
     * Then ask each plugin for a status dump and send it over the HTTP response.
     *
     * You can ask the /@status using the authorization header and putting your status secret key in it.
     * Prior to that you would be required to start play with  a -DstatusKey=yourkey
     */
    @Override
    public boolean rawInvocation(Request request, Response response) throws Exception {
        if (Play.mode == Mode.DEV && request.path.equals("/@kill")) {
            System.out.println("@KILLED");
            if (Play.standalonePlayServer) {
                System.exit(0);
            } else {
                Logger.error("Cannot execute @kill since Play is not running as standalone server");
            }
        }
        if (request.path.equals("/@status") || request.path.equals("/@status.json")) {
            if(!Play.started) {
                response.print("Application is not started");
                response.status = 503;
                return true;
            }
            response.contentType = request.path.contains(".json") ? "application/json" : "text/plain";
            Header authorization = request.headers.get("authorization");
            if (authorization != null && (Crypto.sign("@status").equals(authorization.value()) || System.getProperty("statusKey", Play.secretKey).equals(authorization.value()))) {
                response.print(computeApplicationStatus(request.path.contains(".json")));
                response.status = 200;
                return true;
            }
            response.status = 401;
            if (response.contentType.equals("application/json")) {
                response.print("{\"error\": \"Not authorized\"}");
            } else {
                response.print("Not authorized");
            }
            return true;
        }
        return super.rawInvocation(request, response);
    }

    /**
     * Retrieve status about play core.
     */
    @Override
    public String getStatus() {
        StringWriter sw = new StringWriter();
        PrintWriter out = new PrintWriter(sw);
        out.println("Java:");
        out.println("~~~~~");
        out.println("Version: " + System.getProperty("java.version"));
        out.println("Home: " + System.getProperty("java.home"));
        out.println("Max memory: " + Runtime.getRuntime().maxMemory());
        out.println("Free memory: " + Runtime.getRuntime().freeMemory());
        out.println("Total memory: " + Runtime.getRuntime().totalMemory());
        out.println("Available processors: " + Runtime.getRuntime().availableProcessors());
        out.println();
        out.println("Play framework:");
        out.println("~~~~~~~~~~~~~~~");
        out.println("Version: " + Play.version);
        out.println("Path: " + Play.frameworkPath);
        out.println("ID: " + (StringUtils.isEmpty(Play.id) ? "(not set)" : Play.id));
        out.println("Mode: " + Play.mode);
        out.println("Tmp dir: " + (Play.tmpDir == null ? "(no tmp dir)" : Play.tmpDir));
        out.println();
        out.println("Application:");
        out.println("~~~~~~~~~~~~");
        out.println("Path: " + Play.applicationPath);
        out.println("Name: " + Play.configuration.getProperty("application.name", "(not set)"));
        out.println("Started at: " + (Play.started ? new SimpleDateFormat("MM/dd/yyyy HH:mm").format(new Date(Play.startedAt)) : "Not yet started"));
        out.println();
        out.println("Loaded modules:");
        out.println("~~~~~~~~~~~~~~");
        for (String module : Play.modules.keySet()) {
            out.println(module + " at " + Play.modules.get(module).getRealFile());
        }
        out.println();
        out.println("Loaded plugins:");
        out.println("~~~~~~~~~~~~~~");
        for (PlayPlugin plugin : Play.pluginCollection.getAllPlugins()) {
            out.println(plugin.index + ":" + plugin.getClass().getName() + " [" + (Play.pluginCollection.isEnabled(plugin) ? "enabled" : "disabled") + "]");
        }
        out.println();
        out.println("Threads:");
        out.println("~~~~~~~~");
        try {
            visit(out, getRootThread(), 0);
        } catch (Throwable e) {
            out.println("Oops; " + e.getMessage());
        }
        out.println();
        out.println("Requests execution pool:");
        out.println("~~~~~~~~~~~~~~~~~~~~~~~~");
        out.println("Pool size: " + Invoker.executor.getPoolSize());
        out.println("Active count: " + Invoker.executor.getActiveCount());
        out.println("Scheduled task count: " + Invoker.executor.getTaskCount());
        out.println("Queue size: " + Invoker.executor.getQueue().size());
        out.println();
        try {
            out.println("Monitors:");
            out.println("~~~~~~~~");
            Object[][] data = Misc.sort(MonitorFactory.getRootMonitor().getBasicData(), 3, "desc");
            int lm = 10;
            for (Object[] row : data) {
                if (row[0].toString().length() > lm) {
                    lm = row[0].toString().length();
                }
            }
            for (Object[] row : data) {
                if (((Double) row[1]) > 0) {
                    out.println(String.format("%-" + (lm) + "s -> %8.0f hits; %8.1f avg; %8.1f min; %8.1f max;", row[0], row[1], row[2], row[6], row[7]));
                }
            }
        } catch (Exception e) {
            out.println("No monitors found");
        }
        return sw.toString();
    }

    @Override
    public JsonObject getJsonStatus() {
        JsonObject status = new JsonObject();

        {
            JsonObject java = new JsonObject();
            java.addProperty("version", System.getProperty("java.version"));
            status.add("java", java);
        }

        {
            JsonObject memory = new JsonObject();
            memory.addProperty("max", Runtime.getRuntime().maxMemory());
            memory.addProperty("free", Runtime.getRuntime().freeMemory());
            memory.addProperty("total", Runtime.getRuntime().totalMemory());
            status.add("memory", memory);
        }

        {
            JsonObject application = new JsonObject();
            application.addProperty("uptime", Play.started ? System.currentTimeMillis() - Play.startedAt : -1);
            application.addProperty("path", Play.applicationPath.getAbsolutePath());
            status.add("application", application);
        }

        {
            JsonObject pool = new JsonObject();
            pool.addProperty("size", Invoker.executor.getPoolSize());
            pool.addProperty("active", Invoker.executor.getActiveCount());
            pool.addProperty("scheduled", Invoker.executor.getTaskCount());
            pool.addProperty("queue", Invoker.executor.getQueue().size());
            status.add("pool", pool);
        }

        {
            JsonArray monitors = new JsonArray();
            try {
                Object[][] data = Misc.sort(MonitorFactory.getRootMonitor().getBasicData(), 3, "desc");
                for (Object[] row : data) {
                    if (((Double) row[1]) > 0) {
                        JsonObject o = new JsonObject();
                        o.addProperty("name", row[0].toString());
                        o.addProperty("hits", (Double) row[1]);
                        o.addProperty("avg", (Double) row[2]);
                        o.addProperty("min", (Double) row[6]);
                        o.addProperty("max", (Double) row[7]);
                        monitors.add(o);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            status.add("monitors", monitors);
        }

        return status;
    }

    /**
     * Recursively visit all JVM threads
     */
    static void visit(PrintWriter out, ThreadGroup group, int level) {
        // Get threads in `group'
        int numThreads = group.activeCount();
        Thread[] threads = new Thread[numThreads * 2];
        numThreads = group.enumerate(threads, false);

        // Enumerate each thread in `group'
        for (int i = 0; i < numThreads; i++) {
            // Get thread
            Thread thread = threads[i];
            out.println(thread + " " + thread.getState());
        }

        // Get thread subgroups of `group'
        int numGroups = group.activeGroupCount();
        ThreadGroup[] groups = new ThreadGroup[numGroups * 2];
        numGroups = group.enumerate(groups, false);

        // Recursively visit each subgroup
        for (int i = 0; i < numGroups; i++) {
            visit(out, groups[i], level + 1);
        }
    }

    /**
     * Retrieve the JVM root thread group.
     */
    static ThreadGroup getRootThread() {
        ThreadGroup root = Thread.currentThread().getThreadGroup().getParent();
        while (root.getParent() != null) {
            root = root.getParent();
        }
        return root;
    }

    @Override
    public void enhance(ApplicationClass applicationClass) throws Exception {
        Class<?>[] enhancers = new Class[]{
            SigEnhancer.class,
            ControllersEnhancer.class,
            ContinuationEnhancer.class,
            MailerEnhancer.class,
            PropertiesEnhancer.class,
            LocalvariablesNamesEnhancer.class
        };
        for (Class<?> enhancer : enhancers) {
            try {
                long start = System.currentTimeMillis();
                ((Enhancer) enhancer.newInstance()).enhanceThisClass(applicationClass);
                if (Logger.isTraceEnabled()) {
                    Logger.trace("%sms to apply %s to %s", System.currentTimeMillis() - start, enhancer.getSimpleName(), applicationClass.name);
                }
            } catch (Exception e) {
                throw new UnexpectedException("While applying " + enhancer + " on " + applicationClass.name, e);
            }
        }
    }
}