/**
 * BladeX Commercial License Agreement
 * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
 * <p>
 * Use of this software is governed by the Commercial License Agreement
 * obtained after purchasing a license from BladeX.
 * <p>
 * 1. This software is for development use only under a valid license
 * from BladeX.
 * <p>
 * 2. Redistribution of this software's source code to any third party
 * without a commercial license is strictly prohibited.
 * <p>
 * 3. Licensees may copyright their own code but cannot use segments
 * from this software for such purposes. Copyright of this software
 * remains with BladeX.
 * <p>
 * Using this software signifies agreement to this License, and the software
 * must not be used for illegal purposes.
 * <p>
 * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
 * not liable for any claims arising from secondary or illegal development.
 * <p>
 * Author: Chill Zhuang (bladejava@qq.com)
 */
package org.springblade.auth.granter;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springblade.auth.dto.IamSsoProfileResponse;
import org.springblade.auth.dto.IamSsoTokenResponse;
import org.springblade.auth.props.IamSsoProperties;
import org.springblade.auth.utils.TokenUtil;
import org.springblade.core.mp.enums.StatusType;
import org.springblade.core.oauth2.constant.OAuth2TokenConstant;
import org.springblade.core.oauth2.exception.UserInvalidException;
import org.springblade.core.oauth2.granter.AuthorizationCodeGranter;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.entity.UserInfo;
import org.springblade.system.pojo.enums.UserType;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * IAM统一身份认证授权器
 *
 * @author Chill
 */
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class IamSsoTokenGranter extends AuthorizationCodeGranter {

	private static final String GRANT_TYPE = "iam_sso";
	private static final String IAM_GRANT_TYPE = "authorization_code";
	private static final String IAM_TOKEN_URI = "/iam/sso/token";
	private static final String BEARER_PREFIX = "Bearer ";
	private static final String BASIC_PREFIX = "Basic ";

	private final OAuth2ClientService clientService;
	private final IUserClient userClient;
	private final IamSsoProperties properties;
	private final ObjectMapper objectMapper;
	private final HttpClient httpClient;

	public IamSsoTokenGranter(OAuth2ClientService clientService,
							  OAuth2UserService userService,
							  PasswordHandler passwordHandler,
							  BladeRedis bladeRedis,
							  IUserClient userClient,
							  IamSsoProperties properties,
							  ObjectMapper objectMapper) {
		super(clientService, userService, passwordHandler, bladeRedis);
		this.clientService = clientService;
		this.userClient = userClient;
		this.properties = properties;
		this.objectMapper = objectMapper;
		this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
	}

	@Override
	public String type() {
		return GRANT_TYPE;
	}

	@Override
	public OAuth2Client client(OAuth2Request request) {
		String clientId = StringUtil.isBlank(request.getClientId()) ? properties.getSystemClientId() : request.getClientId();
		String clientSecret = StringUtil.isBlank(request.getClientSecret()) ? properties.getSystemClientSecret() : request.getClientSecret();
		OAuth2Client client = clientService.loadByClientId(clientId);
		if (!clientService.validateClient(client, clientId, clientSecret)) {
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		}
		return client;
	}

	@Override
	public OAuth2User user(OAuth2Request request) {
		if (!isDedicatedIamEndpoint(request)) {
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		}
		if (!isIamRequest(request, true)) {
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		}
		validateRequest(request);
		// 先校验本系统客户端，IAM的redirect_uri由后续远端接口校验。
		client(request);

		IamSsoTokenResponse tokenResponse = requestIamToken(request);
		String accessToken = tokenResponse.getAccessToken();
		if (StringUtil.isBlank(accessToken)) {
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		}

		IamSsoProfileResponse profileResponse = requestIamProfile(accessToken);
		String accountNo = profileResponse.getAccountNo();
		if (StringUtil.isBlank(accountNo)) {
			log.warn("IAM统一身份认证用户信息缺少account_no，iamId={}", profileResponse.getId());
			throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
		}

		String tenantId = resolveTenantId(request);
		if (StringUtil.isBlank(tenantId)) {
			log.warn("IAM统一身份认证请求缺少租户ID，accountNo={}", accountNo);
			throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
		}
		UserInfo userInfo = loadOrCreateIamUser(request, profileResponse, tenantId, accountNo);
		OAuth2User user = TokenUtil.convertUser(userInfo, request);
		if (user == null) {
			throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
		}
		user.setClient(client(request));
		return user;
	}

	private UserInfo loadOrCreateIamUser(OAuth2Request request, IamSsoProfileResponse profileResponse, String tenantId, String accountNo) {
		R<UserInfo> result = userClient.userInfo(tenantId, accountNo);
		if (result.isSuccess() && result.getData() != null) {
			return result.getData();
		}
		log.info("IAM统一身份认证未匹配到本系统账号，开始自动创建用户，tenantId={}, accountNo={}", tenantId, accountNo);
		R<Boolean> saveResult = userClient.saveUser(buildIamUser(profileResponse, tenantId, accountNo));
		if (!saveResult.isSuccess() || !Boolean.TRUE.equals(saveResult.getData())) {
			log.warn("IAM统一身份认证自动创建用户失败，tenantId={}, accountNo={}, msg={}", tenantId, accountNo, saveResult.getMsg());
			throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
		}
		R<UserInfo> createdResult = userClient.userInfo(tenantId, accountNo);
		if (!createdResult.isSuccess() || createdResult.getData() == null) {
			log.warn("IAM统一身份认证自动创建用户后未查询到用户，tenantId={}, accountNo={}", tenantId, accountNo);
			throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND);
		}
		return createdResult.getData();
	}

	private User buildIamUser(IamSsoProfileResponse profileResponse, String tenantId, String accountNo) {
		User user = new User();
		user.setTenantId(tenantId);
		user.setUserType(UserType.WEB.getCategory());
		user.setAccount(accountNo);
		user.setPassword(accountNo);
		user.setName(accountNo);
		user.setRealName(accountNo);
		user.setRoleId(StringPool.MINUS_ONE);
		user.setDeptId(StringPool.MINUS_ONE);
		user.setPostId(StringPool.MINUS_ONE);
		user.setStatus(StatusType.ACTIVE.getType());
		log.info("IAM统一身份认证自动创建用户参数，tenantId={}, accountNo={}, iamId={}", tenantId, accountNo, profileResponse.getId());
		return user;
	}

	public boolean supports(OAuth2Request request) {
		return isIamRequest(request, false);
	}

	private boolean isDedicatedIamEndpoint(OAuth2Request request) {
		return request.getHttpRequest() != null
			&& request.getHttpRequest().getRequestURI().endsWith(IAM_TOKEN_URI);
	}

	private boolean isIamRequest(OAuth2Request request, boolean logMiss) {
		String redirectUri = normalizeRedirectUri(request.getRedirectUri());
		boolean iamRequest = StringUtil.isNotBlank(properties.getRedirectUri())
			&& StringUtil.isNotBlank(request.getCode())
			&& StringUtil.equals(properties.getRedirectUri(), redirectUri);
		if (!iamRequest && logMiss) {
			log.info("IAM统一身份认证请求未命中，configRedirectUri={}, requestRedirectUri={}, normalizedRedirectUri={}, code={}",
				properties.getRedirectUri(), request.getRedirectUri(), redirectUri, request.getCode());
		}
		return iamRequest;
	}

	private String resolveTenantId(OAuth2Request request) {
		if (StringUtil.isNotBlank(request.getTenantId())) {
			return request.getTenantId();
		}
		if (request.getHttpRequest() == null) {
			return null;
		}
		return request.getHttpRequest().getParameter("tenantId");
	}

	private String normalizeRedirectUri(String redirectUri) {
		if (StringUtil.isBlank(redirectUri)) {
			return redirectUri;
		}
		try {
			return URLDecoder.decode(redirectUri, StandardCharsets.UTF_8);
		} catch (IllegalArgumentException exception) {
			log.warn("IAM统一身份认证redirect_uri解码失败，redirectUri={}", redirectUri, exception);
			return redirectUri;
		}
	}

	private void validateRequest(OAuth2Request request) {
		if (StringUtil.isBlank(request.getCode())) {
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		}
		if (StringUtil.isAnyBlank(
			properties.getTokenUrl(),
			properties.getProfileUrl(),
			properties.getClientId(),
			properties.getClientSecret(),
			properties.getRedirectUri()
		)) {
			throw new UserInvalidException("IAM统一身份认证配置不完整");
		}
	}

	private IamSsoTokenResponse requestIamToken(OAuth2Request request) {
		try {
			Map<String, String> tokenParams = buildTokenParams(request);
			String tokenBody = buildTokenBody(tokenParams);
			HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(properties.getTokenUrl()))
				.timeout(Duration.ofSeconds(10))
				.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
				.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
				.header(HttpHeaders.AUTHORIZATION, authorizationHeader())
				.POST(HttpRequest.BodyPublishers.ofString(tokenBody, StandardCharsets.UTF_8))
				.build();
			log.info("IAM统一身份认证换取Token请求，method={}, headers={}, body={}", httpRequest.method(), httpRequest.headers().map(), tokenBody);
			HttpResponse<String> response = httpClient.send(
				httpRequest,
				HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
			);
			if (response.statusCode() < 200 || response.statusCode() >= 300) {
				log.warn("IAM统一身份认证换取Token失败，status={}", response.statusCode());
				throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
			}
			return objectMapper.readValue(response.body(), IamSsoTokenResponse.class);
		} catch (InterruptedException exception) {
			Thread.currentThread().interrupt();
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		} catch (IOException | IllegalArgumentException exception) {
			log.warn("IAM统一身份认证换取Token异常", exception);
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		}
	}

	private IamSsoProfileResponse requestIamProfile(String accessToken) {
		try {
			HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(buildProfileUrl(accessToken)))
				.timeout(Duration.ofSeconds(10))
				.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
				.header(HttpHeaders.AUTHORIZATION, profileAuthorizationHeader(accessToken))
				.GET()
				.build();
			log.info("IAM统一身份认证获取用户信息请求，headers={}, body={}", httpRequest.headers().map(), "无");
			HttpResponse<String> response = httpClient.send(
				httpRequest,
				HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
			);
			log.info("IAM统一身份认证获取用户信息响应，headers={}, body={}", response.headers().map(), response.body());
			if (response.statusCode() < 200 || response.statusCode() >= 300) {
				log.warn("IAM统一身份认证获取用户信息失败，status={}", response.statusCode());
				throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
			}
			return objectMapper.readValue(response.body(), IamSsoProfileResponse.class);
		} catch (InterruptedException exception) {
			Thread.currentThread().interrupt();
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		} catch (IOException | IllegalArgumentException exception) {
			log.warn("IAM统一身份认证获取用户信息异常", exception);
			throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT);
		}
	}

	private Map<String, String> buildTokenParams(OAuth2Request request) {
		Map<String, String> params = new LinkedHashMap<>();
		params.put("grant_type", IAM_GRANT_TYPE);
		params.put("client_id", properties.getClientId());
		params.put("client_secret", properties.getClientSecret());
		params.put("code", request.getCode());
		if (StringUtil.isNotBlank(request.getState())) {
			params.put("state", request.getState());
		}
		params.put("redirect_uri", properties.getRedirectUri());
		return params;
	}

	private String buildTokenBody(Map<String, String> params) {
		return params.entrySet().stream()
			.map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
			.collect(Collectors.joining("&"));
	}

	private String buildProfileUrl(String accessToken) {
		String separator = properties.getProfileUrl().contains("?") ? "&" : "?";
		return properties.getProfileUrl() + separator + "access_token=" + encode(accessToken);
	}

	private String authorizationHeader() {
		if (StringUtil.isNotBlank(properties.getAuthorization())) {
			return withBasicPrefix(properties.getAuthorization());
		}
		String token = Base64.getEncoder().encodeToString(
			(properties.getClientId() + ":" + properties.getClientSecret()).getBytes(StandardCharsets.UTF_8)
		);
		return BASIC_PREFIX + token;
	}

	private String withBasicPrefix(String authorization) {
		if (StringUtil.startsWithIgnoreCase(authorization, BASIC_PREFIX)) {
			return authorization;
		}
		return BASIC_PREFIX + authorization;
	}

	private String profileAuthorizationHeader(String accessToken) {
		if (StringUtil.isNotBlank(properties.getProfileAuthorization())) {
			return withBasicPrefix(properties.getProfileAuthorization());
		}
		return BEARER_PREFIX + accessToken;
	}

	private String encode(String value) {
		return URLEncoder.encode(value, StandardCharsets.UTF_8);
	}

}
