package eu.dnetlib.functionality.modular.ui.workflows.controllers;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.google.common.base.Function;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.googlecode.sarasvati.GraphProcess;
import com.googlecode.sarasvati.Node;
import com.googlecode.sarasvati.NodeToken;
import com.googlecode.sarasvati.ProcessState;

import eu.dnetlib.common.logging.DnetLogger;
import eu.dnetlib.common.logging.LogMessage;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpException;
import eu.dnetlib.functionality.modular.ui.AbstractAjaxController;
import eu.dnetlib.functionality.modular.ui.workflows.objects.AdvancedMetaWorkflowDescriptor;
import eu.dnetlib.functionality.modular.ui.workflows.objects.AtomicWorkflowDescriptor;
import eu.dnetlib.functionality.modular.ui.workflows.objects.MetaWorkflowDescriptor;
import eu.dnetlib.functionality.modular.ui.workflows.objects.NodeInfo;
import eu.dnetlib.functionality.modular.ui.workflows.objects.NodeTokenInfo;
import eu.dnetlib.functionality.modular.ui.workflows.objects.NodeWithUserParams;
import eu.dnetlib.functionality.modular.ui.workflows.objects.ProcessListEntry;
import eu.dnetlib.functionality.modular.ui.workflows.objects.sections.WorkflowSectionGrouper;
import eu.dnetlib.functionality.modular.ui.workflows.sarasvati.viewer.ProcessGraphGenerator;
import eu.dnetlib.functionality.modular.ui.workflows.util.ISLookupClient;
import eu.dnetlib.functionality.modular.ui.workflows.util.ISRegistryClient;
import eu.dnetlib.miscutils.datetime.DateUtils;
import eu.dnetlib.msro.workflows.sarasvati.loader.ProfileToSarasvatiConverter;
import eu.dnetlib.msro.workflows.sarasvati.loader.WorkflowExecutor;
import eu.dnetlib.msro.workflows.sarasvati.registry.GraphProcessRegistry;
import eu.dnetlib.msro.workflows.util.ProcessUtils;
import eu.dnetlib.msro.workflows.util.WorkflowsConstants;
import eu.dnetlib.msro.workflows.util.WorkflowsConstants.WorkflowStatus;

/**
 * Web controller for the UI
 * 
 * @author Michele Artini
 */

@Controller
public class WorkflowsController extends AbstractAjaxController {

	private final class JournalEntryFunction implements Function<Map<String, String>, ProcessListEntry> {

		@Override
		public ProcessListEntry apply(final Map<String, String> input) {
			final String name = input.get(WorkflowsConstants.SYSTEM_WF_PROFILE_NAME);
			
			String repo = "";
			if (input.containsKey(WorkflowsConstants.DATAPROVIDER_NAME)) {
				repo += input.get(WorkflowsConstants.DATAPROVIDER_NAME);
			}
			final String procId = input.get(WorkflowsConstants.SYSTEM_WF_PROCESS_ID);
			final String wfId = input.get(WorkflowsConstants.SYSTEM_WF_PROFILE_ID);
			final String family = input.get(WorkflowsConstants.SYSTEM_WF_PROFILE_FAMILY);
			final long date = NumberUtils.toLong(input.get(LogMessage.LOG_DATE_FIELD), 0);
			final String status = Boolean.valueOf(input.get(WorkflowsConstants.SYSTEM_COMPLETED_SUCCESSFULLY)) ? "SUCCESS" : "FAILURE";
		
			return new ProcessListEntry(procId, wfId, name, family, status, date, repo);
		}
	}

	@Resource
	private ISLookupClient isLookupClient;

	@Resource
	private ISRegistryClient isRegistryClient;

	@Resource
	private GraphProcessRegistry graphProcessRegistry;

	@Resource
	private ProcessGraphGenerator processGraphGenerator;

	@Resource
	private WorkflowSectionGrouper workflowSectionGrouper;

	@Resource
	private WorkflowExecutor workflowExecutor;

	@Resource
	private ProfileToSarasvatiConverter profileToSarasvatiConverter;

	@Resource(name = "msroWorkflowLogger")
	private DnetLogger dnetLogger;

	private static final Log log = LogFactory.getLog(WorkflowsController.class);

	@RequestMapping("/ui/list_metaworkflows.json")
	public @ResponseBody List<MetaWorkflowDescriptor> listMetaWorflowsForSection(@RequestParam(value = "section", required = false) final String sectionName, @RequestParam(value = "dsId", required = false) final String dsId)
			throws ISLookUpException, IOException {
		if (sectionName != null) {
			return workflowSectionGrouper.listMetaWorflowsForSection(sectionName);
		} else if (dsId != null) {
			return workflowSectionGrouper.listMetaWorflowsForDatasource(dsId);
		} else {
			return Lists.newArrayList();
		}
	}

	@RequestMapping("/ui/wf_metaworkflow.json")
	public @ResponseBody AdvancedMetaWorkflowDescriptor getMetaWorkflow(@RequestParam(value = "id", required = true) final String id) throws Exception {
		return isLookupClient.getMetaWorkflow(id);
	}

	@RequestMapping("/ui/wf_atomic_workflow.json")
	public @ResponseBody AtomicWorkflowDescriptor getAtomicWorkflow(@RequestParam(value = "id", required = true) final String id) throws Exception {
		final AtomicWorkflowDescriptor wf = isLookupClient.getAtomicWorkflow(id);
		final String xml = profileToSarasvatiConverter.getSarasvatiWorkflow(id).getWorkflowXml();

		wf.setMapContent(processGraphGenerator.getWfDescImageMap(id, xml));

		return wf;
	}

	@RequestMapping("/ui/wf_atomic_workflow.img")
	public void showAtomicWorkflow(final HttpServletResponse response, @RequestParam(value = "id", required = true) final String id) throws Exception {

		String xml = profileToSarasvatiConverter.getSarasvatiWorkflow(id).getWorkflowXml();
		Set<String> notConfiguredNodes = isLookupClient.getNotConfiguredNodes(id);
		BufferedImage image = processGraphGenerator.getWfDescImage(id, xml, notConfiguredNodes);
		sendImage(response, image);
	}

	private void sendImage(final HttpServletResponse response, final BufferedImage image) throws IOException {
		response.setContentType("image/png");
		OutputStream out = response.getOutputStream();
		ImageIO.write(image, "png", out);
		out.flush();
		out.close();
	}

	@RequestMapping("/ui/wf.start")
	public @ResponseBody String startWorkflow(@RequestParam(value = "id", required = true) final String id) throws Exception {
		return workflowExecutor.startProcess(id);
	}

	@RequestMapping("/ui/metawf.start")
	public @ResponseBody String startMetaWorkflow(@RequestParam(value = "id", required = true) final String id) throws Exception {
		workflowExecutor.startMetaWorkflow(id, true);
		return id;
	}

	@RequestMapping("/ui/wf_workflow_node.json")
	public @ResponseBody NodeInfo workflowNode_info(@RequestParam(value = "wf", required = true) final String wfId,
			@RequestParam(value = "node", required = true) final String nodeName) throws ISLookUpException, IOException {
		return isLookupClient.getNodeInfo(wfId, nodeName);
	}

	@RequestMapping("/ui/wf_metaworkflow.edit")
	public @ResponseBody boolean scheduleMetaWorkflow(@RequestParam(value = "json", required = true) final String json) throws Exception {
		
		final AdvancedMetaWorkflowDescriptor info = (new Gson()).fromJson(json, AdvancedMetaWorkflowDescriptor.class);

		log.info("Updating workflow " + info.getName());

		final String xml = isLookupClient.getProfile(info.getWfId());
		boolean res = isRegistryClient.updateSarasvatiMetaWorkflow(info.getWfId(), xml, info);

		return res;
	}

	@RequestMapping("/ui/clone_metaworkflow.do")
	public @ResponseBody String cloneMetaWf(@RequestParam(value = "id", required = true) final String id,
			@RequestParam(value = "name", required = true) final String name) throws Exception {

		if (name.trim().length() > 0) {
			final String xml = isLookupClient.getProfile(id);
			SAXReader reader = new SAXReader();
			Document doc = reader.read(new StringReader(xml));
			doc.selectSingleNode("//METAWORKFLOW_NAME").setText(name);
			for (Object o : doc.selectNodes("//WORKFLOW")) {
				Element n = (Element) o;
				String atomWfXml = isLookupClient.getProfile(n.valueOf("@id"));
				String newAtomWfId = isRegistryClient.registerProfile(atomWfXml);
				n.addAttribute("id", newAtomWfId);
			}
			return isRegistryClient.registerProfile(doc.asXML());
		} else throw new IllegalArgumentException("Name is empty");
	}

	@RequestMapping("/ui/wf_proc_node.json")
	public @ResponseBody NodeTokenInfo getProcessWorkflowNode(@RequestParam(value = "id", required = true) final String pid,
			@RequestParam(value = "node", required = true) final long nid) throws Exception {

		final NodeToken token = findNodeToken(pid, nid);

		final NodeTokenInfo info = (token == null) ? new NodeTokenInfo(findNodeName(pid, nid)) : new NodeTokenInfo(token);

		return info;
	}

	private NodeToken findNodeToken(final String pid, final long nid) {
		final GraphProcess process = graphProcessRegistry.findProcess(pid);
		if (process != null) {
			for (NodeToken token : process.getNodeTokens()) {
				if (token.getNode().getId() == nid) return token;
			}
		}
		return null;
	}

	private String findNodeName(final String pid, final long nid) {
		final GraphProcess process = graphProcessRegistry.findProcess(pid);
		if (process != null) {
			for (Node node : process.getGraph().getNodes()) {
				if (node.getId() == nid) return node.getName();
			}
		}
		return "-";
	}


	@RequestMapping("/ui/wf_proc.img")
	public void showProcessWorkflow(final HttpServletResponse response, @RequestParam(value = "id", required = true) final String id) throws Exception {
		BufferedImage image = processGraphGenerator.getProcessImage(id);
		sendImage(response, image);
	}

	@RequestMapping("/ui/wf_proc.kill")
	public @ResponseBody boolean killProcessWorkflow(@RequestParam(value = "id", required = true) final String id) throws Exception {
		GraphProcess proc = graphProcessRegistry.findProcess(id);
		proc.setState(ProcessState.Canceled);
		return true;
	}
	
	@RequestMapping("/ui/wf_journal.range")
	public @ResponseBody Collection<ProcessListEntry> rangeWfJournal(@RequestParam(value = "start", required = true) final String start,
			@RequestParam(value = "end", required = true) final String end) throws Exception {
		
		final Map<String, ProcessListEntry> res = Maps.newHashMap();
		
		final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd");
		final DateTime startDate = formatter.parseDateTime(start);
		final DateTime endDate = formatter.parseDateTime(end).plusHours(23).plusMinutes(59).plusSeconds(59);
		
		final Iterator<ProcessListEntry> iter = Iterators.transform(dnetLogger.range(startDate.toDate(), endDate.toDate()), new JournalEntryFunction());
		while (iter.hasNext()) {
			ProcessListEntry e = iter.next();
			res.put(e.getProcId(), e);
		}
		
		long now = DateUtils.now();
		if (startDate.isBefore(now) && endDate.isAfter(now)) {
			for (String pid : graphProcessRegistry.listIdentifiers()) {
				final GraphProcess proc = graphProcessRegistry.findProcess(pid);
				res.put(pid, new ProcessListEntry(pid, proc));
			}
		}
	
		return res.values();
		
	}

	@RequestMapping("/ui/wf_journal.find")
	public @ResponseBody Collection<ProcessListEntry> findWfJournal(@RequestParam(value = "wfs", required = true) final String wfs) {
		final Map<String, ProcessListEntry> res = Maps.newHashMap();
		
		final Set<String> wfFilter = (new Gson()).fromJson(wfs, new TypeToken<Set<String>>(){}.getType());
				
		for (String wfId : wfFilter) {
			final Iterator<ProcessListEntry> iter = Iterators.transform(dnetLogger.find(WorkflowsConstants.SYSTEM_WF_PROFILE_ID, wfId), new JournalEntryFunction());
			while (iter.hasNext()) {
				ProcessListEntry e = iter.next();
				res.put(e.getProcId(), e);
			}
		}
		
		for (String pid : graphProcessRegistry.listIdentifiers()) {
			final GraphProcess proc = graphProcessRegistry.findProcess(pid);
			if (wfFilter.contains(ProcessUtils.calculateWfId(proc))) {
				res.put(pid, new ProcessListEntry(pid, proc));
			}
		}
		
		return res.values();
	}
		
	@RequestMapping("/ui/wf_journal_byFamily.find")
	public @ResponseBody Collection<ProcessListEntry> findWfJournalByFamily(@RequestParam(value = "family", required = true) final String family) throws IOException {
		final Iterator<ProcessListEntry> iter = Iterators.transform(dnetLogger.find(WorkflowsConstants.SYSTEM_WF_PROFILE_FAMILY, family), new JournalEntryFunction());
		return Lists.newArrayList(iter);
	}
		
	@RequestMapping("/ui/wf_journal.get")
	public @ResponseBody Map<String, Object> getWfJournalLog(@RequestParam(value = "id", required = true) final String id) throws Exception {
		final Map<String, Object> res = Maps.newHashMap();
		
		final Map<String, String> logs = dnetLogger.findOne("system:processId", id);

		if (logs != null && !logs.isEmpty()) {
			final List<String> keys = Lists.newArrayList(logs.keySet());
			Collections.sort(keys);
			
			final List<Map<String, String>> journalEntry = Lists.newArrayList();
			for (String k : keys) {
				final Map<String, String> m = Maps.newHashMap();
				m.put("name", k);
				m.put("value", logs.get(k));
				journalEntry.add(m);
			}
			res.put("journal", journalEntry);
		}
		
		final GraphProcess process = graphProcessRegistry.findProcess(id);
	
		if (process != null) {
			final String mapContent = (process.getState() == ProcessState.Created) ? "" : processGraphGenerator.getProcessImageMap(id);
	
			String status = "";
			if (!process.isComplete()) {
				status = process.getState().toString().toUpperCase();
			} else if ("true".equals(process.getEnv().getAttribute(WorkflowsConstants.SYSTEM_COMPLETED_SUCCESSFULLY))) {
				status = "SUCCESS";
			} else {
				status = "FAILURE";
			}
	
			final String img = (process.getState() == ProcessState.Created) ? "../resources/img/notStarted.gif" : "wf_proc.img?id=" + id + "&t=" + DateUtils.now();
			
			final String name = process.getGraph().getName();
			
			final long startDate = NumberUtils.toLong(process.getEnv().getAttribute(WorkflowsConstants.SYSTEM_START_DATE), 0);
			final long endDate = NumberUtils.toLong(process.getEnv().getAttribute(WorkflowsConstants.SYSTEM_END_DATE), 0);
	
			final AtomicWorkflowDescriptor wf = new AtomicWorkflowDescriptor(id, name, status, mapContent, img, true, "auto", "RUNNING", startDate, endDate);
		
			res.put("graph", wf);
		}

		return res;
	}

	@RequestMapping("/ui/wf_atomic_workflow.enable")
	public @ResponseBody String enableAtomicWf(@RequestParam(value = "id", required = true) final String id,
			@RequestParam(value = "start", required = true) final String value) throws Exception {
		isRegistryClient.configureWorkflowStart(id, value);

		return value;
	}
	
	@RequestMapping("/ui/workflow_user_params.json")
	public @ResponseBody List<NodeWithUserParams> listWorkflowUserParams(@RequestParam(value = "wf", required = true) final String wfId) throws Exception {
		return isLookupClient.listWorkflowUserParams(wfId);
	}	

	@RequestMapping(value="/ui/save_user_params.do")
	public @ResponseBody boolean saveWorkflowUserParams(@RequestParam(value = "wf", required = true) final String wfId, @RequestParam(value = "params", required = true) final String jsonParams) throws Exception {
		
		final String xml = isLookupClient.getProfile(wfId);
		
		final List<NodeWithUserParams> params = new Gson().fromJson(jsonParams,  new TypeToken<List<NodeWithUserParams>>(){}.getType());
		
		boolean res = isRegistryClient.updateSarasvatiWorkflow(wfId, xml, params);

		for (String metaWfId : isLookupClient.listMetaWorflowsForWfId(wfId)) {
			if (isLookupClient.isExecutable(metaWfId)) {
				isRegistryClient.updateMetaWorkflowStatus(metaWfId, WorkflowStatus.EXECUTABLE);
			} else {
				isRegistryClient.updateMetaWorkflowStatus(metaWfId, WorkflowStatus.WAIT_USER_SETTINGS);
			}
		}
		
		return res;
	}

}
