package page.tools.entity;


import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import org.wikiwebserver.core.Privilege;
import org.wikiwebserver.core.SecurityMan;
import org.wikiwebserver.core.WareHouse;
import org.wikiwebserver.core.WikiMap;
import org.wikiwebserver.handler.http.HTTPException;
import org.wikiwebserver.handler.http.HTTPHandler;
import org.wikiwebserver.handler.http.HTTPHeaders;
import org.wikiwebserver.handler.http.HTTPRequest;
import org.wikiwebserver.handler.http.HTTPResponse;
import org.wikiwebserver.util.MailSender;
import org.wikiwebserver.util.ValidationError;
import org.wikiwebserver.util.ValidationWarning;
import org.wikiwebserver.util.Validator;
import org.wikiwebserver.util.VelocityEnforcement;

import page.config.SiteMonitor;
import page.image.CAPTCHAImage;

/**
 * A User object stores the information required to maintain and 
 * authenticate WikiWebServer users.
 * 
 * A user identity uniquely references a user and is linked to an
 * email address, password hash, privilege level, and a session
 * authorisation code.
 * 
 * Authentication requires an email address and password. After
 * authentication, a session authorisation code is used to keep
 * a user logged in. (maintained in browser cookie)
 * 
 * In an HTTPResponder, make the following call to get the current
 * user making the request:
 * 
 * User currentUser = User.getUserByConnection(conn);
 * 
 * @author Michael Gardiner
 *
 */
public final class User extends Browser {
    
    public static final String TYPE = "User";
    public static final long RESET_PASSWORD_VALIDITY = 4 * 60 * 60 * 1000;
    
    private static final Privilege MODIFY_BY_CODE_PRIVILEGE = Privilege.MODERATOR;    
    
    public String getType() {
        return TYPE;
    }    
    
    private User() {
        super();
    }
    
    private User(String id) {
        super(id);
    }
    
    public String getEmail() {
        String email = (String) get("email");
        return (email != null) ? email : "unknown@wikiwebserver.org";
    }
    
    public static Collection<String> listIds() {
        return listIds(TYPE);
    }      
    
    public static String allocateId() {
        return Storable.allocateId(TYPE);
    }    
    
    public void setEmail(String password, String email) {
        if (this.isCorrectPassword(password)) {
            removeFromIndex(TYPE, "Emails", getEmail());
            putInIndex(TYPE, "Emails", email, getId());
            put("email", email);
        }
    }    
    
    public void setEmail(String email, HTTPRequest request) {
        if (email != null && !email.equals(getEmail())) {
            if (canModifyPrivateData(request)) {
                removeFromIndex(TYPE, "Emails", getEmail());
                putInIndex(TYPE, "Emails", email, getId());
                put("email", email);
            }
        }
    }      
    
    public Privilege getPrivilege() {
        Integer p = (Integer) get("privilege");
        if (p != null && p instanceof Integer) {
            return Privilege.getPrivilegeFromValue(p.intValue());
        }
        return Privilege.GUEST;
    }
    
    public void setPrivilege(Privilege privilege, HTTPRequest request) {
        User operator = getUser(request);
        if (operator.getPrivilege().equals(privilege) ||
            operator.getPrivilege().isAbove(privilege)) {
            put("privilege", new Integer(privilege.getValue()));
        }
        else throw new SecurityException("Permission denied. Can not raise privilege to this level.");
    } 
    
    public void setPrivilege(Privilege privilege, String superPassword) {
        SecurityMan.checkSuperPassword(superPassword);
        
        put("privilege", new Integer(privilege.getValue()));
    }

    public boolean isCorrectPassword(String password) {
        if (password == null) return false;     
        String correctHash = (String) super.get("passwordHash");
        String testHash = getMD5(password);
        if (testHash.equals(correctHash)) return true;
        
        Long resetTime = (Long) super.get("resetPasswordTime"); 
        String resetHash = (String) super.get("resetPasswordHash"); 
        if (resetTime != null && resetHash != null) {
            if (testHash.equals(resetHash)) {
                long time = System.currentTimeMillis();
                if (resetTime.longValue() > time - RESET_PASSWORD_VALIDITY) {    
                    return true;
                }
                else throw new SecurityException("Temporary password has expired");
            }
        }
        
        return false;
    }
    
    public void setPassword(String oldPassword, String newPassword) {
        if (this.isCorrectPassword(oldPassword)) {
            String passwordHash = getMD5(newPassword);
            put("passwordHash", passwordHash);
        }
        else throw new SecurityException("Permission denied. Incorrect old password.");
    }    
    
    public void setPassword(String password, HTTPRequest request) {
        if (canModifyPrivateData(request)) {
            String passwordHash = getMD5(password);
            put("passwordHash", passwordHash);
        }
        else throw new SecurityException("Permission denied. Can not set password.");
    }
    
    public void resetPassword(HTTPHandler conn) throws HTTPException {
        
        Long resetTime = (Long) get("resetPasswordTime"); 
         if (resetTime != null) {
            long time = System.currentTimeMillis();
            if (resetTime.longValue() > time - RESET_PASSWORD_VALIDITY) {
                throw new SecurityException("Password has already been" +
                		                    " reset recently, check your email.");
            }
        }
         
        // Generate new password
        StringBuilder password = new StringBuilder();
        for (int i=0; i<8; i++) {
            int r = rand.nextInt(3);
            if (r == 0) {
                password.append((char) ('a' + rand.nextInt(26)));
            } else if (r == 1) {
                password.append((char) ('A' + rand.nextInt(26)));
            } else if (r == 2) {
                password.append((char) ('0' + rand.nextInt(10)));
            }
        }
        GregorianCalendar cal = new GregorianCalendar();
        cal.add(Calendar.MILLISECOND, (int)RESET_PASSWORD_VALIDITY);
        
        // Email to the user
        String message = "You (or possibly someone else) has requested that your password\n" +
                         "for WikiWebServer is reset.\n\n" +
                         "A temporary password has been created to enable you to log in.\n\n" + 
                         "Temporary password: " + password + "\n\n" + 
                         "This password will expire on " + cal.getTime() + ".\n\n" +
                         "If you did not request a password reset, or have remembered your\n" +
                         "old password you may ignore this email.";                    
        
        String email = getEmail();
        try {
            Validator.validateEmail(email);
        } catch (ValidationError e) {
            throw new HTTPException(500, "Invalid Email", e);
        } catch (ValidationWarning e) {}
        MailSender sender = new MailSender();
        sender.postMail(email, "Temporary password for WikiWebServer", message, 
                        "no-reply-i-am-a-computer@wikiwebserver.org", conn);
        
        // Store as secondary password
        String passwordHash = getMD5(password.toString());
        put("resetPasswordHash", passwordHash);
        Long time = new Long(System.currentTimeMillis());
        put("resetPasswordTime", time);        
    }       
    
    public void startSession(String password, HTTPHandler conn) {
        if (this.isCorrectPassword(password)) {
            int auth = rand.nextInt();
            String hex = Long.toHexString( 0x1000000000000l | auth).substring(1).toUpperCase();            
            super.put("sessionAuth", hex);
            String ip = conn.getSourceAddress();
            String fip = conn.getRequest().getHeaders().getFirst("X-Forwarded-For");
            super.put("sessionIP", ip);
            super.put("sessionFIP", fip);
            HTTPHeaders responseHeaders = conn.getResponse().getHeaders();
            responseHeaders.setResponseCookie("userID", getId());
            responseHeaders.setResponseCookie("sessionAuth", hex);
            // Implant cookie into current request
            HTTPHeaders requestHeaders = conn.getRequest().getHeaders();
            requestHeaders.set("Cookie", requestHeaders.getFirst("Cookie") +
                               "; userID=" + getId());
            requestHeaders.set("Cookie", requestHeaders.getFirst("Cookie") +
                               "; sessionAuth=" + hex);
        }
    }
    
    public void checkAuthorised(HTTPRequest request) {
        
        Map<String, String> requestCookie = request.getHeaders().getRequestCookies();
        String testAuth = requestCookie.get("sessionAuth");        
        String correctAuth = (String) super.get("sessionAuth");
        
        if (testAuth == null || !testAuth.equals(correctAuth)) {
            throw new SecurityException("User not authorised");
        }
        
    }
    
    public void strictCheckAuthorised(HTTPHandler conn) {
        
        Map<String, String> requestCookie = conn.getRequest().getHeaders().getRequestCookies();
        String testAuth = requestCookie.get("sessionAuth");        
        String correctAuth = (String) super.get("sessionAuth");
        
        if (testAuth != null && testAuth.equals(correctAuth)) {
            // No advanced protection against session stealing
            if (getPrivilege().isBelow(Privilege.ADMIN)) return;
            String ip = conn.getSourceAddress();
            String fip = conn.getRequest().getHeaders().getFirst("X-Forwarded-For");
            if (ip.equals((String) super.get("sessionIP"))) { 
                if (fip == (String) super.get("sessionFIP") || 
                    fip.equals((String) super.get("sessionFIP"))) return;
                
                throw new SecurityException("Admin session started from another IP (internal)");
            }
            throw new SecurityException("Admin session started from another IP");
        }
        throw new SecurityException("User not authorised");
    }    
    
    public void checkAuthorised(String testAuth) {      
        String correctAuth = (String) super.get("sessionAuth");        
        if (testAuth == null || !testAuth.equals(correctAuth)) {
            throw new SecurityException("User not authorised");
        }
    }

    public void endSession(HTTPRequest request, HTTPResponse response) {
        try {
            checkAuthorised(request);
            super.remove("sessionAuth");
        } catch (SecurityException ex) {
            // Try a standard put
            put("sessionAuth", null);
        }
        response.getHeaders().setResponseCookie("sessionAuth", null);      
    }    
    
    public static User createNewUser(String email, String passwordHash, Privilege privilege) {
        
        // Verify email not already in use
        if (isEmailRegistered(email)) {
            throw new SecurityException("Email already registered");
        }
        
        User newUser = new User();
        newUser.put("privilege", privilege.getValue());
        newUser.put("email", email);
        newUser.put("passwordHash", passwordHash);
        putInIndex(TYPE, "Emails", email, newUser.getId());   
        SiteMonitor.logRegistration(newUser);
        return newUser;
    }    
    
    public static User addNewUser(String email, String password, String captcha, HTTPHandler conn) {
        if (password == null) {
            throw new SecurityException("User password can not be null.");
        }
        // Verify CAPTCHA matches that was shown to visitor
        if (isCorrectCaptcha(captcha, conn)) {
        	
            // Don't allow more than 2 registrations from an IP in 24 hours
            String type = "add_new_user_ips";
            String ip = conn.getSourceAddress();
            long period = 24 * 60 * 60 * 1000;
            int limit = 2;
            VelocityEnforcement.enforceVelocity(type, ip, limit, period);      	
        	
            String passwordHash = getMD5(password);
            return createNewUser(email, passwordHash, Privilege.USER);
        }
        throw new SecurityException("CAPTCHA incorrect. Automated user creation not allowed.");
    }
    
    public void addComment(Comment comment, HTTPRequest request) {
        checkAuthorised(request);
        if (canModifyPrivateData(request)) {
            Long time = new Long(System.currentTimeMillis());
            super.put(comment.getId(), time, "Comments");
        }
        else throw new SecurityException("Permission denied. Can not add comment.");
    }   
    
    public Collection<Comment> getComments() {
        Collection<Comment> comments = new ArrayList<Comment>();
        WikiMap commentStore = (WikiMap) get("Comments");
        if (commentStore == null) return comments;
        
        Set<String> ids = commentStore.keySet();
        for (String id : ids) {
            Comment comment = Comment.getCommentById(id);
            if (comment == null) super.remove(id, "Comments");
            else comments.add(comment);
        }
        return comments;
    }
    
    public void addPayment(Payment payment, HTTPHandler conn) {
        //checkAuthorised(conn);
        Privilege privilege = SecurityMan.getCodePrivilege();
        if (privilege.isBelow(MODIFY_BY_CODE_PRIVILEGE)) {
            throw new SecurityException("Permission denied. Can not add payment.");
        }
        Long time = new Long(System.currentTimeMillis());
        super.put(payment.getId(), time, "Payments");
    }   
    
    public Collection<Payment> getPayments() {
        Collection<Payment> payments = new ArrayList<Payment>();
        WikiMap paymentsStore = (WikiMap) get("Payments");
        if (paymentsStore == null) return payments;
        
        Set<String> ids = paymentsStore.keySet();
        for (String id : ids) {
            Payment payment = Payment.getPaymentById(id);
            if (payment == null) {
                // remove(id, "Payments");
            }
            else payments.add(payment);
        }
        return payments;
    }
    
    
    public static boolean isCorrectCaptcha(String captcha, HTTPHandler conn) {
        return CAPTCHAImage.isCorrectCaptcha(captcha, conn);
    }
  
    private boolean canModifyPrivateData(HTTPRequest request) {
        
        // Only current user or admin user can change current user values
        User operator = getUser(request);
        if (this.equals(operator) || 
            (operator.getPrivilege().isAbove(Privilege.MODERATOR) &&
             operator.getPrivilege().isAbove(getPrivilege()))) {
            return true;
        }
        
        return false;
    }    
    
    public static User getUserById(String id) {
            User u = new User(id);
            if (u.isValid()) return u;
            // Not created
            return null;
    }

    public static User getUserByEmail(String email) {
        String id = getFromIndex(TYPE, "Emails", email);
        if (id == null) return null;
        return getUserById(id);
    }

    public static User getUser(HTTPRequest request) {
        
        Map<String, String> requestCookie = request.getHeaders().getRequestCookies();
        String userId = requestCookie.get("userID");
        String testAuth = requestCookie.get("sessionAuth");
    
        if (userId != null && testAuth != null) {
            if (userId != null) {
                // Strip nasties
                userId = userId.replace("'", "");
                userId = userId.replace("\"", "");
                userId = userId.replace("<", "");
                userId = userId.replace(">", "");
                userId = userId.replace("&", "");
            }            
            User u = getUserById(userId);
            try {
                u.checkAuthorised(request);
                return u;
            } catch (Exception ex) {
                // not authorised
            }
        }
        return null;
    }    
    
    public static boolean isEmailRegistered(String email) {
        User u = User.getUserByEmail(email);
        return (u != null);
    }        
    
    private static String getMD5(String plain) {
        return WareHouse.getMD5String(plain);        
    }         
    
    public Object get(String key, String... hierarchy) {
        
        // Prevent special values from being read        
        String [] protectedKeys = { 
                "passwordHash", "sessionAuth", "sessionIP", "sessionFIP",
                "resetPasswordHash", "resetPasswordTime"
        };
        for (String protectedKey : protectedKeys) {                       
            if (key.equals(protectedKey)) {
                checkCanModify();
            }
        }
        
        checkCanRead(key, hierarchy);
        return super.get(key, hierarchy);
    }   

    private static Random rand = new Random();
}

