package org.dromara.member.controller;

import cn.dev33.satoken.annotation.SaIgnore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.member.utils.JwtUtil;
import org.dromara.member.domain.AppUser;
import org.dromara.member.mapper.AppUserMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

/**
 * JWT认证控制器
 *
 * @author pengles
 */
@SaIgnore
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/auth/jwt")
public class AppJwtController {

    private final AppUserMapper appUserMapper;

    @Value("${ruoyi.app.jwt.secret:ruoyi-secret}")
    private String jwtSecret;
    @Value("${ruoyi.app.jwt.access-ttl-seconds:7200}")
    private long accessTtlSeconds;
    @Value("${ruoyi.app.jwt.refresh-ttl-seconds:604800}")
    private long refreshTtlSeconds;
    private static final String CAPTCHA_PREFIX = "APP:JWT:CAPTCHA:";
    private static final String BLACKLIST_PREFIX = "APP:JWT:BLACKLIST:";
    private static final String TOKEN_JTI_PREFIX = "APP:JWT:JTI:";
    private static final String USER_TOKENS_PREFIX = "APP:JWT:USER_TOKENS:";
    private static final String LOGIN_FAIL_PREFIX = "APP:JWT:LOGIN_FAIL:";

    @Operation(summary = "生成图形验证码(4位字母数字)")
    @GetMapping("/captcha")
    public R<Map<String, String>> captcha() {
        String code = randomCode(4);
        String uuid = UUID.randomUUID().toString();
        RedisUtils.setCacheObject(CAPTCHA_PREFIX + uuid, code, Duration.ofMinutes(5));
        // 这里简化为直接返回文本验证码；实际可返回Base64图片
        return R.ok(Map.of("uuid", uuid, "code", code));
    }

    @Operation(summary = "注册")
    @PostMapping("/register")
    public R<Void> register(
            @Parameter(description = "手机号(+86或11位)") @RequestParam @Pattern(regexp = "^(\\+86\\d{11}|\\d{11})$") String phone,
            @Parameter(description = "密码") @RequestParam @Size(min = 6, max = 64) String password,
            @Parameter(description = "验证码uuid") @RequestParam @NotBlank String captchaUuid,
            @Parameter(description = "验证码") @RequestParam @NotBlank String captchaCode) {
        if (!verifyCaptcha(captchaUuid, captchaCode)) {
            return R.fail("验证码错误");
        }
        AppUser exist = findByPhone(phone);
        if (exist != null) {
            return R.fail("手机号已注册");
        }
        org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder enc = new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder(12);
        AppUser u = new AppUser();
        u.setPhone(phone);
        u.setPassword(enc.encode(password));
        boolean ok = appUserMapper.insert(u) > 0;
        return ok ? R.ok() : R.fail("注册失败");
    }

    @Operation(summary = "登录")
    @PostMapping("/login")
    public R<Map<String, Object>> login(
            @Parameter(description = "手机号(+86或11位)") @RequestParam @Pattern(regexp = "^(\\+86\\d{11}|\\d{11})$") String phone,
            @Parameter(description = "密码") @RequestParam @Size(min = 6, max = 64) String password,
            @Parameter(description = "验证码uuid") @RequestParam @NotBlank String captchaUuid,
            @Parameter(description = "验证码") @RequestParam @NotBlank String captchaCode) {
        if (!verifyCaptcha(captchaUuid, captchaCode)) {
            return R.fail("验证码错误");
        }
        String failKey = LOGIN_FAIL_PREFIX + phone;
        Integer cnt = RedisUtils.getCacheObject(failKey);
        if (cnt != null && cnt >= 5) {
            return R.fail("登录失败次数过多，请稍后再试");
        }
        AppUser user = findByPhone(phone);
        org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder enc = new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder(12);
        if (user == null || !enc.matches(password, user.getPassword())) {
            int n = (cnt == null ? 0 : cnt) + 1;
            RedisUtils.setCacheObject(failKey, n, Duration.ofHours(1));
            return R.fail("手机号或密码错误");
        }
        JwtUtil jwt = new JwtUtil(jwtSecret);
        String jti = UUID.randomUUID().toString();
        String access = jwt.generateToken(String.valueOf(user.getUserId()), accessTtlSeconds, jti);
        String refresh = jwt.generateToken("refresh:" + user.getUserId(), refreshTtlSeconds, UUID.randomUUID().toString());
        RedisUtils.setCacheObject(TOKEN_JTI_PREFIX + jti, "1", Duration.ofSeconds(accessTtlSeconds));
        limitUserDevices(user.getUserId(), jti);
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("accessToken", access);
        data.put("refreshToken", refresh);
        data.put("expiresIn", accessTtlSeconds);
        return R.ok(data);
    }

    @Operation(summary = "刷新Token")
    @PostMapping("/refresh")
    public R<Map<String, Object>> refresh(@Parameter(description = "刷新令牌") @RequestParam String refreshToken) {
        JwtUtil jwt = new JwtUtil(jwtSecret);
        if (!jwt.verify(refreshToken)) return R.fail("刷新令牌无效");
        Map<String, Object> payload = jwt.parsePayload(refreshToken);
        String sub = String.valueOf(payload.get("sub"));
        if (sub == null || !sub.startsWith("refresh:")) return R.fail("刷新令牌错误");
        String userId = sub.substring("refresh:".length());
        String jti = UUID.randomUUID().toString();
        String access = jwt.generateToken(userId, accessTtlSeconds, jti);
        RedisUtils.setCacheObject(TOKEN_JTI_PREFIX + jti, "1", Duration.ofSeconds(accessTtlSeconds));
        limitUserDevices(Long.parseLong(userId), jti);
        return R.ok(Map.of("accessToken", access, "expiresIn", accessTtlSeconds));
    }

    @Operation(summary = "注销")
    @PostMapping("/logout")
    public R<Void> logout(@Parameter(description = "访问令牌") @RequestParam String accessToken) {
        JwtUtil jwt = new JwtUtil(jwtSecret);
        if (!jwt.verify(accessToken)) return R.fail("令牌无效");
        Map<String, Object> payload = jwt.parsePayload(accessToken);
        String jti = String.valueOf(payload.get("jti"));
        Object expObj = payload.get("exp");
        long now = java.time.Instant.now().getEpochSecond();
        long exp = expObj instanceof Number ? ((Number) expObj).longValue() : now;
        long remain = Math.max(0, exp - now);
        RedisUtils.setCacheObject(BLACKLIST_PREFIX + jti, "1", Duration.ofSeconds(remain));
        RedisUtils.deleteObject(TOKEN_JTI_PREFIX + jti);
        return R.ok();
    }

    private boolean verifyCaptcha(String uuid, String code) {
        String real = RedisUtils.getCacheObject(CAPTCHA_PREFIX + uuid);
        return real != null && real.equalsIgnoreCase(code);
    }

    private String randomCode(int n) {
        String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
        StringBuilder sb = new StringBuilder();
        java.util.Random r = new java.util.Random();
        for (int i = 0; i < n; i++) sb.append(chars.charAt(r.nextInt(chars.length())));
        return sb.toString();
    }

    private void limitUserDevices(Long userId, String jti) {
        String listKey = USER_TOKENS_PREFIX + userId;
        java.util.List<String> list = RedisUtils.getCacheObject(listKey);
        if (list == null) list = new java.util.ArrayList<>();
        list.add(0, jti);
        while (list.size() > 3) {
            String old = list.remove(list.size() - 1);
            RedisUtils.setCacheObject(BLACKLIST_PREFIX + old, "1", Duration.ofHours(2));
            RedisUtils.deleteObject(TOKEN_JTI_PREFIX + old);
        }
        RedisUtils.setCacheObject(listKey, list, Duration.ofDays(8));
    }

    private AppUser findByPhone(String phone) {
        com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AppUser> lqw = com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery();
        lqw.eq(AppUser::getPhone, phone);
        return appUserMapper.selectOne(lqw);
    }
}
