package cz.muni.ics.oidc.server.filters.impl;

import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oidc.BeanUtil;
import cz.muni.ics.oidc.exceptions.ConfigurationException;
import cz.muni.ics.oidc.saml.SamlProperties;
import cz.muni.ics.oidc.server.filters.AuthProcFilter;
import cz.muni.ics.oidc.server.filters.AuthProcFilterCommonVars;
import cz.muni.ics.oidc.server.filters.AuthProcFilterInitContext;
import cz.muni.ics.oidc.server.filters.FiltersUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;

import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.apache.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;


/**
 * Filter for collecting data about login.
 *
 * Configuration (replace [name] part with the name defined for the filter):
 * @see AuthProcFilter (basic configuration options)
 * <ul>
 *     <li><b>filter.[name].idpNameAttributeName</b> - Mapping to Request attribute containing name of used
 *         Identity Provider</li>
 *     <li><b>filter.[name].idpEntityIdAttributeName</b> - Mapping to Request attribute containing entity_id of used
 *         Identity Provider</li>
 *     <li><b>filter.[name].apiUrl</b> - API location of the write endpoint</li>
 *     <li><b>filter.[name].apiUsername</b> - Username for basic auth to call API endpoint</li>
 *     <li><b>filter.[name].apiPassword</b> - Password for basic auth to call API endpoint</li>
 *     <li><b>filter.[name].usernameAttribute</b> - Custom mapping of SAML username attribute, otherwise SAML
 *         properties configured attribute is used</li>
 * </ul>
 *
 * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
 */
@SuppressWarnings("SqlResolve")
@Slf4j
public class ApiProxyStatisticsFilter extends AuthProcFilter {

	/* CONFIGURATION OPTIONS */
	private static final String IDP_NAME_ATTRIBUTE_NAME = "idpNameAttributeName";
	private static final String IDP_ENTITY_ID_ATTRIBUTE_NAME = "idpEntityIdAttributeName";
	private static final String API_URL = "apiUrl";
	private static final String API_USERNAME = "apiUsername";
	private static final String API_PASSWORD = "apiPassword";
	private static final String USERNAME_ATTRIBUTE = "usernameAttribute";

	private final String idpNameAttributeName;
	private final String idpEntityIdAttributeName;
	private final String apiUrl;
	private final String apiUsername;
	private final String apiPassword;
	private final String usernameAttribute;
	/* END OF CONFIGURATION OPTIONS */

	private final HttpClient httpClient;

	public ApiProxyStatisticsFilter(AuthProcFilterInitContext ctx) throws ConfigurationException {
		super(ctx);
		BeanUtil beanUtil = ctx.getBeanUtil();
		SamlProperties samlProperties = beanUtil.getBean(SamlProperties.class);

		this.idpNameAttributeName = FiltersUtils.fillStringPropertyOrDefaultVal(IDP_NAME_ATTRIBUTE_NAME, ctx,
				"urn:cesnet:proxyidp:attribute:sourceIdPName");
		this.idpEntityIdAttributeName = FiltersUtils.fillStringPropertyOrDefaultVal(IDP_ENTITY_ID_ATTRIBUTE_NAME, ctx,
				"urn:cesnet:proxyidp:attribute:sourceIdPEntityID");
		this.apiUrl = FiltersUtils.fillStringMandatoryProperty(API_URL, ctx);
		this.apiUsername = FiltersUtils.fillStringMandatoryProperty(API_USERNAME, ctx);
		this.apiPassword = FiltersUtils.fillStringMandatoryProperty(API_PASSWORD, ctx);
		this.usernameAttribute = FiltersUtils.fillStringProperty(USERNAME_ATTRIBUTE, ctx, samlProperties.getUserIdentifierAttribute());
		httpClient = HttpClient.newBuilder()
				.authenticator(new Authenticator() {
					@Override
					protected PasswordAuthentication getPasswordAuthentication() {
						return new PasswordAuthentication(apiUsername, apiPassword.toCharArray());
					}
				}).build();
	}

	@Override
	protected boolean process(HttpServletRequest req, HttpServletResponse res, AuthProcFilterCommonVars params) {
		ClientDetailsEntity client = params.getClient();
		if (client == null) {
			log.warn("{} - skip execution: no client provided", getFilterName());
			return true;
		} else if (!StringUtils.hasText(client.getClientId())) {
			log.warn("{} - skip execution: no client identifier provided", getFilterName());
			return true;
		} else if (!StringUtils.hasText(client.getClientName())) {
			log.warn("{} - skip execution: no client name provided", getFilterName());
			return true;
		}

		SAMLCredential samlCredential = FiltersUtils.getSamlCredential(req);
		if (samlCredential == null) {
			log.warn("{} - skip execution: no authN object available, cannot extract user identifier and idp identifier",
					getFilterName());
			return true;
		}
		String userIdentifier = FiltersUtils.getExtLogin(samlCredential, usernameAttribute);
		if (!StringUtils.hasText(userIdentifier)) {
			log.warn("{} - skip execution: no user identifier provided", getFilterName());
			return true;
		} else if (!StringUtils.hasText(samlCredential.getAttributeAsString(idpEntityIdAttributeName))) {
			log.warn("{} - skip execution: no authenticating idp identifier provided", getFilterName());
			return true;
		} else if (!StringUtils.hasText(samlCredential.getAttributeAsString(idpNameAttributeName))) {
			log.warn("{} - skip execution: no authenticating idp identifier provided", getFilterName());
			return true;
		}

		String idpEntityId = changeParamEncoding(samlCredential.getAttributeAsString(idpEntityIdAttributeName));
		String idpName = changeParamEncoding(samlCredential.getAttributeAsString(idpNameAttributeName));
		String clientId = client.getClientId();
		String clientName = client.getClientName();

		callApi(idpEntityId, idpName, clientId, clientName, userIdentifier);

		log.info("{} - User identity: {}, service: {}, serviceName: {}, via IdP: {}",
				getFilterName(), userIdentifier, client.getClientId(), client.getClientName(), idpEntityId);
		return true;
	}

	@Override
	protected boolean oncePerSession() {
		return true;
	}

	private String changeParamEncoding(String original) {
		if (original != null && !original.isEmpty()) {
			byte[] sourceBytes = original.getBytes(ISO_8859_1);
			return new String(sourceBytes, UTF_8);
		}

		return null;
	}

	private void callApi(String idpEntityId, String idpName, String spIdentifier, String spName, String userId) {
		try {
			ObjectNode body = JsonNodeFactory.instance.objectNode();
			body.put("userId", userId);
			body.put("serviceIdentifier", spIdentifier);
			body.put("serviceName", spName);
			body.put("idpIdentifier", idpEntityId);
			body.put("idpName", idpName);
			HttpRequest request = HttpRequest.newBuilder()
					.POST(HttpRequest.BodyPublishers.ofString(body.toPrettyString()))
					.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
					.header(AUTHORIZATION, getBasicAuthenticationHeader())
					.uri(new URI(this.apiUrl))
					.build();
			log.debug("Contacting statistics API endpoint '{}', body: '{}'",
					this.apiUrl, body);
			HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
			if (response.statusCode() != 200) {
				log.warn("Contacting statistics API endpoint returned non-200 response code ({})", response.statusCode());
				log.debug("Response body: {}", response.body());
			}
		} catch (Exception ex) {
			log.warn("Caught exception when writing statistics via API", ex);
		}
	}

	private String getBasicAuthenticationHeader() {
		String valueToEncode = apiUsername + ":" + apiPassword;
		return "Basic " + Base64.getEncoder().encodeToString(valueToEncode.getBytes());
	}

}
