package org.dromara.web.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.date.DateUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.sse.dto.SseMessageDto;
import org.dromara.common.sse.utils.SseMessageUtils;
import org.dromara.web.domain.DriverDeviceTokenBind;
import org.dromara.web.mapper.DriverDeviceTokenBindMapper;
import org.dromara.web.service.DriverMobileSessionService;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Locale;

@Slf4j
@Service
@RequiredArgsConstructor
/**
 * 驾驶员移动端会话冲突控制：
 * 1）同一驾驶员仅允许一个有效移动端会话；
 * 2）同一设备仅允许一个驾驶员会话；
 * 3）发现冲突时踢出旧会话并发送SSE强制下线消息。
 */
public class DriverMobileSessionServiceImpl implements DriverMobileSessionService {

    private static final String DRIVER_TOKEN_KEY = "auth:driver:session:driver:";
    private static final String DEVICE_TOKEN_KEY = "auth:driver:session:device:";
    private static final String TOKEN_CTX_KEY = "auth:driver:session:ctx:";
    private static final String LOCK_KEY = "auth:driver:session:lock:";
    private static final String FORCE_LOGOUT_MSG = "您的账号已在其他设备登录，当前设备已自动签退。";

    private final DriverDeviceTokenBindMapper bindMapper;

    @Override
    /**
     * 登录成功回调：执行冲突检测并更新三元组映射（driver/device/token）。
     */
    public void onLoginSuccess(String tokenValue, SaLoginParameter loginParameter) {
        SessionPrincipal principal = resolvePrincipal(loginParameter);
        // 不是驾驶员移动端则直接放行
        if (!needControl(principal.getLoginPort(), principal.getClientKey(), principal.getDeviceType())) {
            return;
        }
        // businessUserId 在驾驶员端即 driverId
        String driverId = StringUtils.defaultString(principal.getDriverId());
        if (StringUtils.isBlank(driverId)) {
            return;
        }
        // 设备ID优先取前端上送，没有则服务端降级生成
        String deviceId = resolveDeviceId(principal.getClientKey(), principal.getDeviceType(), principal.getUserId());
        // driverId + deviceId 级别加锁，确保冲突检测与写入映射具备原子性
        String lockKey = LOCK_KEY + driverId + ":" + deviceId;
        var lock = RedisUtils.getClient().getLock(lockKey);
        boolean locked = false;
        try {
            locked = lock.tryLock(5, TimeUnit.SECONDS);
            if (!locked) {
                log.warn("驾驶员登录锁获取失败, driverId={}, deviceId={}", driverId, deviceId);
                return;
            }
            // 同一驾驶员在其他设备已登录：踢掉旧会话
            String oldDriverToken = RedisUtils.getCacheObject(DRIVER_TOKEN_KEY + driverId);
            if (StringUtils.isNotBlank(oldDriverToken) && !StringUtils.equals(oldDriverToken, tokenValue)) {
                forceKick(oldDriverToken, "OTHER_DEVICE_LOGIN");
            }
            // 同一设备上已有其他驾驶员会话：踢掉旧会话
            String oldDeviceToken = RedisUtils.getCacheObject(DEVICE_TOKEN_KEY + deviceId);
            if (StringUtils.isNotBlank(oldDeviceToken) && !StringUtils.equals(oldDeviceToken, tokenValue)) {
                forceKick(oldDeviceToken, "DEVICE_SWITCH_ACCOUNT");
            }
            // 建立 token 上下文，便于后续下线事件快速反查并清理映射
            TokenCtx ctx = new TokenCtx();
            ctx.setTenantId(principal.getTenantId());
            ctx.setUserId(principal.getUserId());
            ctx.setDriverId(driverId);
            ctx.setDeviceId(deviceId);
            // 刷新三元组映射
            cache(DRIVER_TOKEN_KEY + driverId, tokenValue, loginParameter.getTimeout());
            cache(DEVICE_TOKEN_KEY + deviceId, tokenValue, loginParameter.getTimeout());
            cache(TOKEN_CTX_KEY + tokenValue, ctx, loginParameter.getTimeout());
            persistLoginBind(principal, tokenValue, deviceId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    @Override
    /**
     * token下线回调：回收映射并更新绑定记录状态。
     */
    public void onTokenOffline(String tokenValue, String reason) {
        if (StringUtils.isBlank(tokenValue)) {
            return;
        }
        // 根据 token 反查上下文，删除仍指向该 token 的 driver/device 映射
        TokenCtx ctx = RedisUtils.getCacheObject(TOKEN_CTX_KEY + tokenValue);
        if (ctx != null) {
            String driverToken = RedisUtils.getCacheObject(DRIVER_TOKEN_KEY + ctx.getDriverId());
            if (StringUtils.equals(driverToken, tokenValue)) {
                RedisUtils.deleteObject(DRIVER_TOKEN_KEY + ctx.getDriverId());
            }
            String deviceToken = RedisUtils.getCacheObject(DEVICE_TOKEN_KEY + ctx.getDeviceId());
            if (StringUtils.equals(deviceToken, tokenValue)) {
                RedisUtils.deleteObject(DEVICE_TOKEN_KEY + ctx.getDeviceId());
            }
        }
        // token -> ctx 映射无条件回收
        RedisUtils.deleteObject(TOKEN_CTX_KEY + tokenValue);
        updateTokenStatus(tokenValue, reason);
    }

    /**
     * 仅对驾驶员移动端生效，其他端口与角色不参与冲突治理。
     */
    private boolean needControl(String loginPort, String clientKey, String deviceType) {
        if (StringUtils.isBlank(loginPort)) {
            return false;
        }
        if (!StringUtils.equals(loginPort, "port-driver")) {
            return false;
        }
        clientKey = StringUtils.defaultString(clientKey).toLowerCase(Locale.ROOT);
        deviceType = StringUtils.defaultString(deviceType).toLowerCase(Locale.ROOT);
        if (isMobileClient(clientKey) || isMobileClient(deviceType)) {
            return true;
        }
        return StringUtils.isNotBlank(deviceType) && !deviceType.contains("pc");
    }

    /**
     * 优先使用前端传入的X-Device-Id；缺失时降级为UA+IP哈希。
     */
    private String resolveDeviceId(String clientKey, String deviceType, Long userId) {
        HttpServletRequest request = ServletUtils.getRequest();
        if (request != null) {
            // 前端标准请求头
            String fromHeader = StringUtils.defaultIfBlank(request.getHeader("X-Device-Id"), request.getHeader("device-id"));
            if (StringUtils.isNotBlank(fromHeader)) {
                return fromHeader.trim();
            }
            // 兼容参数透传
            String fromParam = request.getParameter("deviceId");
            if (StringUtils.isNotBlank(fromParam)) {
                return fromParam.trim();
            }
            // 兜底策略：UA + IP 哈希
            String seed = StringUtils.defaultString(request.getHeader("User-Agent")) + "|" + StringUtils.defaultString(ServletUtils.getClientIP());
            return DigestUtil.md5Hex(seed);
        }
        // 无 request 上下文时（极少场景）使用登录参数拼接兜底
        String seed = StringUtils.defaultString(clientKey) + "|" + StringUtils.defaultString(deviceType) + "|" + userId;
        return DigestUtil.md5Hex(seed);
    }

    /**
     * 踢出旧token并发送SSE强制下线事件。
     */
    private void forceKick(String tokenValue, String reason) {
        LoginUser oldUser = LoginHelper.getLoginUser(tokenValue);
        if (oldUser != null) {
            // 给旧会话推送强制下线事件，前端收到后弹窗并回登录页
            String payload = "{\"type\":\"FORCE_LOGOUT\",\"targetToken\":\"" + tokenValue + "\",\"reason\":\"" + reason + "\",\"message\":\"" + FORCE_LOGOUT_MSG + "\"}";
            SseMessageDto dto = new SseMessageDto();
            dto.setUserIds(Collections.singletonList(oldUser.getUserId()));
            dto.setMessage(payload);
            SseMessageUtils.publishMessage(dto);
        }
        try {
            // 统一走 Sa-Token 踢下线，后续请求会被 checkLogin 拦截
            StpUtil.kickoutByTokenValue(tokenValue);
        } catch (Exception e) {
            log.warn("踢出token失败, tokenValue={}, reason={}", tokenValue, reason);
        }
        updateTokenStatus(tokenValue, reason);
    }

    /**
     * 持久化登录绑定记录，用于审计与排障。
     */
    private void persistLoginBind(SessionPrincipal principal, String tokenValue, String deviceId) {
        Date now = DateUtil.date();
        DriverDeviceTokenBind bind = new DriverDeviceTokenBind();
        bind.setTenantId(principal.getTenantId());
        bind.setUserId(principal.getUserId());
        bind.setDriverId(principal.getDriverId());
        bind.setDeviceId(deviceId);
        bind.setTokenValue(tokenValue);
        bind.setClientKey(principal.getClientKey());
        bind.setDeviceType(principal.getDeviceType());
        bind.setStatus(1);
        bind.setLoginTime(now);
        bindMapper.insert(bind);
    }

    /**
     * 标记token绑定记录为已踢下线。
     */
    private void updateTokenStatus(String tokenValue, String reason) {
        LambdaUpdateWrapper<DriverDeviceTokenBind> luw = new LambdaUpdateWrapper<>();
        luw.eq(DriverDeviceTokenBind::getTokenValue, tokenValue);
        luw.eq(DriverDeviceTokenBind::getStatus, 1);
        luw.set(DriverDeviceTokenBind::getStatus, 2);
        luw.set(DriverDeviceTokenBind::getKickReason, reason);
        luw.set(DriverDeviceTokenBind::getKickTime, DateUtil.date());
        bindMapper.update(null, luw);
    }

    private void cache(String key, Object value, long timeoutSeconds) {
        if (timeoutSeconds < 0) {
            RedisUtils.setCacheObject(key, value);
            return;
        }
        RedisUtils.setCacheObject(key, value, Duration.ofSeconds(Math.max(timeoutSeconds, 60)));
    }

    private boolean isMobileClient(String text) {
        return text.contains("app") || text.contains("xcx") || text.contains("mini")
                || text.contains("h5") || text.contains("mobile")
                || text.contains("android") || text.contains("ios");
    }

    private SessionPrincipal resolvePrincipal(SaLoginParameter loginParameter) {
        SessionPrincipal resolved = new SessionPrincipal();
        resolved.setTenantId((String) loginParameter.getExtra(LoginHelper.TENANT_KEY));
        Object userIdObj = loginParameter.getExtra(LoginHelper.USER_KEY);
        if (userIdObj != null) {
            resolved.setUserId(Long.valueOf(userIdObj.toString()));
        }
        Object businessUserObj = loginParameter.getExtra(LoginHelper.BUSINESS_USER_KEY);
        if (businessUserObj != null) {
            resolved.setDriverId(businessUserObj.toString());
        }
        Object loginPortObj = loginParameter.getExtra(LoginHelper.LOGIN_PORT);
        if (loginPortObj != null) {
            resolved.setLoginPort(loginPortObj.toString());
        }
        Object clientObj = loginParameter.getExtra(LoginHelper.CLIENT_KEY);
        if (clientObj != null) {
            resolved.setClientKey(clientObj.toString());
        }
        resolved.setDeviceType(loginParameter.getDeviceType());
        return resolved;
    }

    @lombok.Data
    private static class SessionPrincipal {
        private String tenantId;
        private Long userId;
        private String driverId;
        private String loginPort;
        private String clientKey;
        private String deviceType;
    }

    @lombok.Data
    private static class TokenCtx {
        private String tenantId;
        private Long userId;
        private String driverId;
        private String deviceId;
    }
}
