package org.lingniu.sdk.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.lingniu.sdk.model.token.AccessTokenInfo;
import org.lingniu.sdk.model.token.RefreshTokenInfo;
import org.lingniu.sdk.model.token.TokenInfo;
import org.lingniu.sdk.model.user.UserInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;

@Component
@Slf4j
public class TokenService {
    private final ObjectMapper objectMapper;
    // 令牌有效期（默认30分钟）单位分钟
    @Value("${token.accessToken.expireTime:30}")
    private int accessTokenExpireTime;
    // 刷新令牌有效期(默认24小时) 单位小时
    @Value("${token.refreshToken.expireTime:168}")
    private int refreshTokenExpireTime;
    @Value("${token.header:Authorization}")
    private String tokenHeader;
    private final RedisRefreshTokenService refreshTokenService;

    private final RedisAccessTokenService accessTokenService;


    private final RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService;
    public TokenService(RedisRefreshTokenService refreshTokenService, RedisAccessTokenService accessTokenService, RedisOAuth2AuthorizedClientService redisOAuth2AuthorizedClientService, ObjectMapper objectMapper) {
        this.refreshTokenService = refreshTokenService;
        this.accessTokenService = accessTokenService;
        this.redisOAuth2AuthorizedClientService = redisOAuth2AuthorizedClientService;
        this.objectMapper = objectMapper;
    }

    public TokenInfo createToken(String username) throws JsonProcessingException {
        String accessToken = UUID.randomUUID().toString().replace("-", "");
        String refreshToken = UUID.randomUUID().toString().replace("-", "");
        Instant issuedAt = Instant.now();
        Instant accessExpiresAt = issuedAt.plusSeconds(accessTokenExpireTime * 60L);
        Instant refreshExpiresAt = issuedAt.plus(refreshTokenExpireTime, ChronoUnit.HOURS);
        AccessTokenInfo accessTokenInfo = AccessTokenInfo.builder()
                .tokenValue(accessToken)
                .username(username)
                .issuedAt(issuedAt)
                .expiresAt(accessExpiresAt)
                .refreshTokenId(refreshToken)
                .build();
        RefreshTokenInfo refreshTokenInfo = RefreshTokenInfo.builder()
                .tokenValue(refreshToken)
                .username(username)
                .accessToken(accessToken)
                .createdAt(issuedAt)
                .lastUsedAt(refreshExpiresAt)
                .accessTokenCount(1)
                .usageCount(0)
                .build();
        return new TokenInfo(accessTokenInfo,refreshTokenInfo);
    }
    public void storeTokenInfo(TokenInfo tokenInfo){
        accessTokenService.storeAccessToken(tokenInfo.getAccessTokenInfo());

        refreshTokenService.storeRefreshToken(tokenInfo.getRefreshTokenInfo());
    }
    public void setAccessTokenHeader(HttpServletResponse response,String accessToken){
        response.addHeader(tokenHeader,accessToken);

    }

    public void setRefreshTokenCookie(HttpServletResponse response, TokenInfo tokenInfo) {
        String refreshToken = tokenInfo.getRefreshTokenInfo().getTokenValue();
//        Cookie cookie = new Cookie("refresh_token", refreshToken);
//        cookie.setHttpOnly(true);
//        cookie.setSecure(true); // 生产环境设为true
//        cookie.setPath("/");
//        cookie.setMaxAge(refreshTokenExpireTime * 60 * 60);
//        cookie.setDomain(".lingniu.com"); // 设置域名

        // 添加SameSite属性
        response.addHeader("Set-Cookie",
                String.format("app_refresh_token=%s; HttpOnly; Secure; Path=/; Max-Age=%d; SameSite=Strict",
                        refreshToken, refreshTokenExpireTime * 60 * 60));
    }
    public String getCookieRefreshToken(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        return Arrays.stream(cookies)
                .filter(cookie -> "app_refresh_token".equals(cookie.getName()))
                .map(Cookie::getValue)
                .findFirst()
                .orElse(null);
    }
    public boolean validateAccessToken(HttpServletRequest request){
        String accessToken = request.getHeader(tokenHeader);
        return accessTokenService.validateAccessToken(accessToken);
    }

    public AccessTokenInfo getAccessTokenInfo(HttpServletRequest request){
        String accessToken = request.getHeader(tokenHeader);
        return accessTokenService.getAccessTokenInfo(accessToken);
    }
    public RefreshTokenInfo getRefreshTokenInfo(HttpServletRequest request){
        String accessToken = getCookieRefreshToken(request);
        return refreshTokenService.getRefreshTokenInfo(accessToken);
    }

    public AccessTokenInfo refreshToken(HttpServletRequest request,HttpServletResponse response) throws IOException {
        String accessToken = request.getHeader(tokenHeader);
        String cookieRefreshToken = getCookieRefreshToken(request);
        RefreshTokenInfo refreshTokenInfo = refreshTokenService.getRefreshTokenInfo(cookieRefreshToken);
        if(refreshTokenInfo == null || !refreshTokenInfo.isValid()){
            log.error("token 已刷新");
            return null;
        }
        if(refreshTokenInfo.getAccessToken()!=null && !refreshTokenInfo.getAccessToken().equals(accessToken)){
            log.error("token 已刷新");
        }
        String clientRegistrationId = refreshTokenInfo.getClientRegistrationId();
        String username = refreshTokenInfo.getUsername();
        OAuth2AuthorizedClient oAuth2AuthorizedClient = redisOAuth2AuthorizedClientService.loadAuthorizedClient(clientRegistrationId, username);
        if(oAuth2AuthorizedClient==null){
            log.error("idp client is expire");
            return null;
        }
        if(hasTokenExpired(oAuth2AuthorizedClient.getAccessToken())){
            RefreshTokenOAuth2AuthorizedClientProvider refreshTokenOAuth2AuthorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
            OAuth2AuthorizationContext oAuth2AuthorizationContext = OAuth2AuthorizationContext.withAuthorizedClient(oAuth2AuthorizedClient).principal(createAuthentication(username)).build();
            oAuth2AuthorizedClient = refreshTokenOAuth2AuthorizedClientProvider.authorize(oAuth2AuthorizationContext);
            redisOAuth2AuthorizedClientService.saveAuthorizedClient(oAuth2AuthorizedClient,createAuthentication(username));
        }
        if(oAuth2AuthorizedClient==null){
            log.error("idp client is expire");
            return null;
        }
        DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();

        OAuth2UserRequest oAuth2UserRequest = new OAuth2UserRequest(oAuth2AuthorizedClient.getClientRegistration(),oAuth2AuthorizedClient.getAccessToken());

        OAuth2User oAuth2User = defaultOAuth2UserService.loadUser(oAuth2UserRequest);
        String s = objectMapper.writeValueAsString(oAuth2User.getAttributes());

        String accessTokenNew = UUID.randomUUID().toString().replace("-", "");
        Instant issuedAt = Instant.now();
        Instant accessExpiresAt = issuedAt.plusSeconds(accessTokenExpireTime * 60L);
        AccessTokenInfo accessTokenInfo = AccessTokenInfo.builder()
                .tokenValue(accessTokenNew)
                .username(refreshTokenInfo.getUsername())
                .username(refreshTokenInfo.getUsername())
                .issuedAt(issuedAt)
                .expiresAt(accessExpiresAt)
                .additionalInfo(s)
                .clientRegistrationId(refreshTokenInfo.getClientRegistrationId())
                .refreshTokenId(refreshTokenInfo.getTokenValue())
                .build();
        accessTokenService.storeAccessToken(accessTokenInfo);

        refreshTokenInfo.incrementUsage();
        refreshTokenInfo.incrementAccessTokenUsage(accessTokenNew);
        refreshTokenService.updateRefreshToken(refreshTokenInfo);
        setAccessTokenHeader(response,accessTokenNew);
        return accessTokenInfo;
    }

    public UserInfo convertPrincipal(AccessTokenInfo accessTokenInfo) throws JsonProcessingException {
        return objectMapper.convertValue(objectMapper.readValue(accessTokenInfo.getAdditionalInfo(), Map.class), UserInfo.class);
    }
    public void revokeToken(HttpServletRequest request,HttpServletResponse response){
        String accessToken = request.getHeader(tokenHeader);
        String cookieRefreshToken = getCookieRefreshToken(request);
        AccessTokenInfo accessTokenInfo = accessTokenService.getAccessTokenInfo(accessToken);
        RefreshTokenInfo refreshTokenInfo = refreshTokenService.getRefreshTokenInfo(cookieRefreshToken);
        Instant now = Instant.now();
        if(accessTokenInfo!=null){
            accessTokenService.revokeAccessToken(accessTokenInfo);
        }
        if(refreshTokenInfo!=null){
            refreshTokenService.revokeRefreshToken(refreshTokenInfo);
        }
        clearRefreshTokenCookie(response);
    }
    public void clearToken(HttpServletRequest request){
        RefreshTokenInfo refreshTokenInfo = getRefreshTokenInfo(request);
        AccessTokenInfo accessTokenInfo = getAccessTokenInfo(request);
        accessTokenService.revokeAccessToken(accessTokenInfo);
        refreshTokenService.revokeRefreshToken(refreshTokenInfo);
        if(refreshTokenInfo!=null){
            AccessTokenInfo accessTokenInfo1 = accessTokenService.getAccessTokenInfo(refreshTokenInfo.getAccessToken());
            accessTokenService.revokeAccessToken(accessTokenInfo1);
        }
//        redisOAuth2AuthorizedClientService.removeAuthorizedClient(refreshTokenInfo.getClientRegistrationId(),refreshTokenInfo.getUsername());
    }

    public void clearRefreshTokenCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("refresh_token", null);
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        cookie.setPath("/");
        cookie.setMaxAge(0);

        response.addCookie(cookie);
    }
    private final Duration clockSkew = Duration.ofSeconds(60);

    private final Clock clock = Clock.systemUTC();
    private boolean hasTokenExpired(OAuth2Token token) {
        return this.clock.instant().isAfter(Objects.requireNonNull(token.getExpiresAt()).minus(this.clockSkew));
    }
    private Authentication createAuthentication(String principalName) {
        return new UsernamePasswordAuthenticationToken(
                principalName,
                null,
                Collections.emptyList()
        );
    }
}
