package cn.iocoder.yudao.module.report.framework.jmreport.core.service;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
import org.springframework.http.HttpHeaders;

import javax.servlet.http.HttpServletRequest;
import java.util.Objects;

/**
 * {@link JmReportTokenServiceI} 实现类，提供积木报表的 Token 校验、用户信息的查询等功能
 *
 * @author 随心
 */
@Slf4j
@RequiredArgsConstructor
public class JmReportTokenServiceImpl implements JmReportTokenServiceI {

    /**
     * 积木 token head 头
     */
    private static final String JM_TOKEN_HEADER = "X-Access-Token";
    /**
     * auth 相关格式
     */
    private static final String AUTHORIZATION_FORMAT = SecurityFrameworkUtils.AUTHORIZATION_BEARER + " %s";

    private final OAuth2TokenApi oauth2TokenApi;
    private final PermissionApi permissionApi;

    private final SecurityProperties securityProperties;

    /**
     * 自定义 API 数据集appian自定义 Header，解决 Token 传递。
     * 参考 <a href="http://report.jeecg.com/2222224">api数据集token机制详解</a> 文档
     *
     * @return 新 head
     */
    @Override
    public HttpHeaders customApiHeader() {
        // 读取积木标标系统的 token
        HttpServletRequest request = ServletUtils.getRequest();
        String token = request.getHeader(JM_TOKEN_HEADER);

        // 确保设置用户和租户上下文，用于后续的 API 调用
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null && StrUtil.isNotEmpty(token)) {
            loginUser = buildLoginUserByToken(token);
        }
        if (loginUser != null && loginUser.getTenantId() != null) {
            TenantContextHolder.setTenantId(loginUser.getTenantId());
        }

        // 设置到 yudao 系统的 token
        HttpHeaders headers = new HttpHeaders();
        headers.add(securityProperties.getTokenHeader(), String.format(AUTHORIZATION_FORMAT, token));
        // 同时传递租户ID，确保 API 调用时能获取到租户上下文
        if (loginUser != null && loginUser.getTenantId() != null) {
            headers.add(WebFrameworkUtils.HEADER_TENANT_ID, String.valueOf(loginUser.getTenantId()));
        }
        return headers;
    }

    /**
     * 校验 Token 是否有效，即验证通过
     *
     * @param token JmReport 前端传递的 token
     * @return 是否认证通过
     */
    @Override
    public Boolean verifyToken(String token) {
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            // 尝试通过 token 构建用户并设置到 SecurityContext
            loginUser = buildLoginUserByToken(token);
            if (loginUser == null) {
                return false;
            }
        }
        // 确保设置租户上下文，积木报表 Saas 模式需要
        if (loginUser.getTenantId() != null) {
            TenantContextHolder.setTenantId(loginUser.getTenantId());
        }
        return true;
    }

    /**
     * 获得用户编号
     * <p>
     * 虽然方法名获得的是 username，实际对应到项目中是用户编号
     *
     * @param token JmReport 前端传递的 token
     * @return 用户编号
     */
    @Override
    public String getUsername(String token) {
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            // 尝试通过 token 构建用户并设置到 SecurityContext
            loginUser = buildLoginUserByToken(token);
            if (loginUser == null) {
                return null;
            }
        }
        // 确保设置租户上下文，积木报表 Saas 模式需要
        if (loginUser.getTenantId() != null) {
            TenantContextHolder.setTenantId(loginUser.getTenantId());
        }
        return String.valueOf(loginUser.getId());
    }

    /**
     * 基于 token 构建登录用户
     *
     * @param token token
     * @return 返回 token 对应的用户信息
     */
    private LoginUser buildLoginUserByToken(String token) {
        if (StrUtil.isEmpty(token)) {
            return null;
        }
        // TODO 如下的实现不算特别优雅，主要咱是不想搞的太复杂，所以参考对应的 Filter 先实现了

        // ① 参考 TokenAuthenticationFilter 的认证逻辑（Security 的上下文清理，交给 Spring Security 完成）
        // 目的：实现基于 JmReport 前端传递的 token，实现认证
        TenantContextHolder.setIgnore(true); // 忽略租户，保证可查询到 token 信息
        LoginUser user = null;
        try {
            OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token);
            if (accessToken == null) {
                return null;
            }
            user = new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
                    .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
        } catch (ServiceException ignored) {
            // do nothing：如果报错，说明认证失败，则返回 false 即可
        }
        if (user == null) {
            return null;
        }
        SecurityFrameworkUtils.setLoginUser(user, WebFrameworkUtils.getRequest());

        // ② 参考 TenantContextWebFilter 实现（Tenant 的上下文清理，交给 TenantContextWebFilter 完成）
        // 目的：基于 LoginUser 获得到的租户编号，设置到 Tenant 上下文，避免查询数据库时的报错
        TenantContextHolder.setIgnore(false);
        TenantContextHolder.setTenantId(user.getTenantId());
        return user;
    }

    @Override
    public String[] getRoles(String token) {
        // 设置租户上下文。原因是：/jmreport/** 纯前端地址，不会走 buildLoginUserByToken 逻辑
        // 如果 SecurityContext 中没有用户信息，则通过 token 构建
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            // 尝试通过 token 构建用户并设置到 SecurityContext
            loginUser = buildLoginUserByToken(token);
            if (loginUser == null) {
                return null;
            }
        }
        TenantContextHolder.setTenantId(loginUser.getTenantId());

        // 参见文档 https://help.jeecg.com/jimureport/prodSafe.html 文档
        // 适配：如果是本系统的管理员，则转换成 jimu 报表的管理员
        Long userId = loginUser.getId();
//        return permissionApi.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode(),RoleCodeEnum.TENANT_ADMIN.getCode(),RoleCodeEnum.GROUP_ADMIN.getCode())
//                ? new String[]{"admin"} : null;
        // 因为菜单已经做了权限的区分，能进菜单的肯定都是有权限的，暂时没必要再校验是否有积木报表的权限，先直接给权限
        return new String[]{"admin"};
    }

    @Override
    public String getTenantId() {
        // 补充说明：不能直接通过 TenantContext 获取，因为 jimu 报表前端请求时，没有带上 tenant-id Header
        // 如果 SecurityContext 中没有用户信息，则尝试从请求头获取 token 并构建用户
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            // 尝试从请求头获取 token
            HttpServletRequest request = ServletUtils.getRequest();
            if (request != null) {
                String token = request.getHeader(JM_TOKEN_HEADER);
                if (StrUtil.isNotEmpty(token)) {
                    // 通过 token 构建用户并设置到 SecurityContext
                    loginUser = buildLoginUserByToken(token);
                }
            }
            if (loginUser == null) {
                // 如果无法获取用户，返回 null，积木报表会报错
                log.warn("[getTenantId] 无法获取登录用户信息，请确保已传递正确的 token");
                return null;
            }
        }
        
        // 确保设置租户上下文，积木报表 Saas 模式需要
        // 这是关键：积木报表在查询数据时会检查租户上下文，必须在这里设置
        Long tenantId = loginUser.getTenantId();
        if (tenantId == null) {
            log.warn("[getTenantId] 用户 {} 的租户ID为空", loginUser.getId());
            return null;
        }
        
        // 设置租户上下文，确保后续数据库查询能正确过滤租户数据
        TenantContextHolder.setTenantId(tenantId);
        TenantContextHolder.setIgnore(false); // 确保不忽略租户
        
        return String.valueOf(tenantId);
    }

}
