/*
 * Copyright 2012 LinkedIn Corp.
 *
 * Licensed 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 azkaban.webapp.servlet;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import azkaban.server.AzkabanServer;
import azkaban.server.HttpRequestUtils;
import azkaban.server.session.Session;
import azkaban.utils.Props;
import azkaban.utils.JSONUtils;
import azkaban.utils.WebUtils;
import azkaban.webapp.AzkabanWebServer;
import azkaban.webapp.plugin.PluginRegistry;
import azkaban.webapp.plugin.ViewerPlugin;
import azkaban.webapp.plugin.TriggerPlugin;

/**
 * Base Servlet for pages
 */
public abstract class AbstractAzkabanServlet extends HttpServlet {
  private static final DateTimeFormatter ZONE_FORMATTER = DateTimeFormat
      .forPattern("z");
  private static final String AZKABAN_SUCCESS_MESSAGE =
      "azkaban.success.message";
  private static final String AZKABAN_FAILURE_MESSAGE =
      "azkaban.failure.message";

  private static final long serialVersionUID = -1;
  public static final String DEFAULT_LOG_URL_PREFIX =
      "predefined_log_url_prefix";
  public static final String LOG_URL_PREFIX = "log_url_prefix";
  public static final String HTML_TYPE = "text/html";
  public static final String XML_MIME_TYPE = "application/xhtml+xml";
  public static final String JSON_MIME_TYPE = "application/json";

  protected static final WebUtils utils = new WebUtils();

  private AzkabanServer application;
  private String name;
  private String label;
  private String color;

  private List<ViewerPlugin> viewerPlugins;
  private List<TriggerPlugin> triggerPlugins;

  /**
   * To retrieve the application for the servlet
   *
   * @return
   */
  public AzkabanServer getApplication() {
    return application;
  }

  @Override
  public void init(ServletConfig config) throws ServletException {
    application =
        (AzkabanServer) config.getServletContext().getAttribute(
            AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY);

    if (application == null) {
      throw new IllegalStateException(
          "No batch application is defined in the servlet context!");
    }

    Props props = application.getServerProps();
    name = props.getString("azkaban.name", "");
    label = props.getString("azkaban.label", "");
    color = props.getString("azkaban.color", "#FF0000");

    if (application instanceof AzkabanWebServer) {
      AzkabanWebServer server = (AzkabanWebServer) application;
      viewerPlugins = PluginRegistry.getRegistry().getViewerPlugins();
      triggerPlugins =
          new ArrayList<TriggerPlugin>(server.getTriggerPlugins().values());
    }
  }

  /**
   * Checks for the existance of the parameter in the request
   *
   * @param request
   * @param param
   * @return
   */
  public boolean hasParam(HttpServletRequest request, String param) {
    return HttpRequestUtils.hasParam(request, param);
  }

  /**
   * Retrieves the param from the http servlet request. Will throw an exception
   * if not found
   *
   * @param request
   * @param name
   * @return
   * @throws ServletException
   */
  public String getParam(HttpServletRequest request, String name)
      throws ServletException {
    return HttpRequestUtils.getParam(request, name);
  }

  /**
   * Retrieves the param from the http servlet request.
   *
   * @param request
   * @param name
   * @param default
   *
   * @return
   */
  public String getParam(HttpServletRequest request, String name,
      String defaultVal) {
    return HttpRequestUtils.getParam(request, name, defaultVal);
  }

  /**
   * Returns the param and parses it into an int. Will throw an exception if not
   * found, or a parse error if the type is incorrect.
   *
   * @param request
   * @param name
   * @return
   * @throws ServletException
   */
  public int getIntParam(HttpServletRequest request, String name)
      throws ServletException {
    return HttpRequestUtils.getIntParam(request, name);
  }

  public int getIntParam(HttpServletRequest request, String name, int defaultVal) {
    return HttpRequestUtils.getIntParam(request, name, defaultVal);
  }

  public long getLongParam(HttpServletRequest request, String name)
      throws ServletException {
    return HttpRequestUtils.getLongParam(request, name);
  }

  public long getLongParam(HttpServletRequest request, String name,
      long defaultVal) {
    return HttpRequestUtils.getLongParam(request, name, defaultVal);
  }

  public Map<String, String> getParamGroup(HttpServletRequest request,
      String groupName) throws ServletException {
    return HttpRequestUtils.getParamGroup(request, groupName);
  }

  /**
   * Returns the session value of the request.
   *
   * @param request
   * @param key
   * @param value
   */
  protected void setSessionValue(HttpServletRequest request, String key,
      Object value) {
    request.getSession(true).setAttribute(key, value);
  }

  /**
   * Adds a session value to the request
   *
   * @param request
   * @param key
   * @param value
   */
  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected void addSessionValue(HttpServletRequest request, String key,
      Object value) {
    List l = (List) request.getSession(true).getAttribute(key);
    if (l == null)
      l = new ArrayList();
    l.add(value);
    request.getSession(true).setAttribute(key, l);
  }

  /**
   * Sets an error message in azkaban.failure.message in the cookie. This will
   * be used by the web client javascript to somehow display the message
   *
   * @param response
   * @param errorMsg
   */
  protected void setErrorMessageInCookie(HttpServletResponse response,
      String errorMsg) {
    Cookie cookie = new Cookie(AZKABAN_FAILURE_MESSAGE, errorMsg);
    cookie.setPath("/");
    response.addCookie(cookie);
  }

  /**
   * Sets a message in azkaban.success.message in the cookie. This will be used
   * by the web client javascript to somehow display the message
   *
   * @param response
   * @param errorMsg
   */
  protected void setSuccessMessageInCookie(HttpServletResponse response,
      String message) {
    Cookie cookie = new Cookie(AZKABAN_SUCCESS_MESSAGE, message);
    cookie.setPath("/");
    response.addCookie(cookie);
  }

  /**
   * Retrieves a success message from a cookie. azkaban.success.message
   *
   * @param request
   * @return
   */
  protected String getSuccessMessageFromCookie(HttpServletRequest request) {
    Cookie cookie = getCookieByName(request, AZKABAN_SUCCESS_MESSAGE);

    if (cookie == null) {
      return null;
    }
    return cookie.getValue();
  }

  /**
   * Retrieves a success message from a cookie. azkaban.failure.message
   *
   * @param request
   * @return
   */
  protected String getErrorMessageFromCookie(HttpServletRequest request) {
    Cookie cookie = getCookieByName(request, AZKABAN_FAILURE_MESSAGE);
    if (cookie == null) {
      return null;
    }

    return cookie.getValue();
  }

  /**
   * Retrieves a cookie by name. Potential issue in performance if a lot of
   * cookie variables are used.
   *
   * @param request
   * @return
   */
  protected Cookie getCookieByName(HttpServletRequest request, String name) {
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
      for (Cookie cookie : cookies) {
        if (name.equals(cookie.getName())) {
          return cookie;
        }
      }
    }

    return null;
  }

  /**
   * Creates a new velocity page to use. With session.
   *
   * @param req
   * @param resp
   * @param template
   * @return
   */
  protected Page newPage(HttpServletRequest req, HttpServletResponse resp,
      Session session, String template) {
    Page page = new Page(req, resp, application.getVelocityEngine(), template);
    page.add("azkaban_name", name);
    page.add("azkaban_label", label);
    page.add("azkaban_color", color);
    page.add("utils", utils);
    page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
    page.add("currentTime", (new DateTime()).getMillis());
    if (session != null && session.getUser() != null) {
      page.add("user_id", session.getUser().getUserId());
    }
    page.add("context", req.getContextPath());

    String errorMsg = getErrorMessageFromCookie(req);
    page.add("error_message", errorMsg == null || errorMsg.isEmpty() ? "null"
        : errorMsg);
    setErrorMessageInCookie(resp, null);

    String successMsg = getSuccessMessageFromCookie(req);
    page.add("success_message",
        successMsg == null || successMsg.isEmpty() ? "null" : successMsg);
    setSuccessMessageInCookie(resp, null);

    // @TODO, allow more than one type of viewer. For time sake, I only install
    // the first one
    if (viewerPlugins != null && !viewerPlugins.isEmpty()) {
      page.add("viewers", viewerPlugins);
    }

    if (triggerPlugins != null && !triggerPlugins.isEmpty()) {
      page.add("triggerPlugins", triggerPlugins);
    }

    return page;
  }

  /**
   * Creates a new velocity page to use.
   *
   * @param req
   * @param resp
   * @param template
   * @return
   */
  protected Page newPage(HttpServletRequest req, HttpServletResponse resp,
      String template) {
    Page page = new Page(req, resp, application.getVelocityEngine(), template);
    page.add("azkaban_name", name);
    page.add("azkaban_label", label);
    page.add("azkaban_color", color);
    page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
    page.add("currentTime", (new DateTime()).getMillis());
    page.add("context", req.getContextPath());

    // @TODO, allow more than one type of viewer. For time sake, I only install
    // the first one
    if (viewerPlugins != null && !viewerPlugins.isEmpty()) {
      page.add("viewers", viewerPlugins);
      ViewerPlugin plugin = viewerPlugins.get(0);
      page.add("viewerName", plugin.getPluginName());
      page.add("viewerPath", plugin.getPluginPath());
    }

    if (triggerPlugins != null && !triggerPlugins.isEmpty()) {
      page.add("triggers", triggerPlugins);
    }

    return page;
  }

  /**
   * Writes json out to the stream.
   *
   * @param resp
   * @param obj
   * @throws IOException
   */
  protected void writeJSON(HttpServletResponse resp, Object obj)
      throws IOException {
    writeJSON(resp, obj, false);
  }

  protected void writeJSON(HttpServletResponse resp, Object obj, boolean pretty)
      throws IOException {
    resp.setContentType(JSON_MIME_TYPE);
    JSONUtils.toJSON(obj, resp.getOutputStream(), true);
  }

  /**
   * Retrieve the Azkaban application
   *
   * @param config
   * @return
   */
  public static AzkabanWebServer getApp(ServletConfig config) {
    AzkabanWebServer app =
        (AzkabanWebServer) config.getServletContext().getAttribute(
            AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY);

    if (app == null) {
      throw new IllegalStateException(
          "No batch application is defined in the servlet context!");
    } else {
      return app;
    }
  }

  public static String createJsonResponse(String status, String message,
      String action, Map<String, Object> params) {
    HashMap<String, Object> response = new HashMap<String, Object>();
    response.put("status", status);
    if (message != null) {
      response.put("message", message);
    }
    if (action != null) {
      response.put("action", action);
    }
    if (params != null) {
      response.putAll(params);
    }

    return JSONUtils.toJSON(response);
  }
}
