/* See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Esri Inc. 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.
 */
package com.esri.gpt.sdisuite;
import com.esri.gpt.framework.context.RequestContext;
import com.esri.gpt.framework.security.identity.NotAuthorizedException;
import com.esri.gpt.framework.security.principal.User;
import com.esri.gpt.framework.util.Val;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.rmi.ServerException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringEscapeUtils;

/**
 * Callback servlet when certain links associated with a discovered resource are clicked.
 */
public class IntegrationLinkServlet extends HttpServlet {
  
  /** The Logger. */
  private static Logger LOGGER = Logger.getLogger(IntegrationLinkServlet.class.getName());
  
  /** User attribute key holding the SAML security token */
  private static String SDI_SECURITY_TOKEN = "sdi.security.token";

  @Override
  public void destroy() {
    super.destroy();
  }

  @Override
  public void init() throws ServletException {
    super.init();
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    this.execute(request,response);
  }
  
  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    this.execute(request,response);
  }
  
  /**
   * Processes the HTTP request.
   * @param request the HTTP request
   * @param response HTTP response
   * @throws ServletException if an exception occurs
   * @throws IOException if an I/O exception occurs
   */
  private void execute(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    LOGGER.finer("Query string="+request.getQueryString());
    
    // initialize parameters execute the appropriate request
    String lcb = request.getParameter("lcb");
    String act = request.getParameter("act");
    if ((lcb != null) && lcb.equals("true")) {
      this.executeLicenseCallback(request,response);
    } else if (act != null) {
      this.executeClick(request,response);
    } else {
      this.writeError(request,response,"No action was specified.",null);
    }
  }
  
  /**
   * Processes a click on a resource link.
   * @param request the HTTP request
   * @param response HTTP response
   * @throws ServletException if an exception occurs
   * @throws IOException if an I/O exception occurs
   */
  private void executeClick(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    LOGGER.finer("Processing resource link click...");
  
    // initialize parameters
    String act = request.getParameter("act");
    String fwd = request.getParameter("fwd");
    String resourceUrl = null;
    String addToMapHint = null;
    
    // determine the resource URL to be checked
    if (act != null) {
      if (act.equals("open")) {
        resourceUrl = fwd;
      } else if (act.equals("preview")) {
        Map<String,String> params = this.gatherParams(fwd);
        resourceUrl = params.get("url");
      } else if (act.equals("addToMap")) {
        Map<String,String> params = this.gatherParams(fwd);
        resourceUrl = params.get("resource");
        if ((resourceUrl != null) && (resourceUrl.length() > 0)) {
          if (!resourceUrl.toLowerCase().startsWith("http")) {
            int idx = resourceUrl.indexOf(":");
            if (idx != -1) {
              addToMapHint = resourceUrl.substring(0,idx);
              resourceUrl = resourceUrl.substring(idx+1);
            }
          }
        }
      } else {
        resourceUrl = fwd;
      }
    }
    
    // check the resource URL
    if ((resourceUrl != null) && (resourceUrl.length() > 0)) {
      LOGGER.finer("Checking resource URL: "+resourceUrl);
      RequestContext rc = null;
      String samlToken = null;
      try {
        rc = RequestContext.extract(request);
        User user = rc.getUser();
        IntegrationResponse resp = null;
        IntegrationContextFactory icf = new IntegrationContextFactory();
        if (icf.isIntegrationEnabled()) {
          IntegrationContext ic = icf.newIntegrationContext();
          if (ic != null) {
            resp = ic.checkUrl(resourceUrl,user,null,null,null);
          
            if ((resp != null) && resp.isLicensed()) {
              if ((user != null) && (user.getProfile() != null)) {
                if (user.getProfile().containsKey(SDI_SECURITY_TOKEN)) {
                  samlToken = ic.getBase64EncodedToken(user);
                }
              }
            }
            
          }
        }
        
        // handle a licensed URL
        if ((resp != null) && resp.isLicensed()) {
          String wssUrl = resp.getUrl();
          String licenseSelectionUrl = resp.getLicenseSelectionClientUrl();
          if ((licenseSelectionUrl != null) && (licenseSelectionUrl.length() > 0) &&
              (wssUrl != null) && (wssUrl.length() > 0)) {
            
            // save resource URL parameters
            String wssUrlParams = null;
            int idx = wssUrl.indexOf("?");
            if (idx != -1) {
              wssUrlParams = wssUrl.substring(idx+1).trim();
              wssUrl = wssUrl.substring(0,idx);
            }
            
            // make the callback URL
            String callbackUrl = RequestContext.resolveBaseContextPath(request)+"/link";
            callbackUrl += "?lcb="+URLEncoder.encode("true","UTF-8");
            callbackUrl += "&act="+URLEncoder.encode(act,"UTF-8");
            callbackUrl += "&fwd="+URLEncoder.encode(fwd,"UTF-8");
            if ((wssUrlParams != null) && (wssUrlParams.length() > 0)) {
              callbackUrl += "&rqs="+URLEncoder.encode(wssUrlParams,"UTF-8");
            }
            if ((addToMapHint != null) && (addToMapHint.length() > 0)) {
              callbackUrl += "&atmh="+URLEncoder.encode(addToMapHint,"UTF-8");
            }

            // make the full license selection URL (can set &embedded=true)
            licenseSelectionUrl += "?WSS="+URLEncoder.encode(wssUrl,"UTF-8");
            licenseSelectionUrl += "&returnURL="+URLEncoder.encode(callbackUrl,"UTF-8");
            
            // if user is logged in, 
            //   return an HTML response that immediately posts the SAML token to the license selection URL
            // else
            //   forward to the licenseSelectionUrl            
            if ((samlToken != null) && (samlToken.length() > 0)) {
              LOGGER.finer("Sending POST redirect with token to: " + licenseSelectionUrl);
              fwd = null;
              String title = "License redirect SSO page";
              StringBuilder sbHtml = new StringBuilder();
              sbHtml.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
              sbHtml.append("\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">");
              sbHtml.append("\r\n<head>");
              sbHtml.append("\r\n<title>").append(Val.escapeXmlForBrowser(title)).append("</title>");
              sbHtml.append("\r\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"/>");
              sbHtml.append("\r\n<meta http-equiv=\"Expires\" content=\"Mon, 01 Jan 1990 00:00:01 GMT\"/>");
              sbHtml.append("\r\n<meta http-equiv=\"pragma\" content=\"no-cache\"/>");
              sbHtml.append("\r\n<meta http-equiv=\"cache-control\" content=\"no-cache\"/>");
              sbHtml.append("\r\n<meta name=\"robots\" content=\"noindex\"/>");
              sbHtml.append("\r\n</head>");
              sbHtml.append("\r\n<body onload=\"document.forms[0].submit();\">");
              sbHtml.append("\r\n<form method=\"post\" action=\"").append(Val.escapeXmlForBrowser(licenseSelectionUrl)).append("\">");
              sbHtml.append("\r\n<input type=\"hidden\" name=\"ticket\" value=\"").append(Val.escapeXmlForBrowser(samlToken)).append("\"/>");
              sbHtml.append("\r\n</form>");
              sbHtml.append("\r\n</body>");
              sbHtml.append("\r\n</html>");
              this.writeCharacterResponse(response,sbHtml.toString(),"UTF-8","text/html; charset=UTF-8");
            } else {
              fwd = licenseSelectionUrl;
            }

          } else {
            String msg = "IntegrationResponse isLicensed() was true, but getLicenseSelectionClientUrl() was empty.";
            LOGGER.warning(msg);
          }
          
        // handle a secured URL
        } else if ((resp != null) && resp.isSecured()) {
          String securedUrl = resp.getUrl();
          if ((securedUrl != null) && !securedUrl.equals(resourceUrl)) {
            if (act.equals("open")) {
              fwd = securedUrl;
            } else if (act.equals("preview")) {
              fwd = this.replaceParam(fwd,"url",securedUrl);
            } else if (act.equals("addToMap")) {
              if ((addToMapHint != null) && (addToMapHint.length() > 0)) {
                securedUrl = addToMapHint+":"+securedUrl;
              }
              fwd = this.replaceParam(fwd,"resource",securedUrl);
            } else {
              fwd = securedUrl;
            }
          }
        }
        
      } catch (NotAuthorizedException e) {
        String msg = "Error checking resource URL";
        LOGGER.log(Level.SEVERE,msg,e);
        this.writeError(request,response,msg+": "+e.toString(),null);
        return;
      } catch (Exception e) {
        String msg = "Error checking resource URL";
        LOGGER.log(Level.SEVERE,msg,e);
        this.writeError(request,response,msg+": "+e.toString(),null);
        return;
      } finally {
        if (rc != null) rc.onExecutionPhaseCompleted();
      }
    }

    // send the redirect
    if ((fwd != null) && (fwd.length() > 0)) {
      LOGGER.finer("Redirecting to: "+fwd);
      if (!Val.isUrl(fwd)) {
          throw new ServerException("Invalid redirect URL.");
      }
      response.sendRedirect(fwd);
    }
  }
  
  /**
   * Processes the response of a license selection.
   * @param request the HTTP request
   * @param response HTTP response
   * @throws ServletException if an exception occurs
   * @throws IOException if an I/O exception occurs
   */
  private void executeLicenseCallback(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    LOGGER.finer("Processing license selection callback...");
    
    // this code is a modification of Oliver's returnAction.jsp
    
    // Geoportal parameters 
    String act = request.getParameter("act");
    if ((act == null) || (act.length() == 0)) act = request.getParameter("amp;act");
    String fwd = request.getParameter("fwd");
    if ((fwd == null) || (fwd.length() == 0)) fwd = request.getParameter("amp;fwd");
    String wssUrlParams = request.getParameter("rqs");
    if ((wssUrlParams == null) || (wssUrlParams.length() == 0)) wssUrlParams = request.getParameter("amp;rqs");
    String addToMapHint = request.getParameter("atmh");
    if ((addToMapHint == null) || (addToMapHint.length() == 0)) addToMapHint = request.getParameter("amp;atmh");
    
    // license parameters
    String lLicenseReference = request.getParameter("licenseReference");
    String lWssUrl = request.getParameter("WSS");
    String lSuccess = request.getParameter("success");
    String lError = request.getParameter("errorMessage");
    
    // check for error
    if ((lSuccess == null) || !lSuccess.equals("true")) {
      if ((lError != null) && (lError.length() > 0)) {
        this.writeError(request,response,"An error occurred while acquiring a license: "+lError,Level.SEVERE);
      } else {
        this.writeError(request,response,"Canceled",null);
      }
    } else if ((act == null) || (act.length() == 0)) {
      this.writeError(request,response,"Empty parameter on license callback (act)",Level.SEVERE);
    } else if ((fwd == null) || (fwd.length() == 0)) {
      this.writeError(request,response,"Empty parameter on license callback (fwd)",Level.SEVERE);
    } else {
      
      // get license id
      String lLicenseId = null;
      IntegrationContextFactory icf = new IntegrationContextFactory();
      if (icf.isIntegrationEnabled()) {
        try {
          LOGGER.finer("Getting license id form licenseReference="+lLicenseReference);
          IntegrationContext ic = icf.newIntegrationContext();
          lLicenseId = ic.getLicenseId(lLicenseReference);
          LOGGER.finer("License id="+lLicenseId);
        } catch (Exception e) {
          String msg = "Error getting license id.";
          LOGGER.log(Level.SEVERE,msg,e);
          this.writeError(request,response,msg+": "+e.toString(),null);
          return;
        }
      }
      
      // change wss to http auth url
      if ((lWssUrl != null) && (lWssUrl.length() > 0)) {
        if ((lLicenseId != null) && (lLicenseId.length() > 0)) {
          lWssUrl = lWssUrl.replace("/WSS","/httpauth/licid-" + lLicenseId);
        }
        if (!lWssUrl.endsWith("?")) {
          lWssUrl += "?";
        }
        if ((wssUrlParams != null) && (wssUrlParams.length() > 0)) {
          lWssUrl += wssUrlParams;
        }
      } else {
        lWssUrl = "";
      }
      LOGGER.finer("Licensed WSS endpoint: "+lWssUrl);

      // determine the redirection endpoint based upon the resource link action
      if ((lWssUrl != null) && (lWssUrl.length() > 0)) {
        if (act.equals("open")) {
          fwd = lWssUrl;
        } else if (act.equals("preview")) {
          fwd = this.replaceParam(fwd,"url",lWssUrl);
        } else if (act.equals("addToMap")) {
          if ((addToMapHint != null) && (addToMapHint.length() > 0)) {
            lWssUrl = addToMapHint+":"+lWssUrl;
          }
          fwd = this.replaceParam(fwd,"resource",lWssUrl);
        } else {
          fwd = lWssUrl;
        }
      }
    
      // send the redirect
      if ((fwd != null) && (fwd.length() > 0)) {
        LOGGER.finer("Redirecting to: "+fwd);
        fwd = Val.stripControls(fwd);
        if (!Val.isUrl(fwd)) {
            throw new ServerException("Invalid redirect URL.");
        }
        response.sendRedirect(fwd);
      }
    }
  }
  
  /**
   * Gather the parameters of a target URL.
   * <br/>This won't work for any URL, we are expecting a URL constructed by a ResourceLinkBuilder.
   * @param url the URL
   * @return a map of the parameters
   * @throws UnsupportedEncodingException if UTF-8 is unsupported (should never happen)
   */
  private Map<String,String> gatherParams(String url) throws UnsupportedEncodingException {
    Map<String,String> params = new HashMap<String,String>();
    int idx = url.indexOf("?");
    if (idx != -1) {
      String queryString = url.substring(idx+1);
      String[] pairs = queryString.split("&");
      for (String pair: pairs) {
        idx = pair.indexOf("=");
        if (idx > 0) {
          String key = pair.substring(0,idx);
          String value = pair.substring(idx+1);
          value = URLDecoder.decode(value,"UTF-8");
          params.put(key,value);
        }
      }
    }
    return params;
  }
  
  /**
   * Replaces a parameter value within a target URL.
   * <br/>This won't work for any URL, we are expecting a URL constructed by a ResourceLinkBuilder.
   * @param url the URL
   * @return the modified URL
   * @throws UnsupportedEncodingException if UTF-8 is unsupported (should never happen)
   */
  private String replaceParam(String url, String paramName, String paramValue) 
    throws UnsupportedEncodingException {
    String result = url;
    int idx = url.indexOf("?");
    if (idx != -1) {
      boolean bFound = true;
      String path = url.substring(0,idx);
      String queryString = url.substring(idx+1);
      String[] pairs = queryString.split("&");
      StringBuilder params = new StringBuilder();
      for (String pair: pairs) {
        idx = pair.indexOf("=");
        if (idx > 0) {
          String key = pair.substring(0,idx);
          String value = pair.substring(idx+1);
          value = URLDecoder.decode(value,"UTF-8");
          if (paramName.equals(key)) {
            value = paramValue;
            bFound = true;
          }
          if (params.length() > 0) params.append("&");
          params.append(URLEncoder.encode(key,"UTF-8"));
          params.append("=");
          params.append(URLEncoder.encode(value,"UTF-8"));
        }
      }
      if (bFound) {
        result = path+"?"+params.toString();
      }
    }
    return result;
  }
  
  /**
   * Writes characters to the response stream.
   * @param response the servlet response
   * @param content the content to write
   * @param charset the response character encoding 
   * @param contentType the response content type
   * @throws IOException if an IO exception occurs
   */
  private void writeCharacterResponse(HttpServletResponse response, 
                                      String content,
                                      String charset,
                                      String contentType) 
    throws IOException {
    PrintWriter writer = null;
    try {
      if (content.length() > 0) {
        response.setCharacterEncoding(charset);
        response.setContentType(contentType);
        writer = response.getWriter();
        writer.write(content);
        writer.flush();
      }
    } finally {
      try {
        if (writer != null) {
          writer.flush();
          writer.close();
        }
      } catch (Exception ef) {
        LOGGER.log(Level.SEVERE,"Error closing PrintWriter.",ef);
      }
    }
  }
  
  /**
   * Writes an error message to the response stream.
   * @param request the HTTP request
   * @param response the servlet response
   * @param message the error message
   * @param level if not null, the service side log file level
   * @throws IOException if an IO exception occurs
   */
  private void writeError(HttpServletRequest request, HttpServletResponse response, String message, Level level)
    throws IOException {
    if (level != null) {
      LOGGER.log(level,message);
    }
    String fwd = request.getContextPath()+"/catalog/tc/error.page?error="+URLEncoder.encode(message,"UTF-8");
    response.sendRedirect(fwd);
    
    /*
    String gptTitle = "Geoportal";
    String pageTitle = "Error";
    String css = request.getContextPath()+"/catalog/skins/themes/red/main.css";
    StringBuilder sbHtml = new StringBuilder();
    sbHtml.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">");
    sbHtml.append("\r\n<html>");
    sbHtml.append("\r\n<head>");
    sbHtml.append("\r\n<title>").append(Val.escapeXmlForBrowser(pageTitle)).append("</title>");
    sbHtml.append("\r\n<link rel=\"stylesheet\" type=\"text/css\" href=\""+Val.escapeXmlForBrowser(css)+"\"/>");
    sbHtml.append("\r\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"/>");
    sbHtml.append("\r\n<meta name=\"robots\" content=\"noindex\"/>");
    sbHtml.append("\r\n</head>");
    sbHtml.append("\r\n<body>");
    sbHtml.append("\r\n<div id=\"gptMainWrap\">");
    sbHtml.append("\r\n  <div id=\"gptBanner\">");
    sbHtml.append("\r\n    <div id=\"gptTitle\">").append(Val.escapeXmlForBrowser(gptTitle)).append("</div>");
    sbHtml.append("\r\n  </div>");
    sbHtml.append("\r\n  <p class=\"errorMessage\">").append(Val.escapeXmlForBrowser(message)).append("</p>");
    sbHtml.append("\r\n</div>");
    sbHtml.append("\r\n</body>");
    sbHtml.append("\r\n</html>");
    this.writeCharacterResponse(response,sbHtml.toString(),"UTF-8","text/html; charset=UTF-8");
    this.writeCharacterResponse(response,message,"UTF-8","text/plain; charset=UTF-8");
    */
  }
  
}
