package edu.stanford.nlp.sempre.interactive;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpCookie;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import com.google.common.base.Strings;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import edu.stanford.nlp.sempre.Derivation;
import edu.stanford.nlp.sempre.ErrorValue;
import edu.stanford.nlp.sempre.Example;
import edu.stanford.nlp.sempre.Json;
import edu.stanford.nlp.sempre.Master;
import edu.stanford.nlp.sempre.Session;
import edu.stanford.nlp.sempre.StringValue;
import edu.stanford.nlp.sempre.Value;
import fig.basic.IOUtils;
import fig.basic.LogInfo;
import fig.basic.MapUtils;
import fig.basic.Option;

/**
 * JsonServer, interactive learning queries run through this. All the logs are
 * handled here.
 *
 * @author sidaw
 */
public class InteractiveServer {
  public static class Options {
    @Option
    public int port = 8400;
    @Option
    public int numThreads = 4;
    @Option
    public int verbose = 1;
    @Option
    public int maxCandidates = Integer.MAX_VALUE;
    @Option
    public String queryLogPath = "./int-output/query.log";
    @Option
    public String responseLogPath = "./int-output/response.log";
    @Option
    public String fullResponseLogPath;
    @Option
    public int maxExecutionTime = 10; // in seconds
  }

  public static Options opts = new Options();
  private static Object queryLogLock = new Object();
  private static Object responseLogLock = new Object();
  private static AtomicLong queryCounter = new AtomicLong();
  Master master;

  class Handler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) {
      try {
        new ExchangeState(exchange);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }

  class ExchangeState {
    // Input
    HttpExchange exchange;
    Map<String, String> reqParams = new HashMap<>();
    String remoteHost;

    // For header
    HttpCookie cookie;
    boolean isNewSession;

    // For writing main content

    public ExchangeState(HttpExchange exchange) throws IOException {
      this.exchange = exchange;

      URI uri = exchange.getRequestURI();
      this.remoteHost = exchange.getRemoteAddress().getHostName();

      // Don't use uri.getQuery: it can't distinguish between '+' and '-'
      String[] tokens = uri.toString().split("\\?");
      if (tokens.length == 2) {
        for (String s : tokens[1].split("&")) {
          String[] kv = s.split("=", 2);
          try {
            String key = URLDecoder.decode(kv[0], "UTF-8");
            String value = URLDecoder.decode(kv[1], "UTF-8");
            // logs("%s => %s", key, value);
            reqParams.put(key, value);
          } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
          }
        }
      }
      // do not decode sessionId, keep it filename and lisptree friendly
      String sessionId = URLEncoder.encode(MapUtils.get(reqParams, "sessionId", ""), "UTF-8");
      if (sessionId != null) {
        isNewSession = false;
      } else {
        isNewSession = true;
      }

      if (opts.verbose >= 2)
        logs("GET %s from %s (%ssessionId=%s)", uri, remoteHost, isNewSession ? "new " : "", sessionId);

      String uriPath = uri.getPath();
      if (uriPath.equals("/"))
        uriPath += "index.html";
      if (uriPath.equals("/sempre")) {
        handleQuery(sessionId);
      } else {
        // getFile(opts.basePath + uriPath); security
      }
      exchange.close();
    }

    String getMimeType(String path) {
      String[] tokens = path.split("\\.");
      String ext = tokens[tokens.length - 1];
      if (ext.equals("html"))
        return "text/html";
      if (ext.equals("css"))
        return "text/css";
      if (ext.equals("jpeg"))
        return "image/jpeg";
      if (ext.equals("gif"))
        return "image/gif";
      return "text/plain";
    }

    void setHeaders(String mimeType) throws IOException {
      Headers headers = exchange.getResponseHeaders();
      headers.set("Content-Type", mimeType);
      headers.set("Access-Control-Allow-Origin", "*");
      if (isNewSession && cookie != null)
        headers.set("Set-Cookie", cookie.toString());
      exchange.sendResponseHeaders(200, 0);
    }

    Map<String, Object> makeJson(Master.Response response) {
      Map<String, Object> json = new HashMap<String, Object>();
      json.put("stats", response.stats);

      if (response.lines != null) {
        json.put("lines", response.lines);
      }
      if (response.getExample() != null) {
        List<Object> items = new ArrayList<Object>();
        json.put("candidates", items);
        List<Derivation> allCandidates = response.getExample().getPredDerivations();
        Derivation.sortByScore(allCandidates);
        if (allCandidates != null) {
          if (allCandidates.size() >= InteractiveServer.opts.maxCandidates) {
            response.lines.add(String.format("Exceeded max options: (current: %d / max: %d) ", allCandidates.size(),
                InteractiveServer.opts.maxCandidates));
            allCandidates = allCandidates.subList(0, InteractiveServer.opts.maxCandidates);
          }

          for (Derivation deriv : allCandidates) {
            Map<String, Object> item = new HashMap<String, Object>();
            Value value = deriv.getValue();
            if (value instanceof StringValue)
              item.put("value", ((StringValue) value).value);
            else if (value instanceof ErrorValue)
              item.put("value", ((ErrorValue) value).sortString());
            else if (value != null)
              item.put("value", value.sortString());
            else
              item.put("value", "[[]]");
            item.put("score", deriv.getScore());
            item.put("prob", deriv.getProb());
            item.put("anchored", deriv.allAnchored); // used only anchored rules
            item.put("formula", deriv.formula.toLispTree().toString());
            items.add(item);
          }
        }
      }

      return json;
    }

    // This should be concurrent
    Master.Response processQuery(Session session, String query) {
      String message = null;
      Master.Response response = master.new Response();
      ExecutorService executor = Executors.newSingleThreadExecutor();
      Future<Master.Response> future = executor.submit(() -> master.processQuery(session, query));
      long startTime = System.nanoTime();
      try {
        // most exceptions should be handled in InteractiveMaster
        // so the response can be more specific
        response = future.get(opts.maxExecutionTime, TimeUnit.SECONDS);
      } catch (Throwable e) {
        e.printStackTrace();
        message = e.toString();
        response.lines.add(String.format("Exceeded the maximum allowed time: %ss", opts.maxExecutionTime));
        response.stats.put("uncaught_error", message);
        LogInfo.flush();
        LogInfo.resetInfos();
      } finally {
        future.cancel(true);
        executor.shutdown();
        long endTime = System.nanoTime();
        response.stats.put("walltime", (endTime - startTime) / 1.0e9);
        response.stats.put("count", queryCounter.get());
      }
      return response;
    }

    void handleQuery(String sessionId) throws IOException {
      String query = reqParams.get("q");
      long queryNumber = queryCounter.incrementAndGet();
      Session session = master.getSession(sessionId);
      session.reqParams = reqParams;
      session.remoteHost = remoteHost;
      session.format = "json";

      LocalDateTime queryTime = LocalDateTime.now();
      synchronized (queryLogLock) { // write the query log
        Map<String, Object> jsonMap = new LinkedHashMap<>();
        jsonMap.put("count", queryNumber);
        jsonMap.put("q", query);
        // jsonMap.put("remote", remoteHost);
        // jsonMap.put("time", queryTime.toString());
        jsonMap.put("sessionId", sessionId);
        reqParams.remove("q");
        jsonMap.putAll(reqParams);
        if (session.isLogging()) {
          logLine(opts.queryLogPath, Json.writeValueAsStringHard(jsonMap));
        } else {
          logLine(opts.queryLogPath + ".sandbox", Json.writeValueAsStringHard(jsonMap));
        }
      }

      // If JSON, don't store cookies.

      if (query == null)
        query = "null";
      logs("Server.handleQuery %s: %s", session.id, query);

      // Print header
      setHeaders("application/json");

      Master.Response masterResponse = null;
      if (query != null) {
        masterResponse = processQuery(session, query);
      }

      Map<String, Object> responseMap = null;
      {
        PrintWriter out = new PrintWriter(new OutputStreamWriter(exchange.getResponseBody()));
        if (masterResponse != null) {
          // Render answer
          Example ex = masterResponse.getExample();
          responseMap = makeJson(masterResponse);
          out.println(Json.writeValueAsStringHard(responseMap));
        }
        out.close();
      }

      synchronized (responseLogLock) { // write the response log log
        Map<String, Object> jsonMap = new LinkedHashMap<>();
        LocalDateTime responseTime = LocalDateTime.now();
        // jsonMap.put("responseTime", responseTime.toString());
        jsonMap.put("time", queryTime.toString());
        jsonMap.put("ms", String.format("%.3f", java.time.Duration.between(queryTime, responseTime).toNanos() / 1.0e6));
        jsonMap.put("sessionId", sessionId);
        jsonMap.put("q", query); // backwards compatibility...
        jsonMap.put("lines", responseMap.get("lines"));
        if (session.isLogging()) {
          logLine(opts.responseLogPath, Json.writeValueAsStringHard(jsonMap));
          if (!Strings.isNullOrEmpty(opts.fullResponseLogPath)) {
            jsonMap.put("candidates", responseMap.get("candidates").toString());
            logLine(opts.fullResponseLogPath, Json.writeValueAsStringHard(jsonMap));
          }
        } else {
          logLine(opts.responseLogPath + ".sandbox", Json.writeValueAsStringHard(jsonMap));
          if (!Strings.isNullOrEmpty(opts.fullResponseLogPath)) {
            jsonMap.put("candidates", responseMap.get("candidates"));
            logLine(opts.fullResponseLogPath + ".sandbox", Json.writeValueAsStringHard(jsonMap));
          }
          // LogInfo.log(Json.writeValueAsStringHard(jsonMap));
        }
      }
    }

    void logLine(String path, String line) {
      PrintWriter out;
      try {
        out = IOUtils.openOutAppend(path);
        out.println(line);
        out.close();
      } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }

  private void logs(String s, Object... args) {
  };

  public InteractiveServer(Master master) {
    this.master = master;
  }

  public void run() {
    try {
      String hostname = fig.basic.SysInfoUtils.getHostName();
      HttpServer server = HttpServer.create(new InetSocketAddress(opts.port), 10);
      // generous timeout here
      ExecutorService pool = new ThreadPoolExecutor(opts.numThreads, opts.numThreads, 120, TimeUnit.SECONDS,
          new LinkedBlockingQueue<Runnable>());
      // Executors.newFixedThreadPool(opts.numThreads);
      server.createContext("/", new Handler());
      server.setExecutor(pool);
      server.start();
      LogInfo.logs("JSON Server (%d threads) started at http://%s:%s/sempre", opts.numThreads, hostname, opts.port);
      LogInfo.log("Press Ctrl-D to terminate.");
      LogInfo.begin_threads();
      while (LogInfo.stdin.readLine() != null) {
      }
      LogInfo.log("Shutting down server...");
      server.stop(0);
      LogInfo.log("Shutting down executor pool...");
      pool.shutdown();
      LogInfo.end_threads();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}
