package hirondelle.fish.access.user;

import java.util.regex.*;
import java.util.logging.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.model.Validator;
import hirondelle.web4j.security.SafeText;
import static hirondelle.web4j.util.Consts.FAILS;

/**
 User for the Fish and Chips Club.
  
 <P>When first added, the User has a fixed, inconvenient password, which  
 they are encouraged to change. This is not enforced by the application, however.

 <P>The password is "hashed". 
 A one-way hash function ensures the password is NOT stored in cleartext.
 When the container enforces a security-constraint defined in <tt>web.xml</tt>, the container 
 must be instructed to call the exact same hash function (SHA-1), in order to match the database.

 <P>Performing the hash here, instead of in the database, provides independance from 
 database implementations of hash functions (or lack thereof). 

 <P>Note that a one-way <em>hash</em> function is used here, NOT an <em>encryption</em> 
 function: encrypted items are ultimately intended for decryption. Here, no decryption is ever attempted. 
 In fact, the whole point of using a hash function is that it is nearly impossible to 
 deduce the password from the hashed value itself.
*/
public final class User {

  /** Verify presence of hash function in current JRE.  */
  static {
    MessageDigest sha = null;
    try {
      sha = MessageDigest.getInstance("SHA-1");
    }
    catch (NoSuchAlgorithmException ex){
      throw new RuntimeException("Unable to hash passwords. MessageDigest class cannot find the SHA-1 hash function.");
    }
  }
  
  /**
   Constructor taking a password that is already hashed.
  
   <P>This constructor is used when retrieving from the database, where passwords are 
   already stored in a hashed form.
   
   @param aName user name (required), 6..50 characters, no spaces.
   @param aHashedPassword <em>hashed</em> user password (required), 6..50 characters, no spaces, and never the 
   same as the user name.
  */
  public User(SafeText aName, SafeText aHashedPassword) throws ModelCtorException {
    fName = aName;
    fHashedPassword = aHashedPassword;
    validateState();
  }
  
  /**
   Factory method for a new {@link User}, with a fixed, initial password. 
   
   <P><span class="highlight">In this implementation, the initial password is very
   long, and thus inconvenient to use. New users are encouraged to change it immediately, 
   upon first use. This is not enforced, however.</span> 
  */ 
  public static User forNewUserOrPasswordReset(SafeText aName) throws ModelCtorException {
    return new User(aName, SafeText.from(hash(MAGIC_INITIAL_PASSWORD)));
  }
  
  /**
   Factory method for a new {@link User}, reflecting a new password.
   
   <P>The arguments must pass the same constraints as {@link #User(SafeText, SafeText)}. 
  */
  public static User forPasswordChange(SafeText aName, SafeText aClearTextPassword) throws ModelCtorException {
    User temp = new User(aName, aClearTextPassword); //will throw ex if fails
    return new User(aName, SafeText.from(hash(aClearTextPassword.getRawString())));
  }
  
  /** Return the user name passed to the constructor.  */
  public SafeText getName() {  
    return fName; 
  }
  
  /** Return the <em>hashed</em> password (never cleartext).  */
  public SafeText getPassword() {  
    return fHashedPassword;  
  }
  
  /**
   Return <tt>true</tt> only if the password matches the initial, reset value.
   
   <P>Passwords which match the initial, reset value should be changed by the end user.
  */
  public boolean isResetValue(){
    return hash(MAGIC_INITIAL_PASSWORD).equalsIgnoreCase(fHashedPassword.getRawString());
  }
  
  /** Intended for debugging only. The return value will mask the password. */
  @Override public String toString() {
    return hirondelle.fish.access.user.User.class + " User Name : " + fName + " Password : ****"; 
  }

  @Override public boolean equals( Object aThat ) {
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if ( result == null ){
      User that = (User) aThat;
      result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
    }
    return result;    
  }

  @Override public int hashCode() {
    if ( fHashCode == 0 ) {
      fHashCode = ModelUtil.hashCodeFor(getSignificantFields());
    }
    return fHashCode;
  }
   
  //PRIVATE//
  private final SafeText fName;
  private final SafeText fHashedPassword;
  private int fHashCode;
  
  /**
   Not public, since could be used in an attack, if visible in javadoc.
   
   <P>HIGHLY RECOMMENDED that this value be changed for your application, but that the basic idea be 
   preserved : this initial/reset password should be inconvenient to type, so that the user will 
   be strongly encouraged to change it as soon as possible. 
  */
  private static final String MAGIC_INITIAL_PASSWORD = "changemetosomethingalotmoreconvenienttotype";
  private static final Pattern ACCEPTED_PATTERN = Pattern.compile("(?:\\S){6,50}");
  private static final Logger fLogger = Util.getLogger(User.class);

  private void validateState() throws ModelCtorException {
    ModelCtorException ex = new ModelCtorException();
    Validator validPattern = Check.pattern(ACCEPTED_PATTERN);
    if ( FAILS == Check.required(fName, validPattern) ) {
      ex.add("Name is required, 6..50 chars, no spaces.");
    }
    if ( FAILS == Check.required(fHashedPassword, validPattern)) {
      ex.add("Password is required, 6..50 chars, no spaces.");
    }
    if( fName != null && fHashedPassword != null ) {
      if( fName.getRawString().equalsIgnoreCase(fHashedPassword.getRawString()) ){
        ex.add("Password cannot be the same as the user name.");
      }
    }
    if ( ! ex.isEmpty() ) throw ex;
  }
   
  private Object[] getSignificantFields(){
    return new Object[] {fName, fHashedPassword};
  }

  /**
   The static initializer of this class will barf if the hash function is not present.
  */
  private static String hash(String aCleartext) {
    String result = null;
    
    MessageDigest sha = null;
    try {
      sha = MessageDigest.getInstance("SHA-1");
    }
    catch (NoSuchAlgorithmException ex){
      fLogger.severe("Cannot find SHA-1 hash function.");
    }

    if (sha != null){
      byte[] digest =  sha.digest( aCleartext.getBytes() );
      result = hexEncode(digest);
    }
    else {
      result = aCleartext;
    }
    return result;
  }
  
  /**
   The byte[] returned by MessageDigest does not have a nice
   textual representation, so some form of encoding is usually performed.
  
   This implementation follows the example of David Flanagan's book
   "Java In A Nutshell", and converts a byte array into a String
   of hex characters.
  */
  private static String hexEncode( byte[] aInput){
    StringBuffer result = new StringBuffer();
    char[] digits = {'0', '1', '2', '3', '4','5','6','7','8','9','a','b','c','d','e','f'};
    for ( int idx = 0; idx < aInput.length; ++idx) {
      byte b = aInput[idx];
      result.append( digits[ (b&0xf0) >> 4 ] );
      result.append( digits[ b&0x0f] );
    }
    return result.toString();
  }  
  
  /** Informal test harness. Change to public to exercise. */
  private static void main(String[] args){
    System.out.println("Hash: " + hash("testtest"));
  }
}