package org.wikiwebserver.handler.http.responder;

import java.io.IOException;

import org.wikiwebserver.core.WareHouse;
import org.wikiwebserver.distribute.interfaces.WorkerNode;
import org.wikiwebserver.distribute.server.RemoteWorkerNode;
import org.wikiwebserver.distribute.server.TaskStub;
import org.wikiwebserver.distribute.server.WorkerNodeManager;
import org.wikiwebserver.handler.http.HTTPException;
import org.wikiwebserver.handler.http.HTTPHandler;
import org.wikiwebserver.handler.http.HTTPHeaders;
import org.wikiwebserver.handler.http.interfaces.HTTPResponder;
import org.wikiwebserver.util.VelocityEnforcement;
import org.wikiwebserver.util.VelocityExceededException;

import page.config.SiteMonitor;
import page.tools.entity.Browser;
import page.tools.entity.NodeData;
import page.tools.entity.User;

public class NodeTaskAssignmentResponder implements HTTPResponder {
	
	private static final long IDLE_TIME_BEFORE_NAME_REUSE = 28l * 24 * 60 * 60 * 1000;

	public Object respond(HTTPHandler conn) throws IOException {
		
	    //System.out.println(conn.getRequest());
		
	    long time = System.currentTimeMillis();
		HTTPHeaders requestHeaders = conn.getRequest().getHeaders();
		HTTPHeaders responseHeaders = conn.getResponse().getHeaders();
		
		// Reduce header size
		responseHeaders.remove("Server");
		responseHeaders.remove("Content-Language");
		responseHeaders.remove("Allow");
		responseHeaders.remove("Vary");
		responseHeaders.remove("Set-Cookie");
		responseHeaders.remove("X-Developer");
		responseHeaders.remove("X-WikiWebServerID");
		responseHeaders.remove("X-WikiWebServer-ClassLoaderID");        
		responseHeaders.remove("X-Java-Source");
		responseHeaders.remove("Accept-Ranges");        
		responseHeaders.remove("X-Java-VMStartTime");
	
		
		// The first time a node communicates it makes a request for a node id
		String nodeIdReq = requestHeaders.getFirst("X-Node-ID-Request");
		
		// Previously allocated nodeId (within stored configuration)
		String nodeId = requestHeaders.getFirst("X-Node-ID");
		String nodeIdKey = requestHeaders.getFirst("X-Node-ID-Key");
		if (nodeId == null || nodeIdKey == null) {
		    // Previously allocated nodeId (within cookie data)
		    nodeId = conn.getRequest().getHeaders().getRequestCookies().get("nodeID");
		    nodeIdKey = conn.getRequest().getHeaders().getRequestCookies().get("nodeIDKey");
		    // The node itself, needs to know these
            responseHeaders.set("X-Node-ID", nodeId);
            responseHeaders.set("X-Node-ID-Key", nodeIdKey);    
		}
		
        
		// Ensure there is an ID or the request is for a new ID
        if (nodeId == null && nodeIdReq == null) {
        	throw new HTTPException(400, "Node did not correctly identify itself");
        }
		
        String nodeName = requestHeaders.getFirst("X-Node-Name");		
        String nodeConfigurationId = requestHeaders.getFirst("X-Node-Config-ID");
        String nodeRequiresPassword = requestHeaders.getFirst("X-Node-Requires-Password");
		
        NodeData nodeData = NodeData.getNodeDataById(nodeId);	
        
        boolean nodeIdRequested = (nodeId == null) && (nodeIdReq != null);
        boolean nodeIdKeyNotSpecified = (nodeIdKey == null);  // before keys introduced      
        boolean nodeDataNotAvailable = (nodeData == null); // if we lose persistent data
		
        // Allocate new ID
		if (nodeIdRequested || nodeIdKeyNotSpecified || nodeDataNotAvailable) {
			nodeId = NodeData.allocateId();
			nodeIdKey = WareHouse.getMD5String(nodeId + time);
			
			// Make sure we are not allocating tons of IDs
			// If more than 5 in a minute, velocity block for an hour
			String type = "new_node_id_ips"; 
	    	String ip = conn.getSourceAddress();    	
	    	int limit = 5;
	    	long period = 60 * 1000;
	    	long wait = 60 * 60 * 1000;			
	    	try {
	    		VelocityEnforcement.enforceVelocity(type, ip, limit, period, wait);
	    	} catch (VelocityExceededException ex) {
	    		throw new HTTPException(400, "Request for node ID denied (abusive)");
	    	}
			
			nodeData = new NodeData(nodeId);
			nodeData.put("createdTime", new Long(time));
			nodeData.put("nodeIdKey", nodeIdKey);
            responseHeaders.set("X-Node-ID", nodeId);
            responseHeaders.set("X-Node-ID-Key", nodeIdKey);    			
			// Also store nodeId in cookie in case config file lost 
			conn.getResponse().getHeaders().setResponseCookie("nodeID", nodeId);		
			conn.getResponse().getHeaders().setResponseCookie("nodeIDKey", nodeIdKey);		
			
			// Only log first, genuine node id request
			if (nodeIdRequested) {
				SiteMonitor.logNewNode(nodeData);		
			}
		}
		
        if (nodeData == null) {
            responseHeaders.set("X-Task-Check-Delay", "600000");
            responseHeaders.set("Connection", "close");
            throw new HTTPException(400, "Invalid remote worker node data");
        } 	
        
        if (nodeData.isBlocked()) {
            responseHeaders.set("X-Task-Check-Delay", "600000");   
            responseHeaders.set("Connection", "close");
            throw new HTTPException(400, "This node has been blocked");
        }        
        
        String correctNodeIdKey = (String) nodeData.get("nodeIdKey");
        if (correctNodeIdKey != null && !nodeIdKey.equals(correctNodeIdKey)) {
            responseHeaders.set("X-Task-Check-Delay", "600000"); 
            responseHeaders.set("Connection", "close");
            throw new HTTPException(400, "Node ID key incorrect");
        }
		
        if (nodeName != null) {
            
            // Decode the node name
            nodeName = WareHouse.urlDecode(nodeName);  
            
            boolean isNodeNameChangeWithoutConfigChange = 
                (nodeConfigurationId == null && !nodeName.equals(nodeData.getName()));
            
            // Almost certainly two nodes using same ID, allocate another
            if (isNodeNameChangeWithoutConfigChange) {
                nodeId = NodeData.allocateId();
                nodeData = new NodeData(nodeId);
                nodeData.put("createdTime", new Long(time));
            }
        }

		// Will create new if node does not exist
        WorkerNode node = WorkerNodeManager.getNode(nodeId);        
        
        if (!(node instanceof RemoteWorkerNode)) {
            responseHeaders.set("X-Task-Check-Delay", "600000");       
            responseHeaders.set("Connection", "close");
        	throw new HTTPException(400, "Invalid remote worker node");
        }  

        RemoteWorkerNode remoteNode = (RemoteWorkerNode) node;
        
        int delayUntilNextCheck = remoteNode.updateTaskCheckDelay();
        responseHeaders.set("X-Task-Check-Delay", String.valueOf(delayUntilNextCheck));
        if (delayUntilNextCheck > 4000) {
            responseHeaders.set("Connection", "close");
        }
        
        // The configuration of the node effects cacheable content
        // To avoid returning the wrong content, cache keys use node configuration id
        if (nodeConfigurationId != null) {   
        	remoteNode.setConfigurationId(nodeConfigurationId);
            
        	// Check name not in use
            NodeData namedNode = NodeData.getNodeDataByName(nodeName);
            if (namedNode != null && !namedNode.getId().equals(nodeData.getId())) {
                Long namedNodeAccessTime = (Long) namedNode.get("lastRequestTime");
                if (namedNodeAccessTime == null ||
                    namedNodeAccessTime.longValue() + IDLE_TIME_BEFORE_NAME_REUSE > time) {
                    responseHeaders.set("X-Task-Check-Delay", "60000");                        
                    throw new HTTPException(400, "Name in use by another node");
                }
            }
            
            // Update name
            nodeData.setName(nodeName);    
            
            // Update OS
            String nodeOSName = requestHeaders.getFirst("X-Node-OS-Name");
            if (nodeOSName != null) {
                nodeData.put("osName", nodeOSName);
            }     
            
            // Update owner data
            String userID = requestHeaders.getFirst("User-ID");
            if (userID == null) {
            	userID = requestHeaders.getRequestCookies().get("userID");
            }
            if (userID != null) {
                User user = User.getUserById(userID);
                if (user != null) {
                    try {
                        //user.checkAuthorised(conn.getRequest());
                        nodeData.setOwner(user);
                    }
                    catch (SecurityException ex) {}
                }
            }
            
            String browserID = requestHeaders.getFirst("Browser-ID");
            if (browserID == null) {
            	browserID = requestHeaders.getRequestCookies().get("browserID");
            }
            if (browserID != null) {
            	Browser browser = User.getBrowserById(browserID);
                if (browser != null) {
                    nodeData.put("browser", browserID);
                }
            }            
        }
        
        if (nodeRequiresPassword != null) {
            nodeData.setRequiresPassword(nodeRequiresPassword.equals("true"));
        }

        // Read next task from the queue        
        TaskStub taskStub = remoteNode.getNextTaskStubForStarting();	
        
        if (taskStub != null) {

        	if (taskStub.getTaskId() != null) {
        		responseHeaders.set("X-Task-ID", taskStub.getTaskId());
        	}
    
        	String className = taskStub.getTaskClassName();
        	int packageEnd = className.lastIndexOf('.');
        	String taskName = className.substring(packageEnd+1, className.length()-4);
        	responseHeaders.set("X-Task", taskName);
        	
        	if (taskStub.getExpireTime() > 0) {
        		responseHeaders.setDate("X-Task-Expires", taskStub.getExpireTime());
        	}
        	if (taskStub.getTaskInputMeta() != null) {
        		responseHeaders.add("X-Task-Input-Meta", taskStub.getTaskInputMeta());
        	}
            if (taskStub.getTaskPassword() != null) {
                responseHeaders.add("X-Task-Password", taskStub.getTaskPassword());
            }        	
        	
        	nodeData.incrementValue("numTasksAssigned", 1);
        }

        //System.out.println(conn.getResponse());
        
    	// No content, only HTTP headers
		return null;
	}	
}
