package cz.muni.ics.oidc.server.ga4gh;

import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import cz.muni.ics.oauth2.model.OAuth2AccessTokenEntity;
import cz.muni.ics.oauth2.service.OAuth2TokenEntityService;
import cz.muni.ics.oauth2.token.TokenExchangeGranter;
import cz.muni.ics.oauth2.token.exchange.BaseTokenExchangeGranter;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.Map;
import java.util.Set;

import static cz.muni.ics.oauth2.token.TokenExchangeGranter.ISSUED_TOKEN_TYPE;
import static cz.muni.ics.oauth2.token.TokenExchangeGranter.TOKEN_TYPE_ACCESS_TOKEN;
import static cz.muni.ics.oidc.server.ga4gh.Ga4ghApiClaimSource.GA4GH_SCOPE;

@Component("ga4ghTokenExchangeTokenGranter")
@Slf4j
public class Ga4ghTokenExchangeGranter extends BaseTokenExchangeGranter implements InitializingBean {

	public static final String TOKEN_TYPE_GA4GH_PASSPORT = "urn:ga4gh:params:oauth:token-type:passport";

	@Getter(onMethod_ = @Override)
	private final Set<String> supportedSubjectTokenTypes = Set.of(TOKEN_TYPE_ACCESS_TOKEN);

	@Getter(onMethod_ = @Override)
	private final Set<String> supportedRequestedTokenTypes = Set.of(TOKEN_TYPE_GA4GH_PASSPORT);

	@Getter(onMethod_ = @Override)
	private final Set<String> supportedActorTokenTypes = Set.of(TOKEN_TYPE_ACCESS_TOKEN);

	@Autowired
	@Setter
	private PerunOidcConfig perunOidcConfig;

	@Getter(onMethod_ = @Override)
	private boolean supported = false;

	private String brokerUrl;

	private HttpClient httpClient;

	protected Ga4ghTokenExchangeGranter(PerunOidcConfig perunOidcConfig,
										OAuth2TokenEntityService tokenServices,
										TokenExchangeGranter tokenExchangeGranter)
	{
		super(tokenServices, tokenExchangeGranter);
		this.perunOidcConfig = perunOidcConfig;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		if (StringUtils.hasText(perunOidcConfig.getGa4ghTokenExchangeBrokerUrl())) {
			log.debug("Enabling GA4GH token exchange support");
			supported = true;
			httpClient = HttpClient.newHttpClient();
			this.brokerUrl = perunOidcConfig.getGa4ghTokenExchangeBrokerUrl();
		}
	}

	@Override
	protected boolean validateCallingClient(ClientDetails client) {
		if (client == null || client.getScope() == null || !client.getScope().contains(GA4GH_SCOPE)) {
			log.debug("Client is not authorized to get GA4GH Passports");
			return false;
		}
		return true;
	}

	@Override
	protected boolean validateIncomingAccessToken(OAuth2AccessTokenEntity incomingToken) {
		super.validateIncomingAccessToken(incomingToken);
		if (!incomingToken.getScope().contains(GA4GH_SCOPE)) {
			log.debug("Provided access token is not valid GA4GH access token");
			return false;
		}
		return true;
	}

	@Override
	protected OAuth2AccessToken getAccessToken(ClientDetails clientDetails,
                                               OAuth2AccessTokenEntity accessToken,
                                               String requestedTokenType,
                                               TokenRequest tokenRequest)
	{
		try {
			HttpRequest request = HttpRequest.newBuilder()
					.GET()
					.uri(new URI(brokerUrl))
					.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getValue())
					.build();
			HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
			if (response.statusCode() == 200) {
				SignedJWT jwt = SignedJWT.parse(response.body());
				JWTClaimsSet claimsSet = jwt.getJWTClaimsSet();
				return new Ga4ghPassportToken(claimsSet, response.body());
			} else {
				log.warn("Contacting GA4GH Broker returned non-200 response code ({})", response.statusCode());
				log.debug("Response body: {}", response.body());
			}
		} catch (Exception ex) {
			log.warn("Caught exception when exchanging token for GA4GH passport token", ex);
		}
		throw OAuth2Exception.create(OAuth2Exception.ERROR, "failed to execute token-exchange");
	}

	@Override
	public String toString() {
		return "Ga4ghTokenExchangeGranter{" +
				"supportedSubjectTokenTypes=" + supportedSubjectTokenTypes +
				", supportedRequestedTokenTypes=" + supportedRequestedTokenTypes +
				", supportedActorTokenTypes=" + supportedActorTokenTypes +
				", supported=" + supported +
				", brokerUrl='" + brokerUrl + '\'' +
				'}';
	}

	@Getter(onMethod_ = @Override)
	private static final class Ga4ghPassportToken implements OAuth2AccessToken {

		private final String value;

		private final Date expiration;

		private final int expiresIn;

		private final boolean expired;

		Ga4ghPassportToken(JWTClaimsSet claimsSet, String token) {
			if (!StringUtils.hasText(token)) {
				throw new IllegalArgumentException("No token value provided");
			}
			this.value = token;
			if (claimsSet != null && claimsSet.getExpirationTime() != null) {
				this.expiration = claimsSet.getExpirationTime();
			} else {
				this.expiration = new Date(LocalDateTime.now().plusYears(100).toEpochSecond(ZoneOffset.UTC));
			}
			long diffSeconds = (expiration.getTime() - new Date().getTime()) / 1000;
			this.expiresIn = (int) (diffSeconds < 0 ? 0 : diffSeconds);
			this.expired =  diffSeconds <= 0;
		}

		@Override
		public Map<String, Object> getAdditionalInformation() {
			return Map.of(ISSUED_TOKEN_TYPE, TOKEN_TYPE_GA4GH_PASSPORT);
		}

		@Override
		public Set<String> getScope() {
			return Set.of();
		}

		@Override
		public OAuth2RefreshToken getRefreshToken() {
			return () -> "";
		}

		@Override
		public String getTokenType() {
			return "Bearer";
		}
	}

}
