
package com.nantian.sc.cloud.auth.endpoint;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nantian.sc.cloud.admin.api.dto.UserInfo;
import com.nantian.sc.cloud.admin.api.entity.SysLog;
import com.nantian.sc.cloud.admin.api.entity.SysUser;
import com.nantian.sc.cloud.admin.api.feign.RemoteLogService;
import com.nantian.sc.cloud.admin.api.feign.RemoteUserService;
import com.nantian.sc.cloud.common.core.constant.CacheConstants;
import com.nantian.sc.cloud.common.core.constant.CommonConstants;
import com.nantian.sc.cloud.common.core.constant.PaginationConstants;
import com.nantian.sc.cloud.common.core.constant.SecurityConstants;
import com.nantian.sc.cloud.common.core.util.R;
import com.nantian.sc.cloud.common.data.tenant.TenantContextHolder;
import com.nantian.sc.cloud.common.log.util.SysLogUtils;
import com.nantian.sc.cloud.common.security.annotation.Inner;
import com.nantian.sc.cloud.common.security.service.OAuthTokenUser;
import com.nantian.sc.cloud.common.security.service.ScUser;
import com.nantian.sc.cloud.common.security.util.AuthUtils;
import com.nantian.sc.cloud.common.security.util.SecurityUtils;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.apache.ibatis.annotations.MapKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.ConvertingCursor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.*;

import static org.springframework.web.bind.annotation.RequestMethod.*;
import static org.springframework.web.bind.annotation.RequestMethod.DELETE;

/**
 *
 * @date 2018/6/24
 * 删除token端点
 */
@CrossOrigin(origins="*", methods={GET, POST, PUT, DELETE, OPTIONS}, maxAge=60L)
@RestController
@AllArgsConstructor
@RequestMapping("/token")
public class ScTokenEndpoint {
	private static final String SC_OAUTH_ACCESS = SecurityConstants.SC_PREFIX + SecurityConstants.OAUTH_PREFIX + "auth_to_access:";
	private final ClientDetailsService clientDetailsService;
	private final RedisTemplate redisTemplate;
	private final TokenStore tokenStore;
	private final CacheManager cacheManager;
	private  final String BASIC_ = "Basic ";
	private static final String KEY_ALGORITHM = "AES";
	private static final String ENCODE_KEY = "nantiannantian12";
	private final RemoteUserService remoteUserService;
	private final PasswordEncoder passwordEncoder;
	private final AuthenticationManager authenticationManager;
	private final AuthorizationServerTokenServices defaultAuthorizationServerTokenServices;
	private final RemoteLogService remoteLogService;


	/**
	 * 认证页面
	 *
	 * @param modelAndView
	 * @param error        表单登录失败处理回调的错误信息
	 * @return ModelAndView
	 */
	@GetMapping("/login")
	public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
		modelAndView.setViewName("ftl/login");
		modelAndView.addObject("error", error);
		return modelAndView;
	}

	/**
	 * 确认授权页面
	 *
	 * @param request
	 * @param session
	 * @param modelAndView
	 * @return
	 */
	@GetMapping("/confirm_access")
	public ModelAndView confirm(HttpServletRequest request, HttpSession session, ModelAndView modelAndView) {
		Map<String, Object> scopeList = (Map<String, Object>) request.getAttribute("scopes");
		modelAndView.addObject("scopeList", scopeList.keySet());

		Object auth = session.getAttribute("authorizationRequest");
		if (auth != null) {
			AuthorizationRequest authorizationRequest = (AuthorizationRequest) auth;
			ClientDetails clientDetails = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());
			modelAndView.addObject("app", clientDetails.getAdditionalInformation());
			modelAndView.addObject("user", SecurityUtils.getUser());
		}

		modelAndView.setViewName("ftl/confirm");
		return modelAndView;
	}

	/**
	 * 退出token
	 *
	 * @param authHeader Authorization
	 */
	@DeleteMapping("/logout")
	public R logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
		if (StrUtil.isBlank(authHeader)) {
			return R.ok(Boolean.FALSE, "退出失败，token 为空");
		}

		String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, StrUtil.EMPTY).trim();
		OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
		if (accessToken == null || StrUtil.isBlank(accessToken.getValue())) {
			return R.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();
		tokenStore.removeRefreshToken(refreshToken);
		return R.ok(Boolean.TRUE);
	}

	/**
	 * 令牌管理调用
	 *
	 * @param token token
	 * @return
	 */
	@Inner
	@DeleteMapping("/{token}")
	public R<Boolean> delToken(@PathVariable("token") String token) {
		OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
		tokenStore.removeAccessToken(oAuth2AccessToken);
		return new R<>();
	}


	/**
	 * 查询token
	 *
	 * @param params 分页参数
	 * @return
	 */
	@Inner
	@PostMapping("/page")
	public R<Page> tokenList(@RequestBody Map<String, Object> params) {
		//根据分页参数获取对应数据
		String key = String.format("%s*:%s", SC_OAUTH_ACCESS, TenantContextHolder.getTenantId());
		List<String> pages = findKeysForPage(key, MapUtil.getInt(params, PaginationConstants.CURRENT)
				, MapUtil.getInt(params, PaginationConstants.SIZE));

		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
		Page result = new Page(MapUtil.getInt(params, PaginationConstants.CURRENT), MapUtil.getInt(params, PaginationConstants.SIZE));
		result.setRecords(redisTemplate.opsForValue().multiGet(pages));
		result.setTotal(Long.valueOf(redisTemplate.keys(key).size()));
		return R.ok(result);
	}


	private List<String> findKeysForPage(String patternKey, int pageNum, int pageSize) {
		ScanOptions options = ScanOptions.scanOptions().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;
		int startIndex = (pageNum - 1) * pageSize;
		int 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();
		}
		return result;
	}

	//@Inner(value = false)
	//@GetMapping("/bigScreen")
	public String bigScreen(@RequestParam("callback") String callback, HttpServletRequest request, HttpServletResponse response) {
		//校验验证码
		/*R checkCode = checkCode(request);
		if (checkCode.getCode() == 1) {
			jsonpResult = JSONObject.toJSONString(checkCode);
			break loop;
		}*/
		//校验用户名和密码是否为空
		R checkUserNameAndPassword = checkUserNameAndPassword(request);
		if (checkUserNameAndPassword.getCode() == 1) {
			return JSONObject.toJSONString(checkUserNameAndPassword);
		}
		//校验用户是否存在
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);
		if (result.getCode() == 1){
			return JSONObject.toJSONString(result);
		}
		//校验密码是否正确
		R checkUserInfo = checkUserPassword(result.getData(), password, "");
		if (checkUserInfo.getCode() == 1){
			return JSONObject.toJSONString(checkUserInfo);
		}

		Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
		UserDetails userDetails = getUserDetails(result);
		cache.put(username, userDetails);

		//String header = request.getHeader(HttpHeaders.AUTHORIZATION);
		//if (header == null || !header.startsWith(BASIC_)) {
		//	throw new CheckedException("请求头中client信息为空");
		//}
		String header = "Basic bnQ6bnQ=";

		String[] tokens = AuthUtils.extractAndDecodeHeader(header);
		assert tokens.length == 2;
		String clientId = tokens[0];
		ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

		//校验secret
		if (!passwordEncoder.matches(tokens[1], clientDetails.getClientSecret())) {
			throw new InvalidClientException("Given client ID does not match authenticated client");
		}

		UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
		Authentication authentication = authenticationManager.authenticate(authenticationToken);

		//校验scope
		TokenRequest tokenRequest = new TokenRequest(MapUtil.newHashMap(), clientId, clientDetails.getScope(), "password");
		new DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails);
		OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
		OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
		OAuth2AccessToken oAuth2AccessToken = defaultAuthorizationServerTokenServices.createAccessToken(oAuth2Authentication);
		return JSONObject.toJSONString(R.ok(oAuth2AccessToken));

		/*try {
			//response.getWriter().print(callback+"("+jsonpResult+")");
			response.setContentType("text/html;charset=utf-8");
			response.setHeader("Access-Control-Allow-Origin","*"); //允许所有域名访问
			response.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE");
			PrintWriter out = response.getWriter();
			//获取callback值与需要返回的数据拼接，一并返回给前端。注意需要返回的数据需要用小括号“()”包裹起来
			out.write(callback+"("+jsonpResult+")");
		} catch (IOException e) {
			e.printStackTrace();
		}*/
	}

	@Inner(value = false)
	@GetMapping("/system")
	public R system(HttpServletRequest request, HttpServletResponse response) {
		//String jsonpResult = "";
		String loginType = request.getParameter("loginType");
		String type = "";
		if(!"bigScreen".equals(loginType)){ //不是大屏登录 需校验验证 密码需要解密
			//校验验证码
			R checkCode = checkCode(request);
			if (checkCode.getCode() == 1) {
				return checkCode;
			}
			type = "desc";
		}

		//校验用户名和密码是否为空
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
			return R.failed("用户名或密码不能为空");
		}
		//校验用户是否存在
		R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);
		if (result.getCode() == 1){
			return result;
		}
		//校验密码是否正确
		R checkUserInfo = checkUserPassword(result.getData(), password, type);
		if (checkUserInfo.getCode() == 1){
			return checkUserInfo;
		}
		Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
		UserDetails userDetails = getUserDetails(result);
		cache.put(username, userDetails);
		String header = "Basic bnQ6bnQ=";
		String[] tokens = AuthUtils.extractAndDecodeHeader(header);
		assert tokens.length == 2;
		String clientId = tokens[0];
		ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
		//校验secret
		if (!passwordEncoder.matches(tokens[1], clientDetails.getClientSecret())) {
			throw new InvalidClientException("Given client ID does not match authenticated client");
		}
		//UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
		UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, checkUserInfo.getData()); //要用明文的密码
		Authentication authentication = authenticationManager.authenticate(authenticationToken);
		//校验scope
		TokenRequest tokenRequest = new TokenRequest(MapUtil.newHashMap(), clientId, clientDetails.getScope(), "password");
		new DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails);
		OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
		OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
		OAuth2AccessToken oAuth2AccessToken = defaultAuthorizationServerTokenServices.createAccessToken(oAuth2Authentication);
		//记录登录成功信息
		/*ScUser scUser = (ScUser)authentication.getPrincipal();
		SysLog logVo = SysLogUtils.getSysLog();
		logVo.setTitle("用户登录成功");
		logVo.setCreateBy(scUser.getUsername());
		remoteLogService.saveLog(logVo, SecurityConstants.FROM_IN);*/

		//response.setHeader("Access-Control-Allow-Origin","*"); //允许所有域名访问
		//response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
		return R.ok(oAuth2AccessToken);
	}

	private R checkCode(HttpServletRequest request) {
		String code = request.getParameter("code");
		if("xn2n".equals(code)){ //手机公众号登录 跳过验证码校验
			return R.ok();
		}

		if (StrUtil.isBlank(code)) {
			return R.failed("验证码不能为空");
		}

		String randomStr = request.getParameter("randomStr");
		if (StrUtil.isBlank(randomStr)) {
			return R.failed("randomStrs不能为空");
		}

		String key = CommonConstants.DEFAULT_CODE_KEY + randomStr;
		redisTemplate.setKeySerializer(new StringRedisSerializer());

		if (!redisTemplate.hasKey(key)) {
			return R.failed("验证码不合法");
		}

		Object codeObj = redisTemplate.opsForValue().get(key);

		if (codeObj == null) {
			return R.failed("验证码不合法");
		}

		String saveCode = codeObj.toString();
		if (StrUtil.isBlank(saveCode)) {
			redisTemplate.delete(key);
			return R.failed("验证码不合法");
		}

		if (!StrUtil.equals(saveCode, code)) {
			redisTemplate.delete(key);
			return R.failed("验证码不合法");
		}

		redisTemplate.delete(key);
		return R.ok();
	}

	@SneakyThrows
	private static String decryptAES(String data, String pass) {
		AES aes = new AES(Mode.CBC, Padding.NoPadding,
				new SecretKeySpec(pass.getBytes(), KEY_ALGORITHM),
				new IvParameterSpec(pass.getBytes()));
		byte[] result = aes.decrypt(Base64.decode(data.getBytes(StandardCharsets.UTF_8)));
		return new String(result, StandardCharsets.UTF_8);
	}

	private R checkUserNameAndPassword(HttpServletRequest request) {

		String username = request.getParameter("username");
		if (StrUtil.isBlank(username)) {
			return R.failed("用户名不能为空");
		}

		String password = request.getParameter("password");
		if (StrUtil.isBlank(password)) {
			return R.failed("密码不能为空");
		}
		return R.ok();
	}

	private R checkUserPassword(UserInfo data, String password, String type) {
		if("desc".equals(type)){
			password = decryptAES(password, ENCODE_KEY); //密码需要解密
		}
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		boolean flag = passwordEncoder.matches(password.trim(), data.getSysUser().getPassword());
		if(!flag){
			return R.failed("密码错误");
		}
		return R.ok(password.trim());
	}

	private UserDetails getUserDetails(R<UserInfo> result) {
		if (result == null || result.getData() == null) {
			throw new UsernameNotFoundException("用户不存在");
		}

		UserInfo info = result.getData();
		Set<String> dbAuthsSet = new HashSet<>();
		if (ArrayUtil.isNotEmpty(info.getRoles())) {
			// 获取角色
			Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
			// 获取资源
			dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
		}
		Collection<? extends GrantedAuthority> authorities
				= AuthorityUtils.createAuthorityList(dbAuthsSet.toArray(new String[0]));
		SysUser user = info.getSysUser();
		boolean enabled = StrUtil.equals(user.getLockFlag(), CommonConstants.STATUS_NORMAL);
		// 构造security用户
		return new ScUser( user.getUserRealName(), user.getUserDept(), user.getUserId(), user.getEnterpriseCode(),user.getDeptId(), user.getTenantId(), user.getUsername(), SecurityConstants.BCRYPT + user.getPassword(), enabled,
				true, true, !CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
	}

	/**
	 * @Description:  校验第三方接口是否携带令牌
	 * @Author: dengjian
	 * @DATE: 2024/11/22 16:22
	 * @return com.nantian.sc.cloud.common.core.util.R
	 *
	 */
	@GetMapping("/checkUserIsLogin")
	private OAuthTokenUser checkUserIsLogin(HttpServletRequest request) {
		//获取请求头
		String header = request.getHeader(HttpHeaders.AUTHORIZATION);
		if (StringUtils.isEmpty(header)){
			return null;
		}
		header = header.replace(OAuth2AccessToken.BEARER_TYPE, StrUtil.EMPTY).trim();
		OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(header);
		if (oAuth2AccessToken == null){
			return null;
		}
		OAuthTokenUser user = SecurityUtils.getUserByOAuth2AccessToken(oAuth2AccessToken);
		return user;
	}





}
