package com.ruoyi.framework.web.service;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginSmsSendBody;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.enums.LoginPolicyType;
import com.ruoyi.common.enums.NetworkZone;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.user.SmsCodeException;
import com.ruoyi.common.exception.user.SmsCodeExpireException;
import com.ruoyi.common.exception.user.SmsCodeRequiredException;
import com.ruoyi.common.exception.user.SmsSendIntervalException;
import com.ruoyi.common.exception.user.UserMobileNotBoundException;
import com.ruoyi.common.exception.user.UserNotExistsException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.config.properties.LoginProperties;
import com.ruoyi.framework.web.service.sms.SmsGatewayClient;
import com.ruoyi.system.service.ISysLoginPolicyService;
import com.ruoyi.system.service.ISysUserService;

/**
 * 互联网管理端短信双因子（2FA）登录服务。
 * <p>
 * 等保身份鉴别要求：互联网环境管理员须「账号口令 + 短信验证码」方可登录，缺一不可。
 * 本服务负责验证码的发送、缓存、校验，以及结合 {@link ISysLoginPolicyService} 判断当前用户是否走双因子流程。
 * </p>
 * <p>
 * <b>典型调用链</b>
 * <ol>
 *   <li>前端 {@code POST /login/sms/send} → {@link #sendLoginSms(LoginSmsSendBody)}</li>
 *   <li>前端 {@code POST /login}（携带 smsCode）→ {@link SysLoginService#login} → {@link #validateLoginSmsCode}</li>
 * </ol>
 * </p>
 * <p>
 * <b>Redis 约定</b>（见 {@link CacheConstants}）：
 * <ul>
 *   <li>{@code login:sms:{mobile}} — OTP 明文，TTL 默认 5 分钟，校验成功后立即删除（一次性）</li>
 *   <li>{@code login:sms:limit:{ip}} — 同 IP 发送间隔限制，默认 60 秒</li>
 * </ul>
 * </p>
 *
 * @see com.ruoyi.framework.config.properties.LoginProperties
 * @see doc/密码改造/配置与接口说明.md
 */
@Service
public class LoginSmsService
{
    @Autowired
    private RedisCache redisCache;

    @Autowired
    private ISysUserService userService;

    @Autowired
    private ISysLoginPolicyService loginPolicyService;

    @Autowired
    private LoginProperties loginProperties;

    /** 生产环境对接阿里云 dysmsapi；开发态 dry-run 时仅写日志 */
    @Autowired
    private SmsGatewayClient smsGatewayClient;

    /** 异步写 sys_logininfor，与 Spring 容器解耦便于单测 */
    @Autowired
    private LoginInforRecorder loginInforRecorder;

    /**
     * 发送登录短信验证码。
     * <p>
     * 流程：开关/策略校验 → 用户与手机号校验 → IP 频控 → 生成 OTP 写 Redis → 调短信网关。
     * sms-2fa 模式下不校验图形验证码，依赖 IP 发送间隔防刷。
     * 安全注意：用户不存在时仍返回统一错误文案（{@link UserNotExistsException}），避免枚举用户名。
     * </p>
     *
     * @param body 含 username（code/uuid 可省略）
     */
    public void sendLoginSms(LoginSmsSendBody body)
    {
        String username = StringUtils.trim(body.getUsername());
        assertInternetSmsEnabled();

        // 1. 用户必须存在且当前策略要求短信双因子
        SysUser user = userService.selectUserByUserName(username);
        if (user == null)
        {
            loginInforRecorder.record(username, Constants.LOGIN_FAIL, "用户不存在/密码错误");
            throw new UserNotExistsException();
        }
        if (!requiresSmsTwoFactor(user))
        {
            throw new ServiceException("当前账号无需短信验证码登录");
        }

        // 2. 手机号来自 sys_user.phonenumber（二期可改为 SM4 解密后比对）
        String mobile = resolveMobile(user);
        if (StringUtils.isEmpty(mobile))
        {
            loginInforRecorder.record(username, Constants.LOGIN_FAIL, "账号未绑定手机号，无法登录");
            throw new UserMobileNotBoundException();
        }

        // 3. 同 IP 发送间隔限制（默认 60s，配置 login.internet-sms.send-interval-seconds）
        String clientIp = IpUtils.getIpAddr();
        String limitKey = CacheConstants.LOGIN_SMS_LIMIT_KEY + clientIp;
        if (redisCache.hasKey(limitKey))
        {
            loginInforRecorder.record(username, Constants.LOGIN_FAIL, "短信发送过于频繁，请稍后再试");
            throw new SmsSendIntervalException();
        }

        // 4. 生成数字 OTP 并写入 Redis（按手机号维度，非用户名，避免换账号刷同一手机）
        String code = generateCode(loginProperties.getInternetSms().getCodeLength());
        String smsKey = CacheConstants.LOGIN_SMS_CODE_KEY + mobile;
        int ttl = loginProperties.getInternetSms().getCodeTtlSeconds();
        redisCache.setCacheObject(smsKey, code, ttl, TimeUnit.SECONDS);
        redisCache.setCacheObject(limitKey, "1", loginProperties.getInternetSms().getSendIntervalSeconds(), TimeUnit.SECONDS);

        // 5. 调用短信网关；失败则回滚 Redis 中的 OTP
        boolean sent = smsGatewayClient.sendLoginCode(mobile, code);
        if (!sent)
        {
            redisCache.deleteObject(smsKey);
            loginInforRecorder.record(username, Constants.LOGIN_FAIL, "短信发送失败");
            throw new ServiceException("短信发送失败，请稍后重试");
        }
        loginInforRecorder.record(username, Constants.LOGIN_SUCCESS, "验证码已发送");
    }

    /**
     * 校验登录短信验证码（一次性消费）。
     * <p>
     * 由 {@link SysLoginService#login} 在口令认证之前调用；若当前用户策略非 SMS_2FA 则直接跳过。
     * 校验通过后删除 Redis 中的 OTP，防止重放。
     * </p>
     *
     * @param user    已查库的用户（含角色，用于策略判断）
     * @param smsCode 前端提交的 6 位验证码，可为 null（将抛 {@link SmsCodeRequiredException}）
     */
    public void validateLoginSmsCode(SysUser user, String smsCode)
    {
        if (!requiresSmsTwoFactor(user))
        {
            return;
        }
        if (StringUtils.isEmpty(smsCode))
        {
            loginInforRecorder.record(user.getUserName(), Constants.LOGIN_FAIL, "请输入短信验证码");
            throw new SmsCodeRequiredException();
        }
        String mobile = resolveMobile(user);
        if (StringUtils.isEmpty(mobile))
        {
            throw new UserMobileNotBoundException();
        }
        String smsKey = CacheConstants.LOGIN_SMS_CODE_KEY + mobile;
        String cached = redisCache.getCacheObject(smsKey);
        if (cached == null)
        {
            loginInforRecorder.record(user.getUserName(), Constants.LOGIN_FAIL, "短信验证码已失效");
            throw new SmsCodeExpireException();
        }
        if (!cached.equals(smsCode.trim()))
        {
            // 错误码不删除 OTP，允许在有效期内重试（等保测评常见口径）
            loginInforRecorder.record(user.getUserName(), Constants.LOGIN_FAIL, "短信验证码错误");
            throw new SmsCodeException();
        }
        redisCache.deleteObject(smsKey);
    }

    /**
     * 判断当前部署环境下，该用户是否必须短信双因子登录。
     * <p>
     * 条件（同时满足才进入双因子）：
     * <ol>
     *   <li>{@code login.internet-sms.enabled=true}</li>
     *   <li>{@link ISysLoginPolicyService#resolvePolicyType} 解析结果为 {@link LoginPolicyType#SMS_2FA}</li>
     * </ol>
     * 策略优先级：用户级 {@code sys_user.login_policy} &gt; 角色级 {@code sys_login_policy} &gt; 网络区域默认策略。
     * </p>
     */
    public boolean requiresSmsTwoFactor(SysUser user)
    {
        if (!loginProperties.isSmsTwoFactorActive())
        {
            return false;
        }
        String zone = StringUtils.nvl(loginProperties.getNetworkZone(), NetworkZone.INTERNAL.name());
        LoginPolicyType policyType = loginPolicyService.resolvePolicyType(zone, extractRoleKeys(user), user.getLoginPolicy());
        return LoginPolicyType.SMS_2FA == policyType;
    }

    /** 断言互联网短信双因子总开关已开启 */
    private void assertInternetSmsEnabled()
    {
        if (!loginProperties.isSmsTwoFactorActive())
        {
            throw new ServiceException("短信双因子登录未启用");
        }
    }

    /** 从用户关联角色提取 role_key，供策略表匹配 */
    private List<String> extractRoleKeys(SysUser user)
    {
        if (user.getRoles() == null || user.getRoles().isEmpty())
        {
            return new ArrayList<>();
        }
        return user.getRoles().stream()
                .map(SysRole::getRoleKey)
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toList());
    }

    /** 解析登录用手机号；一期读明文列，二期可走 CryptoService 解密 phonenumber_cipher */
    private String resolveMobile(SysUser user)
    {
        return StringUtils.trim(user.getPhonenumber());
    }

    /** 使用 SecureRandom 生成指定位数的数字验证码 */
    private String generateCode(int length)
    {
        SecureRandom random = new SecureRandom();
        StringBuilder builder = new StringBuilder(length);
        for (int i = 0; i < length; i++)
        {
            builder.append(random.nextInt(10));
        }
        return builder.toString();
    }
}
