package com.lutao.cloud.auth.service;
/**
 * Copyright (c) 2022 cloud Authors. All Rights Reserved.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.net.URLDecoder;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.XML;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lutao.cloud.admin.api.dto.UserInfo;
import com.lutao.cloud.admin.api.feign.RemoteUserService;
import com.lutao.cloud.common.core.constant.CacheConstants;
import com.lutao.cloud.common.core.constant.SecurityConstants;
import com.lutao.cloud.common.core.util.*;
import com.lutao.cloud.common.security.service.CloudUser;
import com.lutao.cloud.common.security.service.CloudUserDetailsService;
import com.lutao.cloud.common.security.sso.SSOAuthenticationToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static com.lutao.cloud.common.core.util.MessageInfo.ok;

/**

 * @date 2020/9/3
 * <p>
 * token 端点相关的业务处理
 */
@Slf4j
@Service
public class CloudTokenDealServiceImpl {
	@Autowired
	private RedisTemplate redisTemplate;

	@Autowired
	private CacheManager cacheManager;

	@Autowired
	private TokenStore tokenStore;

	@Autowired
	private KeyStrResolver keyStrResolver;

	@Autowired
	private RemoteUserService userService;

	@Autowired
	private CloudUserDetailsService cloudUserDetailsService;

	@Value("${sso.logout.url}")
	private String ssoUrl;

	@Value("${sso.login-url}")
	private String loginUrl;

	/**
	 * 分页查询token 列表
	 * @param page page
	 * @return
	 */
	public MessageInfo<Page> queryToken(Page page) {
		// 根据分页参数获取对应数据
		String key = keyStrResolver.extract(SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX + "access:*",
				StrUtil.COLON);
		List<String> pages = findKeysForPage(key, page.getCurrent(), page.getSize());
		redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
		page.setRecords(redisTemplate.opsForValue().multiGet(pages));
		page.setTotal(redisTemplate.keys(key).size());
		return ok(page);
	}

	/**
	 * 通过token查询用户信息和权限信息
	 * @param token
	 * @return
	 */
	public  MessageInfo  userByToken(String token) {
		OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
		if (accessToken == null) {
			return MessageInfo.failed("token 无效");
		}
		OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
		if (auth2Authentication == null) {
			return MessageInfo.failed("token 无效");
		}
		String name = auth2Authentication.getName();
		if (StrUtil.isBlank(name)) {
			return MessageInfo.failed("用户不存在");
		}
		MessageInfo<UserInfo> info = userService.info(name,SecurityConstants.FROM_IN);
		System.err.println(info);
		JSONObject jsonObject = new JSONObject();

		CloudUser cloudUser = cloudUserDetailsService.getUserDetails(info);
		jsonObject.putOnce("userId",cloudUser.getId());
		jsonObject.putOnce("username",cloudUser.getUsername());
		jsonObject.putOnce("nickname",cloudUser.getNickname());
		jsonObject.putOnce("authorities",info.getData().getPermissions());
		return  ok(jsonObject);
	}

	/**
	 * 删除 请求令牌 和 刷新令牌
	 * @param token 请求令牌
	 * @return R
	 * 1.删除token
	 * 2.清空用户的缓存信息
	 * 3.发出退出事件
	 */
	public MessageInfo<Boolean> removeToken(String token) {
		OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
		if (accessToken == null || StrUtil.isBlank(accessToken.getValue())){
			return ok(Boolean.TRUE, "退出失败，token 无效");
		}

		OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);

		// 清空用户信息
		cacheManager.getCache(CacheConstants.USER_DETAILS).evict(auth2Authentication.getName());

		// 清空access token
		tokenStore.removeAccessToken(accessToken);

		// 清空 refresh token
		OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
		if (refreshToken != null) {
			tokenStore.removeRefreshToken(refreshToken);
		}

		// 发送退出成功事件
		SpringContextHolder.publishEvent(new LogoutSuccessEvent(auth2Authentication));

		return ok();
	}

	/**
	 * 删除 请求令牌 和 刷新令牌
	 * @param ticket 请求令牌
	 * @return R
	 * 1.删除token
	 * 2.清空用户的缓存信息
	 * 3.发出退出事件
	 */
	public MessageInfo<String> removeSSOToken(String ticket, String authHeader) {
		// sso登出
		if(StringUtils.isNotBlank(ticket)) {
			RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
			redisTemplate.setValueSerializer(new StringRedisSerializer());
			String key = new String(serializeKey(SSO_TICKET_TO_ACCESS + ticket), StandardCharsets.UTF_8);
			String loginUser  = (String) redisTemplate.boundValueOps(key).get();
			redisTemplate.setValueSerializer(valueSerializer);
			redisTemplate.delete(key);
			if(StringUtils.isNotEmpty(loginUser)) {
				List<OAuth2AccessToken> tokens = queryTokenByUsername(loginUser);
				if(CollUtil.isNotEmpty(tokens)) {
					tokens.forEach(token -> removeToken(token.getValue()));
				}
			}

			MessageInfo<String> messageInfo = MessageInfo.ok();
			messageInfo.setData(ssoUrl + "/logout?service=" + loginUrl);
			return messageInfo;
		} else { // token登出
			String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, StrUtil.EMPTY).trim();
			OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
			if(accessToken != null) {
				OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
				if(auth2Authentication.getUserAuthentication() instanceof SSOAuthenticationToken) {
					String ticketStr = ((SSOAuthenticationToken) auth2Authentication.getUserAuthentication()).getTicket();
					if(StringUtil.isNoneBlank(ticketStr)) {
						MessageInfo<String> messageInfo = removeSSOToken(ticketStr, authHeader);
						messageInfo.setData(ssoUrl + "/logout?service=" + loginUrl);
						return messageInfo;
					}
				}
			}

			removeToken(tokenValue);

			MessageInfo<String> messageInfo = MessageInfo.ok();
			messageInfo.setData("/#/login");
			return messageInfo;
		}
	}

	private static final String SSO_TICKET_TO_ACCESS = "sso_ticket_to_access:";
	private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();

	private byte[] serializeKey(String object){
		return serialize(keyStrResolver.extract((SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX + object), StrUtil.COLON));
	}

	private byte[] serialize(String string){
		return serializationStrategy.serialize(string);
	}

	/**
	 * 根据用户名查询 令牌相关信息
	 * @param page 分页信息
	 * @param username 用户名
	 * @return R
	 * 1.获取缓存key
	 * 2.获取tokenList
	 */
	public MessageInfo queryTokenByUsername(Page page, String username){
		// 1.获取缓存key
		String key = keyStrResolver.extract(
				SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX + "uname_to_access_z:", StrUtil.COLON);

		// 2.获取tokenList
		Object collect = redisTemplate.keys("*:" + username).stream().filter(k -> ((String) k).contains(key))
				.flatMap(k ->{
					redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
					return redisTemplate.opsForZSet().range(k, 0, System.currentTimeMillis()).stream();
				}).collect(Collectors.toList());

		if (collect instanceof List){
			List objSet = (List<?>) collect;
			page.setRecords(objSet);
			page.setTotal(objSet.size());
		}
		return ok(page);
	}

	/**
	 * 根据用户名查询 令牌相关信息
	 * @param username 用户名
	 * @return R
	 * 	1.获取缓存key
	 * 	2.获取tokenList
	 */
	public List queryTokenByUsername(String username){
		String key = keyStrResolver.extract(
				SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX + "uname_to_access_z:", StrUtil.COLON);

		List collect = (List)redisTemplate.keys("*:" + username).stream().filter(k -> ((String) k).contains(key))
				.flatMap(k ->{
					redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
					return redisTemplate.opsForZSet().range(k, 0, System.currentTimeMillis()).stream();
				}).collect(Collectors.toList());

		return collect;
	}



	/**
	 * 使用游标 分页查询key
	 * @param patternKey key 通配符
	 * @param pageNum 当前页
	 * @param pageSize 每页大小
	 * @return 符合的 value列表
	 * 使用scan进行遍历
	 *
	 */
	private List<String> findKeysForPage(String patternKey, long pageNum, long pageSize){
		ScanOptions options = ScanOptions.scanOptions().count(1000L).match(patternKey).build();
		RedisSerializer<String> redisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
		Cursor cursor = (Cursor) redisTemplate.executeWithStickyConnection(
				redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize));
		List<String> result = new ArrayList<>();
		int tmpIndex = 0;
		long startIndex = (pageNum - 1) * pageSize;
		long end = pageNum * pageSize;

		assert cursor != null;
		while (cursor.hasNext()){
			if (tmpIndex >= startIndex && tmpIndex < end){
				result.add(cursor.next().toString());
				tmpIndex++;
				continue;
			}
			if (tmpIndex >= end){
				break;
			}
			tmpIndex++;
			cursor.next();
		}

		try{
			cursor.close();
		}
		catch (Exception e){
			log.error("关闭cursor 失败");
		}
		return result;
	}

	/**
	 * 第三方授权退出
	 * @param body
	 * @return
	 * body 是 xml格式数据
	 *  example
	 *  <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
	 * ID="[RANDOM ID]" Version="2.0"
	 * IssueInstant="[CURRENT DATE/TIME]">
	 * <saml:NameID>@NOT_USED@</saml:NameID>
	 * <samlp:SessionIndex>[ST]</samlp:SessionIndex>
	 * </samlp:LogoutRequest>
	 */
	public String centerLogout(String body) {
		String username = null;
		log.info("centerLogout body:{}",body);
		if (StringUtil.isNotBlank(body)) {
			String urlDecodeRet = URLDecoder.decode(body, StandardCharsets.UTF_8);
			log.info("urlDecodeRet:{}",urlDecodeRet);
			JSONObject jsonObject = XML.toJSONObject(urlDecodeRet.replaceAll(": ",":"));
			String ticket = jsonObject.getJSONObject("samlp:LogoutRequest").getStr("samlp:SessionIndex");
			String key = String.format("casst:%s",ticket);
			Object idCardUUid = redisTemplate.opsForValue().get(key);
			username = idCardUUid == null ? null : idCardUUid.toString();
			log.info("diCardUUid:{}",idCardUUid);
			redisTemplate.delete(key);
		}
		return username;
	}

}
