package eu.dnetlib.oai;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import eu.dnetlib.oai.core.AbstractOAICore;
import eu.dnetlib.oai.info.ListRecordsInfo;
import eu.dnetlib.oai.info.RecordInfo;
import eu.dnetlib.rmi.provision.OaiPublisherException;

/**
 * OAI Servlet.
 *
 * @author michele
 */
@Controller
public final class OAIController {

	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(OAIController.class); // NOPMD by marko on 11/24/08 5:02 PM
	/**
	 * Default content type.
	 */
	private static final String DEFAULT_CONTENT_TYPE = "text/xml;charset=utf-8";
	/**
	 * OAI servlet core. Most of the logic is here
	 */
	@Autowired
	private AbstractOAICore core;
	@Autowired
	private OAIProperties oaiProperties;

	@RequestMapping("/oai/clearCaches.do")
	public void clearOaiCaches() throws OaiPublisherException {
		this.core.getMdFormatsCache().removeAll();
		this.core.setCurrentDBFromIS();
		this.core.getLookupClient().evictCaches();
	}

	@RequestMapping("/oai/oai.do")
	public String oai(final ModelMap map, final HttpServletRequest request, final HttpServletResponse response) throws Exception {
		response.setContentType(OAIController.DEFAULT_CONTENT_TYPE);
		String theVerb = "";
		try {
			final Map<String, String> params = cleanParameters(request.getParameterMap());
			if (params == null) { return oaiError(OAIError.badArgument, map); }
			theVerb = params.get("verb");
			final OAI_VERBS requestedVerb = OAI_VERBS.getVerb(theVerb);
			switch (requestedVerb) {
			case IDENTIFY:
				return oaiIdentify(params, map);
			case LIST_METADATA_FORMATS:
				return oaiListMetadataFormats(params, map);
			case LIST_SETS:
				return oaiListSets(params, map);
			case GET_RECORD:
				return oaiGetRecord(params, map);
			case LIST_IDENTIFIERS:
				return oaiListIdentifiersOrRecords(params, map);
			case LIST_RECORDS:
				return oaiListIdentifiersOrRecords(params, map);
			default:
				return oaiError(OAIError.badVerb, map);
			}
		} catch (final CannotDisseminateFormatException e) {
			log.debug("ERROR", e);
			return oaiError(OAIError.cannotDisseminateFormat, theVerb, map);
		} catch (final NoRecordsMatchException e) {
			log.debug("ERROR", e);
			return oaiError(OAIError.noRecordsMatch, theVerb, map);
		} catch (final BadResumptionTokenException e) {
			log.debug("ERROR", e);
			return oaiError(OAIError.badResumptionToken, theVerb, map);
		} catch (final Exception e) {
			log.error("ERROR", e);
			return oaiError(e, map);
		}
	}

	private Map<String, String> cleanParameters(final Map<?, ?> startParams) {
		final HashMap<String, String> params = new HashMap<String, String>();
		final Iterator<?> iter = startParams.entrySet().iterator();
		while (iter.hasNext()) {
			final Entry<?, ?> entry = (Entry<?, ?>) iter.next();
			final String key = entry.getKey().toString();
			final String[] arr = (String[]) entry.getValue();
			if (arr.length == 0) { return null; }
			final String value = arr[0];
			if (key.equals("verb")) {
				params.put("verb", value);
			} else if (key.equals("from")) {
				params.put("from", value);
			} else if (key.equals("until")) {
				params.put("until", value);
			} else if (key.equals("metadataPrefix")) {
				params.put("metadataPrefix", value);
			} else if (key.equals("identifier")) {
				params.put("identifier", value);
			} else if (key.equals("set")) {
				params.put("set", value);
			} else if (key.equals("resumptionToken")) {
				params.put("resumptionToken", value);
			} else {
				return null;
			}
		}
		return params;
	}

	private String oaiIdentify(final Map<String, String> params, final ModelMap map) throws Exception {
		String verb = null;
		if (params.containsKey("verb")) {
			verb = params.get("verb");
			params.remove("verb");
		}
		if (params.entrySet().size() > 0) { return oaiError(OAIError.badArgument, verb, map); }
		return "oai/OAI_Identify";
	}

	private String oaiGetRecord(final Map<String, String> params, final ModelMap map) throws Exception {
		String verb = null;
		String prefix = null;
		String identifier = null;
		if (params.containsKey("verb")) {
			verb = params.get("verb");
			params.remove("verb");
		}
		if (params.containsKey("metadataPrefix")) {
			prefix = params.get("metadataPrefix");
			params.remove("metadataPrefix");
		} else {
			return oaiError(OAIError.badArgument, verb, map);
		}
		if (params.containsKey("identifier")) {
			identifier = params.get("identifier");
			params.remove("identifier");
		} else {
			return oaiError(OAIError.badArgument, verb, map);
		}
		if (params.entrySet().size() > 0) { return oaiError(OAIError.badArgument, verb, map); }
		this.core.setCurrentDBFromIS();
		final RecordInfo record = this.core.getInfoRecord(identifier, prefix);
		if (record == null) { return oaiError(OAIError.idDoesNotExist, map); }
		map.addAttribute("record", record);

		return "oai/OAI_GetRecord";
	}

	private String oaiListIdentifiersOrRecords(final Map<String, String> params, final ModelMap map) throws Exception {
		OAI_VERBS verb = null;
		String metadataPrefix = null;
		String resumptionToken = null;
		String set = null;
		String from = null;
		String until = null;

		if (params.containsKey("verb")) {
			verb = OAI_VERBS.getVerb(params.get("verb"));
			params.remove("verb");
		}

		if (params.containsKey("resumptionToken")) {
			resumptionToken = params.get("resumptionToken");
			params.remove("resumptionToken");
			this.core.setCurrentDBFromIS();
		} else {
			this.core.setCurrentDBFromIS();
			if (params.containsKey("metadataPrefix")) {
				metadataPrefix = params.get("metadataPrefix");
				params.remove("metadataPrefix");
			} else {
				return oaiError(OAIError.badArgument, verb.toString(), map);
			}
			if (params.containsKey("from")) {
				from = params.get("from");
				params.remove("from");
			}
			if (params.containsKey("until")) {
				until = params.get("until");
				params.remove("until");
			}
			if (params.containsKey("set")) {
				set = params.get("set");
				params.remove("set");
			}
		}
		if (params.entrySet().size() > 0) { return oaiError(OAIError.badArgument, verb.toString(), map); }
		if (StringUtils.isNotBlank(set) && !this.core.existSet(set)) { return oaiError(OAIError.badArgument, verb.toString(), map); }

		boolean onlyIdentifiers = true;
		if (verb == OAI_VERBS.LIST_RECORDS) {
			onlyIdentifiers = false;
		}

		ListRecordsInfo infos;

		if (StringUtils.isBlank(resumptionToken)) {
			infos = this.core.listRecords(onlyIdentifiers, metadataPrefix, set, from, until);
		} else {
			infos = this.core.listRecords(onlyIdentifiers, resumptionToken);
		}

		map.addAttribute("info", infos);

		if (verb == OAI_VERBS.LIST_RECORDS) { return "oai/OAI_ListRecords"; }

		return "oai/OAI_ListIdentifiers";
	}

	private String oaiListSets(final Map<String, String> params, final ModelMap map) throws Exception {
		String verb = null;
		if (params.containsKey("verb")) {
			verb = params.get("verb");
			params.remove("verb");
		}
		if (params.entrySet().size() > 0) { return oaiError(OAIError.badArgument, verb, map); }
		this.core.setCurrentDBFromIS();
		map.addAttribute("sets", this.core.listSets());
		return "oai/OAI_ListSets";
	}

	private String oaiListMetadataFormats(final Map<String, String> params, final ModelMap map) throws Exception {
		String id = null;
		String verb = null;
		if (params.containsKey("verb")) {
			verb = params.get("verb");
			params.remove("verb");
		}
		if (params.containsKey("identifier")) {
			id = params.get("identifier");
			params.remove("identifier");
		}
		if (params.entrySet().size() > 0) { return oaiError(OAIError.badArgument, verb, map); }
		this.core.setCurrentDBFromIS();
		map.addAttribute("formats", this.core.listMetadataFormats());
		if (id != null) {
			map.addAttribute("identifier", id);
			return "oai/OAI_ListMetadataFormats_withid";
		}
		return "oai/OAI_ListMetadataFormats";
	}

	private String oaiError(final OAIError errCode, final String verb, final ModelMap map) throws Exception {
		if (StringUtils.isBlank(verb)) { return oaiError(errCode, map); }
		map.addAttribute("verb", verb);
		map.addAttribute("errcode", errCode.name());
		map.addAttribute("errmsg", errCode.getMessage());
		return "oai/OAI_Error";
	}

	private String oaiError(final OAIError errCode, final ModelMap map) throws Exception {
		map.addAttribute("errcode", errCode.name());
		map.addAttribute("errmsg", errCode.getMessage());
		return "oai/OAI_Error_noverb";
	}

	private String oaiError(final Exception e, final ModelMap map) throws Exception {
		map.addAttribute("errcode", "InternalException");
		map.addAttribute("errmsg", e.getMessage());
		return "oai/OAI_Error_noverb";
	}

	/*
	 * Default Attribute
	 */
	@ModelAttribute("url")
	public String url(final HttpServletRequest request) {
		final String forwardedUrl = request.getHeader(getForwardedUrlHeaderName());
		final String baseURL = getBaseUrl();
		if (StringUtils.isNotBlank(baseURL)) {
			return baseURL;
		} else if (StringUtils.isNotBlank(forwardedUrl)) {
			return forwardedUrl;
		} else {
			return request.getRequestURL() + "";
		}
	}

	/**
	 * Model attribute for the date of response.
	 * <p>
	 * According to OAI-PMH protocol, it must be in UTC with format YYYY-MM-DDThh:mm:ssZ, where Z is the time zone ('Z' for "Zulu Time",
	 * i.e. UTC).
	 * </p>
	 * <p>
	 * See http://www.openarchives.org/OAI/openarchivesprotocol.html#Dates
	 * </p>
	 */
	@ModelAttribute("date")
	public String date() {
		final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
		formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
		final String date = formatter.format(new Date());
		return date.replace("+0000", "Z");
	}

	@ModelAttribute("repoName")
	public String getRepoName() {
		return this.oaiProperties.getRepoName();
	}

	@ModelAttribute("email")
	public String getRepoEmail() {
		return this.oaiProperties.getRepoEmail();
	}

	@ModelAttribute("earliestDatestamp")
	public String getEarliestDatestamp() {
		return this.oaiProperties.getEarliestDatestamp();
	}

	public String getBaseUrl() {
		return this.oaiProperties.getBaseUrl();
	}

	public String getForwardedUrlHeaderName() {
		return this.oaiProperties.getForwardedUrlHeaderName();
	}

	@ModelAttribute("deletedRecord")
	public String getDeletedRecordSupport() {
		return this.oaiProperties.getDeletedRecordSupport();
	}

	@ModelAttribute("granularity")
	public String getDateGranularity() {
		return this.oaiProperties.getDateGranularity();
	}

	public enum OAI_VERBS {
		IDENTIFY, LIST_IDENTIFIERS, LIST_RECORDS, LIST_METADATA_FORMATS, LIST_SETS, GET_RECORD, UNSUPPORTED_VERB;

		public static OAI_VERBS getVerb(final String theVerb) {
			if (StringUtils.isBlank(theVerb)) { return UNSUPPORTED_VERB; }
			if (theVerb.equalsIgnoreCase("Identify")) { return IDENTIFY; }
			if (theVerb.equalsIgnoreCase("ListIdentifiers")) { return LIST_IDENTIFIERS; }
			if (theVerb.equalsIgnoreCase("ListRecords")) { return LIST_RECORDS; }
			if (theVerb.equalsIgnoreCase("ListMetadataFormats")) { return LIST_METADATA_FORMATS; }
			if (theVerb.equalsIgnoreCase("ListSets")) { return LIST_SETS; }
			if (theVerb.equalsIgnoreCase("GetRecord")) { return GET_RECORD; }
			if (theVerb.equalsIgnoreCase("listidentifiers")) { return LIST_IDENTIFIERS; }
			return UNSUPPORTED_VERB;

		}
	}

	public enum DELETED_SUPPORT {
		NO, TRANSIENT, PERSISTENT;

		@Override
		public String toString() {
			switch (this) {
			case TRANSIENT:
				return "transient";
			case PERSISTENT:
				return "persistent";
			default:
				return "no";
			}
		}

	}
}
