/**
 * Copyright (c) 2009-2017 The Apereo Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *             http://opensource.org/licenses/ecl2
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.sakaiproject.lti.impl;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import lombok.extern.slf4j.Slf4j;
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;

import net.oauth.signature.OAuthSignatureMethod;

import org.tsugi.basiclti.BasicLTIConstants;

import org.sakaiproject.authz.api.SecurityAdvisor;
import org.sakaiproject.authz.cover.SecurityService;
import org.sakaiproject.basiclti.util.LegacyShaUtil;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.lti.api.SiteMembershipUpdater;
import org.sakaiproject.lti.api.UserFinderOrCreator;
import org.sakaiproject.lti.api.SiteMembershipsSynchroniser;
import org.sakaiproject.lti.extensions.POXMembershipsResponse;
import org.sakaiproject.site.api.Group;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.user.api.User;

@Slf4j
public class SiteMembershipsSynchroniserImpl implements SiteMembershipsSynchroniser {

    private UserFinderOrCreator userFinderOrCreator = null;
    public void setUserFinderOrCreator(UserFinderOrCreator userFinderOrCreator) {
        this.userFinderOrCreator = userFinderOrCreator;
    }

    private ServerConfigurationService serverConfigurationService;
    public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) {
        this.serverConfigurationService = serverConfigurationService;
    }

    private SiteMembershipUpdater siteMembershipUpdater = null;
    public void setSiteMembershipUpdater(SiteMembershipUpdater siteMembershipUpdater) {
        this.siteMembershipUpdater = siteMembershipUpdater;
    }

    private SiteService siteService = null;
    public void setSiteService(SiteService siteService) {
        this.siteService = siteService;
    }

	private void pushAdvisor() {

		// setup a security advisor
		SecurityService.pushAdvisor(new SecurityAdvisor() {
			public SecurityAdvice isAllowed(String userId, String function,
					String reference) {
				return SecurityAdvice.ALLOWED;
			}
		});
	}

	private void popAdvisor() {
		SecurityService.popAdvisor();
	}

    public void synchroniseSiteMemberships(final String siteId, final String membershipsId, final String membershipsUrl, final String oauth_consumer_key, boolean isEmailTrustedConsumer, final String callbackType) {

        Site site = null;

        try {
            site = siteService.getSite(siteId);
        } catch (IdUnusedException iue) {
            log.error("site.notfound id: {}. This site's memberships will NOT be synchronised. {}", siteId, iue);
            return;
        }

        if (BasicLTIConstants.LTI_VERSION_1.equals(callbackType)) {
            synchronizeLTI1SiteMemberships(site, membershipsId, membershipsUrl, oauth_consumer_key, isEmailTrustedConsumer);
        } else if ("ext-moodle-2".equals(callbackType)) {
            // This is non standard. Moodle's core LTI plugin does not currently do memberships and 
            // a fix for this has been proposed at https://tracker.moodle.org/browse/MDL-41724. I don't
            // think this will ever become core and the first time memberships will appear in core lti
            // is with LTI2. At that point this code will be replaced with standard LTI2 JSON type stuff.
            synchronizeMoodleExtSiteMemberships(site, membershipsId, membershipsUrl, oauth_consumer_key, isEmailTrustedConsumer);
        }
    }

    private final void synchronizeLTI1SiteMemberships(final Site site, final String membershipsId, final String membershipsUrl, final String oauth_consumer_key, boolean isEmailTrustedConsumer) {

        // Lookup the secret
        final String configPrefix = "basiclti.provider." + oauth_consumer_key + ".";
        final String oauth_secret = serverConfigurationService.getString(configPrefix+ "secret", null);
        if (oauth_secret == null) {
            log.error("launch.key.notfound {}. This site's memberships will NOT be synchronised.", oauth_consumer_key);
            return;
        }

        OAuthMessage om = new OAuthMessage("POST", membershipsUrl, null);
        om.addParameter(OAuth.OAUTH_CONSUMER_KEY, oauth_consumer_key);
        om.addParameter(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
        om.addParameter(OAuth.OAUTH_VERSION, "1.0");
        om.addParameter(OAuth.OAUTH_TIMESTAMP, new Long((new Date().getTime()) / 1000).toString());
        om.addParameter(OAuth.OAUTH_NONCE, UUID.randomUUID().toString());
        om.addParameter(BasicLTIConstants.LTI_MESSAGE_TYPE, "basic-lis-readmembershipsforcontext");
        om.addParameter(BasicLTIConstants.LTI_VERSION, "LTI-1p0");
        om.addParameter("id", membershipsId);

        OAuthConsumer oc = new OAuthConsumer(null, oauth_consumer_key, oauth_secret, null);

        try {
            OAuthSignatureMethod osm = OAuthSignatureMethod.newMethod(OAuth.HMAC_SHA1, new OAuthAccessor(oc));
            osm.sign(om);

            URL url = new URL(membershipsUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setInstanceFollowRedirects(false); 
            connection.setRequestMethod("POST");
            connection.setUseCaches (false);
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
            bw.write(OAuth.formEncode(om.getParameters()));
            bw.flush();
            bw.close();

            processMembershipsResponse(connection, site, oauth_consumer_key, isEmailTrustedConsumer);
        } catch (Exception e) {
            log.warn("Problem synchronizing LTI1 memberships.", e);
        }
    }

    private final void synchronizeMoodleExtSiteMemberships(final Site site, final String membershipsId, final String membershipsUrl, final String oauth_consumer_key, boolean isEmailTrustedConsumer) {

        // Lookup the secret
        final String configPrefix = "basiclti.provider." + oauth_consumer_key + ".";
        final String oauth_secret = serverConfigurationService.getString(configPrefix+ "secret", null);
        if (oauth_secret == null) {
            log.error("launch.key.notfound {}. This site's memberships will NOT be synchronised.", oauth_consumer_key);
            return;
        }

        String type = "readMembershipsWithGroups";
        String uuid = UUID.randomUUID().toString();
        String xml = "<sourcedId>" + membershipsId + "</sourcedId>";

        StringBuilder sb = new StringBuilder("<?xml version = \"1.0\" encoding = \"UTF-8\"?>");
        sb.append("<imsx_POXEnvelope xmlns = \"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">");
        sb.append("<imsx_POXHeader>");
        sb.append("<imsx_POXRequestHeaderInfo>");
        sb.append("<imsx_version>V1.0</imsx_version>");
        sb.append("<imsx_messageIdentifier>" + uuid + "</imsx_messageIdentifier>");
        sb.append("</imsx_POXRequestHeaderInfo>");
        sb.append("</imsx_POXHeader>");
        sb.append("<imsx_POXBody>");
        sb.append("<" + type + "Request>");
        sb.append(xml);
        sb.append("</" + type + "Request>");
        sb.append("</imsx_POXBody>");
        sb.append("</imsx_POXEnvelope>");

        String callXml = sb.toString();

        if(log.isDebugEnabled()) log.debug("callXml: {}", callXml);

        String bodyHash = OAuthSignatureMethod.base64Encode(LegacyShaUtil.sha1(callXml));
        log.debug(bodyHash);

        OAuthMessage om = new OAuthMessage("POST", membershipsUrl, null);
        om.addParameter("oauth_body_hash", bodyHash);
        om.addParameter("oauth_consumer_key", oauth_consumer_key);
        om.addParameter("oauth_signature_method", "HMAC-SHA1");
        om.addParameter("oauth_version", "1.0");
        om.addParameter("oauth_timestamp", new Long(new Date().getTime()).toString());

        OAuthConsumer oc = new OAuthConsumer(null, oauth_consumer_key, oauth_secret, null);

        try {
            OAuthSignatureMethod osm = OAuthSignatureMethod.newMethod("HMAC-SHA1",new OAuthAccessor(oc));
            osm.sign(om);

            String authzHeader = om.getAuthorizationHeader(null);

            if(log.isDebugEnabled()) log.debug("AUTHZ HEADER: {}", authzHeader);

            URL url = new URL(membershipsUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setInstanceFollowRedirects(false); 
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Authorization", authzHeader);
            connection.setRequestProperty("Content-Length", "" + Integer.toString(callXml.getBytes().length));
            connection.setRequestProperty("Content-Type", "text/xml");
            connection.setUseCaches (false);
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
            bw.write(callXml);
            bw.flush();
            bw.close();

            processMembershipsResponse(connection, site, oauth_consumer_key, isEmailTrustedConsumer);
        } catch (Exception e) {
            log.warn("Problem synchronizing Mooodle memberships.", e);
        }
    }

    private void processMembershipsResponse(HttpURLConnection connection, Site site, String oauth_consumer_key, boolean isEmailTrustedConsumer) throws Exception {

        log.debug("processMembershipsResponse");

        BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        POXMembershipsResponse poxMembershipsResponse = new POXMembershipsResponse(br);

        connection.disconnect();

        List<POXMembershipsResponse.Member> members = poxMembershipsResponse.getMembers();

        Map<String,List<POXMembershipsResponse.Member>> consumerGroups = poxMembershipsResponse.getGroups();

        if (log.isDebugEnabled()) {
            for (POXMembershipsResponse.Member member : members) {
                log.debug("Member:");
                log.debug("\tUser ID: {}", member.userId);
                log.debug("\tFirst Name: {}", member.firstName);
                log.debug("\tLast Name: {}", member.lastName);
                log.debug("\tEmail: {}", member.email);
                log.debug("\tRole: {}", member.role);
            }

            for (String groupTitle : consumerGroups.keySet()) {
                log.debug("Group: {}", groupTitle);
                for(POXMembershipsResponse.Member groupMember : consumerGroups.get(groupTitle)) {
                    log.debug("\tGroup Member ID: {}", groupMember.userId);
                }
            }
        }

        site.removeMembers();

        for (POXMembershipsResponse.Member member : members) {

            Map map = new HashMap();
            map.put(BasicLTIConstants.USER_ID, member.userId);
            map.put(BasicLTIConstants.LIS_PERSON_NAME_GIVEN, member.firstName);
            map.put(BasicLTIConstants.LIS_PERSON_NAME_FAMILY, member.lastName);
            map.put(BasicLTIConstants.LIS_PERSON_CONTACT_EMAIL_PRIMARY, member.email);
            map.put(BasicLTIConstants.ROLES, member.role);
            map.put(OAuth.OAUTH_CONSUMER_KEY, oauth_consumer_key);
            map.put("tool_id", "n/a");

            User user = userFinderOrCreator.findOrCreateUser(map, false,isEmailTrustedConsumer);
            member.userId = user.getId();
            siteMembershipUpdater.addOrUpdateSiteMembership(map, false, user, site);
        }

        // Do this so we don't get a concurrent mod exception
        List groups = new ArrayList(site.getGroups());

        // Remove the existing groups
        for (Iterator i = groups.iterator(); i.hasNext(); ) {
            try {
                site.deleteGroup((Group) i.next());
            } catch (IllegalStateException e) {
                log.error(".processMembershipsResponse: Group with id {} cannot be removed because is locked", ((Group) i).getId());
            }
        }

        for (String consumerGroupTitle : consumerGroups.keySet()) {
            if (log.isDebugEnabled()) {
                log.debug("Creating group with title '{}' ...", consumerGroupTitle);
            }

            Group sakaiGroup = site.addGroup();
            sakaiGroup.getProperties().addProperty(sakaiGroup.GROUP_PROP_WSETUP_CREATED, Boolean.TRUE.toString());
            sakaiGroup.setTitle(consumerGroupTitle);

            for (POXMembershipsResponse.Member consumerGroupMember : consumerGroups.get(consumerGroupTitle)) {
                if (log.isDebugEnabled()) {
                    log.debug("Adding '{} {}' to '{}' ...", consumerGroupMember.firstName, consumerGroupMember.lastName, consumerGroupTitle);
                }

                try {
                    sakaiGroup.insertMember(consumerGroupMember.userId, consumerGroupMember.role, true, false);
                } catch (IllegalStateException e) {
                    log.error(".processMembershipsResponse: User with id {} cannot be inserted in group with id {} because the group is locked", consumerGroupMember.userId, sakaiGroup.getId());
                }
            }
        }

        pushAdvisor();
        try {
            siteService.save(site);
            log.info("Updated  site={}", site.getId());
        } catch (Exception e) {
            //M_log.error("Failed to add group '" + consumerGroupTitle + "' to site", e);
            log.info("Failed to update site={}", site.getId());
        } finally {
            popAdvisor();
        }
    }
}