package hirondelle.web4j.security;

import hirondelle.web4j.model.Id;
import hirondelle.web4j.util.EscapeChars;
import hirondelle.web4j.util.Regex;
import hirondelle.web4j.util.Util;

import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/** Add a nonce to POSTed forms in any 'text/html' response. */
final class CsrfModifiedResponse {

  CsrfModifiedResponse(HttpServletRequest aRequest, HttpServletResponse aResponse){
    fResponse = aResponse;
    fRequest = aRequest;
  }
  
  String addNonceTo(String aUnmodifiedResponse){
    String result = aUnmodifiedResponse;
    if(isServingHtml() && Util.textHasContent(aUnmodifiedResponse))  {
      fLogger.fine("Adding nonce to forms having method=POST, if any.");
      result = addHiddenParamToPostedForms(aUnmodifiedResponse);
    }
    return result;
  }
  
  // PRIVATE
  
  private HttpServletRequest fRequest;
  private HttpServletResponse fResponse;

  /**
    Group 1 is the FORM start tag *plus the body*, and group 2 is the FORM end tag.
    Note the reluctant qualifier for group 2, to ensure multiple forms are not glommed together.
  */
  private static final String REGEX =
     "(<form" + Regex.ALL_BUT_END_OF_TAG +"method=" + Regex.QUOTE + "POST" + Regex.QUOTE + Regex.ALL_BUT_END_OF_TAG + ">"  
     +  Regex.ANY_CHARS + "?)" + 
     "(</form>)"
  ; 
  private static final Pattern FORM_PATTERN = Pattern.compile(REGEX, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);

  private static final String TEXT_HTML = "text/html";
  
  /**
   Problem: this class is package private. If we use the 'regular' logger, named for this class, then the output will not 
   show up. So, we use a logger attached to a closely related, public class. (Neat!) 
  */
  private static final Logger fLogger = Util.getLogger(CsrfFilter.class);

  /** Return true if content-type of reponse is null, or starts with 'text/html' (case-sensitive).  */
  private boolean isServingHtml(){
    String contentType = fResponse.getContentType();
    boolean missingContentType = ! Util.textHasContent(contentType);
    boolean startsWithHTML = Util.textHasContent(contentType) && contentType.startsWith(TEXT_HTML);
    return missingContentType || startsWithHTML;
  }
  
  private String addHiddenParamToPostedForms(String aOriginalInput) {
    StringBuffer result = new StringBuffer();
    Matcher formMatcher = FORM_PATTERN.matcher(aOriginalInput);
    while ( formMatcher.find() ){
      fLogger.fine("Found a POSTed form. Adding nonce.");
      formMatcher.appendReplacement(result, getReplacement(formMatcher));
    }
    formMatcher.appendTail(result);
    return result.toString();
  }
  
  private String getReplacement(Matcher aMatcher){
    //escape, since '$' char may appear in input
    return EscapeChars.forReplacementString(aMatcher.group(1) +  getHiddenInputTag() +  aMatcher.group(2));
  }
  
  private String getHiddenInputTag(){
    return "<input type='hidden' name='" + getHiddenParamName() + "' value='" + getHiddenParamValue().toString() + "'>";
  }
  
  /** 
   Return the form-source id value, stored in the user's session.
   If there is no session, or if there is no form-source id in the session, throw a RuntimeException.  
  */
  private Id getHiddenParamValue(){
    Id result = null;
    boolean DO_NOT_CREATE = false;
    HttpSession session = fRequest.getSession(DO_NOT_CREATE);
    if ( session != null ) {
      result = (Id)session.getAttribute(CsrfFilter.FORM_SOURCE_ID_KEY);
      if( result == null ){
        String message =  "Session exists, but no CSRF token value is stored in the session"; 
        fLogger.severe(message);
        throw new RuntimeException(message);
      }
    }
    else {
      String message = 
        "No session exists! CsrfFilter can only work when a session is present, and the user has logged in. " + 
        "Ensure CsrfFilter is mapped (using url-pattern) only to URLs having mandatory login and/or a valid session."
      ; 
      fLogger.severe(message);
      throw new RuntimeException(message);
    }
    return result;
  }
  
  /** Return the name of the hidden form parameter.  */
  private String getHiddenParamName(){
    return CsrfFilter.FORM_SOURCE_ID_KEY;
  }
}