package com.ruoyi.app.user;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.app.utils.oauth.OAuthVerifyService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.model.LoginUserDto;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.ip.AddressUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.system.domain.InfoUser;
import com.ruoyi.system.domain.InfoUserOauth;
import com.ruoyi.system.mapper.InfoUserOauthMapper;
import com.ruoyi.system.service.IInfoUserService;
import com.ruoyi.system.utils.JwtUtil;
import eu.bitwalker.useragentutils.UserAgent;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * LINE 登录服务端回调（017-oauth-login 增量，2026-08-05）。
 *
 * <p><b>为什么需要它</b>：当用户走「唤起 LINE App / 系统浏览器」授权时，LINE 把重定向发给
 * 注册的 redirect_uri（后端 https URL），uniapp（另一个 App）无法在中间截获 code。这时必须由
 * 后端接住回调、换 token、完成登录，再把结果 302 跳回 App（自定义 scheme）。原 {@code /oauthLogin}
 * 保留不动，覆盖「前端自己拿到 code 再 POST 给后端」的另一种场景（H5 / webview / SDK）。
 *
 * <p><b>流程</b>：LINE → {@code GET /auth/line/callback?code=...&state=...}
 * → {@link OAuthVerifyService#verify verify("line", code)}（code 换 token 取 userId）
 * → 查 info_user_oauth：已绑定→签 JWT token；未绑定→生成 tempKey（needPhone）
 * → 302 跳到 {@code oauth.line.app-redirect}（如 com.twanmsdyh.app://oauthLogin）带结果：
 * <ul>
 *   <li>已绑定：{@code <app-redirect>?token=xxx}</li>
 *   <li>未绑定：{@code <app-redirect>?needPhone=1&tempKey=xxx}（App 弹手机号+短信码 UI 后调 /infouser/user/oauthBindPhone）</li>
 *   <li>异常：{@code <app-redirect>?error=xxx}</li>
 * </ul>
 *
 * <p><b>redirect_uri 三方一致性</b>：LINE 要求「前端 authorize 的 redirect_uri」「后端换 token 的
 * redirect_uri」「LINE Console 回调白名单」三者完全一致，否则回 400 redirect_uri_mismatch。
 * 故 application.yml 的 {@code oauth.line.redirect-uri} 必须与 Console 一致（= 本端点地址）。
 *
 * @author foodie
 * @date 2026-08-05
 */
@RestController
@RequestMapping("/auth/line")
public class LineCallbackController extends BaseController {

    private static final Logger log = LoggerFactory.getLogger(LineCallbackController.class);

    /** 与 InfoUserController.OAUTH_TEMP_PREFIX 一致：未绑定临时凭证 Redis 前缀（oauthBindPhone 消费） */
    private static final String OAUTH_TEMP_PREFIX = "oauth:bind:";

    @Autowired
    private OAuthVerifyService oauthVerifyService;
    @Autowired
    private IInfoUserService infoUserService;
    @Autowired
    private InfoUserOauthMapper infoUserOauthMapper;
    @Autowired
    private RedisCache redisCache;

    /** 后端登录后 302 跳回 App 的 scheme（App 注册该 scheme 接收 token/tempKey/error） */
    @Value("${oauth.line.app-redirect}")
    private String appRedirect;

    /**
     * LINE 服务端回调：接 code → 换 token → 登录 → 302 回 App。
     * 用 @Anonymous 放行（LINE 以浏览器/App 身份回调，无 token）。
     */
    @Anonymous
    @GetMapping("/callback")
    public void callback(@RequestParam(value = "code", required = false) String code,
                         @RequestParam(value = "state", required = false) String state,
                         HttpServletResponse response) throws IOException {
        try {
            if (code == null || code.isEmpty()) {
                log.warn("[OAuth][LINE] callback 缺 code 参数, state={}", state);
                redirectToApp(response, "error", "no_code");
                return;
            }
            log.info("[OAuth][LINE] callback 收到 code(len={}), state={}", code.length(), state);

            // code → userId（OAuthVerifyService.verifyLine：code 换 access_token 再取 profile）
            String providerUid = oauthVerifyService.verify("line", code);

            InfoUserOauth bind = infoUserOauthMapper.selectOne(
                    new LambdaQueryWrapper<InfoUserOauth>()
                            .eq(InfoUserOauth::getProvider, "line")
                            .eq(InfoUserOauth::getProviderUid, providerUid));
            if (bind != null) {
                InfoUser u = infoUserService.getOne(new QueryWrapper<InfoUser>()
                        .eq("user_id", bind.getUserId()).eq("status", 0).eq("del_flag", "0"));
                if (u == null) {
                    log.warn("[OAuth][LINE] callback 已绑定但账号已停用 userId={}", bind.getUserId());
                    redirectToApp(response, "error", "user_stopped");
                    return;
                }
                String token = buildOauthToken(u, "line");
                log.info("[OAuth][LINE] callback 已绑定登录成功 userId={}", u.getUserId());
                response.sendRedirect(appRedirect + "?token=" + enc(token));
                return;
            }

            // 未绑定：缓存 {line, providerUid}，返回 needPhone + tempKey（与 oauthLogin 同款，oauthBindPhone 消费）
            String tempKey = UUID.randomUUID().toString().replace("-", "");
            redisCache.setCacheObject(OAUTH_TEMP_PREFIX + tempKey, "line@" + providerUid, 5, TimeUnit.MINUTES);
            log.info("[OAuth][LINE] callback 未绑定，返回 needPhone providerUid={}", providerUid);
            response.sendRedirect(appRedirect + "?needPhone=1&tempKey=" + enc(tempKey));
        } catch (Exception e) {
            log.error("[OAuth][LINE] callback 异常", e);
            redirectToApp(response, "error", e.getMessage() == null ? "fail" : e.getMessage());
        }
    }

    /**
     * 签发三方登录 token（与 InfoUserController.issueOauthToken 同款，仅返回 token 串不包 AjaxResult）。
     * 此处刻意不复用 InfoUserController 的私有方法，避免改动已上线代码；两处逻辑保持一致。
     */
    private String buildOauthToken(InfoUser user, String provider) {
        redisCache.deleteKeys(CacheConstants.USER_TOKEN_KEY + user.getUserId() + ":" + "*");
        LoginUserDto dto = new LoginUserDto();
        dto.setUserId(user.getUserId());
        dto.setUserName(user.getPhone());
        dto.setProvider(provider);
        fillLoginUserInfo(dto);
        return JwtUtil.setToken(CacheConstants.USER_TOKEN_KEY, dto);
    }

    /** 填充登录设备/网络信息（IP、地点、浏览器、OS、登录时间），与 InfoUserController.fillLoginUserInfo 一致。 */
    private void fillLoginUserInfo(LoginUserDto userDto) {
        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
        userDto.setIpaddr(ip);
        userDto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        userDto.setBrowser(userAgent.getBrowser().getName());
        userDto.setOs(userAgent.getOperatingSystem().getName());
        userDto.setLoginTime(System.currentTimeMillis());
    }

    /** 302 跳回 App：app-redirect + ?<key>=<value>（value 做 URL 编码）。 */
    private void redirectToApp(HttpServletResponse response, String key, String value) throws IOException {
        response.sendRedirect(appRedirect + "?" + key + "=" + enc(value));
    }

    private static String enc(String s) {
        return URLEncoder.encode(s == null ? "" : s, StandardCharsets.UTF_8);
    }
}
