package hirondelle.web4j.webmaster;

import static hirondelle.web4j.util.Consts.FAILS;
import java.util.regex.Pattern;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.model.ModelCtorException;
import java.util.logging.*;

import hirondelle.web4j.util.Util;
import java.util.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import static hirondelle.web4j.util.Consts.NOT_FOUND;
import static hirondelle.web4j.util.Consts.EMPTY_STRING;
import static hirondelle.web4j.util.Consts.NEW_LINE;
import hirondelle.web4j.webmaster.TroubleTicket;
import hirondelle.web4j.util.Stopwatch;

/**  Detects problems by pinging a target URL every few minutes. */
final class BadResponseDetector extends TimerTask {
  
  /**
   Build a <tt>BadResponseDetector</tt> using a setting of the same name in 
   <tt>web.xml</tt>.
   
   These settings are simply passed to the regular 
   {@link #BadResponseDetector(String, int, int)} constructor.
  */
  static BadResponseDetector getInstanceUsing(String aConfig){
    Scanner parser = new Scanner(aConfig);
    parser.useDelimiter(",\\s*");
    String targetURL = parser.next();
    int pingFrequency = parser.nextInt();
    int timeout = parser.nextInt();
    return new BadResponseDetector(targetURL, pingFrequency, timeout);
  }
  
  /**
   Constructor.
    
   @param aTargetURL the URL to be tested periodically. Required, starts with 'http://'. Must be able 
   to form a {@link java.net.URL}.
   @param aPingFrequency number of <em>minutes</em> between each test (1..60). Required.
   @param aTimeout number of <i>seconds</i> to wait for a response (1..60). Required.  
  */
  BadResponseDetector(String aTargetURL, int aPingFrequency, int aTimeout) {
    fTargetURL = aTargetURL;
    fPingFrequency = aPingFrequency;
    fTimeout = aTimeout;
    validateState();
  }
  
  /** 
    Ping the target URL by attempting to fetch its HTTP header. 
    Email the webmaster if the response status code in the header is 400 or more, or if the ping times out.  
  */
  @Override public void run() {
    fLogger.fine("Pinging the target URL " + Util.quote(fTargetURL));
    Stopwatch stopwatch = new Stopwatch();
    String problem = EMPTY_STRING;
    String pageContent = null;
    int statusCode = 0;
    URLConnection connection = null;
    try {
      stopwatch.start();
      URL target = new URL(fTargetURL);
      connection = target.openConnection();
      connection.setConnectTimeout(fTimeout*1000);
      connection.connect();
      statusCode = extractStatusCode(connection);
      pageContent = getEntireContent(connection);
      if( statusCode == NOT_FOUND ) {
        problem = "Cannot extract status code for : " + fTargetURL;
      }
      else if ( isError(statusCode) ) {
        problem = "URL " + fTargetURL + " is returning an error status code : " + statusCode; 
      }
    }
    catch(MalformedURLException ex){
      fLogger.fine("Malformed URL : " + fTargetURL); //this is checked in ctor
    }
    catch(SocketTimeoutException ex){
      problem = "Connection timed out.";
    }
    catch (IOException ex) {
      problem = "Cannot open connection to the URL : " + fTargetURL;
    }
    
    stopwatch.stop();
    long BILLION = 1000 * 1000 * 1000;
    if( stopwatch.toValue() > (fTimeout * BILLION)) { //nanos
      problem = problem + "Response took too long : " + stopwatch;
    }
    
    if( Util.textHasContent(problem) ) {
      mailTroubleTicket(problem);
    }
    else {
      fLogger.info("No problem detected. Status code : " + statusCode + ". Response time : " + stopwatch + ". Content length: " + pageContent.length());
    }
  }

  /** Return the target URL passed to the constructor. */
  String getTargetURL(){  return fTargetURL;  }
  
  /** Return the ping frequency passed to the constructor. */
  int getPingFrequency(){   return fPingFrequency; }
  
  /** Return the timeout passed to the constructor. */
  int getTimeout(){   return fTimeout; }
  
  // PRIVATE //
  private final String fTargetURL;
  private final int fPingFrequency;
  private final int fTimeout;
  private static final Pattern HTTP_TARGET = Pattern.compile("http://.*");
  private static final int ENTIRE_FIRST_LINE = 0;
  private static final int STATUS_CODE = 1;
  private static final String END_OF_INPUT = "\\Z";  
  //note the user of a logger attached to a public class
  private static final Logger fLogger = Util.getLogger(PerformanceMonitor.class);

  private void validateState(){
    ModelCtorException ex = new ModelCtorException();
    
    if ( FAILS == Check.required(fTargetURL, Check.pattern(HTTP_TARGET)) ) {
      ex.add("Target URL is required, must start with 'http://'.");
    }
    if( Util.textHasContent(fTargetURL)) {
      try {
        URL testUrl = new URL(fTargetURL);
      }
      catch(MalformedURLException exception){
        ex.add("Target URL is malformed.");
      }
    }
    
    if ( FAILS == Check.required(fPingFrequency, Check.range(1,60))) {
      ex.add("Ping Frequency is required, must be in range 1..60 minutes.");
    }
    if ( FAILS == Check.required(fTimeout, Check.range(1,60))) {
      ex.add("Timeout is required, must be in range 1..60 seconds.");
    }
    
    if ( ! ex.isEmpty() ) {
      throw new IllegalArgumentException("Cannot construct BadResponseDetector : " + ex.toString(), ex);
    }
  }
  
  /** Returns -1 if no status code can be detected.  */
  private int extractStatusCode(URLConnection aURLConnection){
    //Typical value, first line of response : 'HTTP/1.1 200 OK'
    int result = -1;
    String firstLine = aURLConnection.getHeaderField(ENTIRE_FIRST_LINE);
    StringTokenizer parser = new StringTokenizer(firstLine, " ");
    List<String> items = new ArrayList<String>();
    while (parser.hasMoreTokens()) {
      items.add(parser.nextToken());
    }
    String status = items.get(STATUS_CODE);
    if( Util.textHasContent(status) ) {
      try {
        result = Integer.valueOf(status);
      }
      catch (NumberFormatException ex){
        //do nothing - return value will reflect the inability to detect status code
      }
    }
    return result;
  }
  
  private boolean isError(int aStatusCode) {
    return aStatusCode >= 400;
  }
  
  private void mailTroubleTicket(String aProblem) {
    StringBuilder problem = new StringBuilder();
    problem.append("Bad response detected." + NEW_LINE);
    problem.append(NEW_LINE);
    problem.append("URL : " + fTargetURL  + NEW_LINE);
    problem.append("Problem : " + aProblem);
    fLogger.severe(problem.toString());
    TroubleTicket ticket = new TroubleTicket(problem.toString());
    try {
      ticket.mailToRecipients();
    }
    catch(AppException ex){
      fLogger.severe("Cannot send email regarding bad response.");
    }
  }
  
  private String getEntireContent(URLConnection aConnection) throws IOException {
    String result = null;
    Scanner scanner = new Scanner(aConnection.getInputStream());
    scanner.useDelimiter(END_OF_INPUT);
    result = scanner.next();
    return result;
  }
}
