package com.writeoff.module.auth.controller;

import com.writeoff.common.api.ApiErrorResponse;
import com.writeoff.common.api.ApiResponse;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.module.auth.dto.LoginRequest;
import com.writeoff.module.auth.dto.PasswordSetupCompleteRequest;
import com.writeoff.module.auth.dto.SwitchTenantRequest;
import com.writeoff.module.auth.dto.TenantLoginRequest;
import com.writeoff.module.auth.model.TenantSwitchOption;
import com.writeoff.module.auth.service.RefreshTokenService;
import com.writeoff.module.system.model.ProfilePreferencesInfo;
import com.writeoff.module.system.service.PlatformIamService;
import com.writeoff.module.system.service.SystemUserService;
import com.writeoff.security.AuthContext;
import com.writeoff.security.AuthScope;
import com.writeoff.security.CaptchaService;
import com.writeoff.security.DataScopeType;
import com.writeoff.security.JwtTokenService;
import com.writeoff.security.LoginPasswordCryptoService;
import com.writeoff.security.LoginAttemptService;
import com.writeoff.security.PasswordCodecService;
import com.writeoff.security.PasswordSetupService;
import com.writeoff.security.PermissionService;
import com.writeoff.security.RequirePermission;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    private final JdbcTemplate jdbcTemplate;
    private final JwtTokenService jwtTokenService;
    private final PermissionService permissionService;
    private final SystemUserService systemUserService;
    private final PlatformIamService platformIamService;
    private final RefreshTokenService refreshTokenService;
    private final LoginAttemptService loginAttemptService;
    private final CaptchaService captchaService;
    private final LoginPasswordCryptoService loginPasswordCryptoService;
    private final PasswordCodecService passwordCodecService;
    private final PasswordSetupService passwordSetupService;
    private final String refreshCookieName;
    private final boolean refreshCookieSecure;
    private final String refreshCookieSameSite;

    public AuthController(JdbcTemplate jdbcTemplate,
                          JwtTokenService jwtTokenService,
                          PermissionService permissionService,
                          SystemUserService systemUserService,
                          PlatformIamService platformIamService,
                          RefreshTokenService refreshTokenService,
                          LoginAttemptService loginAttemptService,
                          CaptchaService captchaService,
                          LoginPasswordCryptoService loginPasswordCryptoService,
                          PasswordCodecService passwordCodecService,
                          PasswordSetupService passwordSetupService,
                          @Value("${app.security.refresh-cookie-name:refreshToken}") String refreshCookieName,
                          @Value("${app.security.refresh-cookie-secure:false}") boolean refreshCookieSecure,
                          @Value("${app.security.refresh-cookie-same-site:Lax}") String refreshCookieSameSite) {
        this.jdbcTemplate = jdbcTemplate;
        this.jwtTokenService = jwtTokenService;
        this.permissionService = permissionService;
        this.systemUserService = systemUserService;
        this.platformIamService = platformIamService;
        this.refreshTokenService = refreshTokenService;
        this.loginAttemptService = loginAttemptService;
        this.captchaService = captchaService;
        this.loginPasswordCryptoService = loginPasswordCryptoService;
        this.passwordCodecService = passwordCodecService;
        this.passwordSetupService = passwordSetupService;
        this.refreshCookieName = refreshCookieName;
        this.refreshCookieSecure = refreshCookieSecure;
        this.refreshCookieSameSite = refreshCookieSameSite;
    }

    @GetMapping("/password-public-key")
    public ApiResponse<Map<String, String>> passwordPublicKey() {
        return ApiResponse.success(Collections.singletonMap("publicKey", loginPasswordCryptoService.getEncodedPublicKey()));
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody @Valid TenantLoginRequest request,
                                   HttpServletRequest httpRequest,
                                   HttpServletResponse httpResponse) {
        // 验证码校验
        if (!captchaService.verify(request.getCaptchaId(), request.getCaptchaCode())) {
            Map<String, String> errors = new LinkedHashMap<>();
            errors.put("captcha", "invalid");
            return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
                    .body(ApiErrorResponse.of(11010, "验证码错误或已过期", errors));
        }
        // 登录锁定检查
        String lockKey = "tenant:" + request.getTenantCode() + ":" + request.getPhone();
        if (loginAttemptService.isLocked(lockKey)) {
            long remaining = loginAttemptService.getRemainingLockSeconds(lockKey);
            Map<String, String> errors = new LinkedHashMap<>();
            errors.put("lockRemainingSeconds", String.valueOf(remaining));
            return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
                    .body(ApiErrorResponse.of(11005, "账号已被锁定，请" + (remaining / 60 + 1) + "分钟后重试", errors));
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT u.id, u.tenant_id, u.user_name, u.phone, u.status, u.password_hash, u.valid_from, u.valid_to, " +
                        "t.tenant_code, t.tenant_name, t.status AS tenant_status " +
                        "FROM sys_user u JOIN tenant t ON u.tenant_id=t.id " +
                        "WHERE u.phone=? AND t.tenant_code=? AND u.is_deleted=0 AND t.is_deleted=0 LIMIT 1",
                request.getPhone(),
                request.getTenantCode()
        );
        if (rows.isEmpty()) {
            return buildLoginFailResponse(lockKey);
        }
        Map<String, Object> row = rows.get(0);
        String dbPassword = String.valueOf(row.get("password_hash"));
        String rawPassword = resolveSubmittedPassword(request.getPassword());
        if (rawPassword == null || !passwordCodecService.matches(rawPassword, dbPassword)) {
            return buildLoginFailResponse(lockKey);
        }
        // 登录成功，清除失败记录
        loginAttemptService.clearFailures(lockKey);
        String status = String.valueOf(row.get("status"));
        if (!"ENABLED".equals(status)) {
            throw new BusinessException(11003, "会话失效");
        }
        Long userId = ((Number) row.get("id")).longValue();
        Long tenantId = ((Number) row.get("tenant_id")).longValue();
        systemUserService.onSuccessfulLogin(userId, tenantId, request.getPhone(), rawPassword);
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime validFrom = toLocalDateTime(row.get("valid_from"));
        LocalDateTime validTo = toLocalDateTime(row.get("valid_to"));
        if (validFrom != null && now.isBefore(validFrom)) {
            throw new BusinessException(11004, "账号尚未生效");
        }
        if (validTo != null && now.isAfter(validTo)) {
            throw new BusinessException(11004, "账号已过有效期");
        }
        String tenantStatus = String.valueOf(row.get("tenant_status"));
        if (!"ENABLED".equals(tenantStatus)) {
            throw new BusinessException(11003, "租户已停用");
        }
        Map<String, Object> issueResult = refreshTokenService.issueWithSession(
                userId,
                tenantId,
                AuthScope.TENANT,
                httpRequest.getRemoteAddr(),
                httpRequest.getHeader("User-Agent")
        );
        Long sessionId = issueResult.get("sessionId") == null ? null : ((Number) issueResult.get("sessionId")).longValue();
        String refreshToken = String.valueOf(issueResult.get("refreshToken"));
        String token = jwtTokenService.createTenantToken(userId, tenantId, request.getPhone(), sessionId);
        setRefreshCookie(httpResponse, refreshToken, false);
        Map<String, Object> data = buildTenantAuthData(userId, tenantId, String.valueOf(row.get("user_name")), request.getPhone(), token);
        return ResponseEntity.ok(ApiResponse.success(data));
    }

    @PostMapping("/platform-login")
    public ResponseEntity<?> platformLogin(@RequestBody @Valid LoginRequest request,
                                            HttpServletRequest httpRequest,
                                            HttpServletResponse httpResponse) {
        // 验证码校验
        if (!captchaService.verify(request.getCaptchaId(), request.getCaptchaCode())) {
            Map<String, String> errors = new LinkedHashMap<>();
            errors.put("captcha", "invalid");
            return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
                    .body(ApiErrorResponse.of(11010, "验证码错误或已过期", errors));
        }
        // 登录锁定检查
        String lockKey = "platform:" + request.getPhone();
        if (loginAttemptService.isLocked(lockKey)) {
            long remaining = loginAttemptService.getRemainingLockSeconds(lockKey);
            Map<String, String> errors = new LinkedHashMap<>();
            errors.put("lockRemainingSeconds", String.valueOf(remaining));
            return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
                    .body(ApiErrorResponse.of(11005, "账号已被锁定，请" + (remaining / 60 + 1) + "分钟后重试", errors));
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT id, user_name, phone, status, password_hash, valid_from, valid_to " +
                        "FROM platform_user WHERE phone=? AND is_deleted=0 LIMIT 1",
                request.getPhone()
        );
        if (rows.isEmpty()) {
            return buildLoginFailResponse(lockKey);
        }
        Map<String, Object> row = rows.get(0);
        String dbPassword = String.valueOf(row.get("password_hash"));
        String rawPassword = resolveSubmittedPassword(request.getPassword());
        if (rawPassword == null || !passwordCodecService.matches(rawPassword, dbPassword)) {
            return buildLoginFailResponse(lockKey);
        }
        // 登录成功，清除失败记录
        loginAttemptService.clearFailures(lockKey);
        String status = String.valueOf(row.get("status"));
        if (!"ENABLED".equals(status)) {
            throw new BusinessException(11003, "会话失效");
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime validFrom = toLocalDateTime(row.get("valid_from"));
        LocalDateTime validTo = toLocalDateTime(row.get("valid_to"));
        if (validFrom != null && now.isBefore(validFrom)) {
            throw new BusinessException(11004, "账号尚未生效");
        }
        if (validTo != null && now.isAfter(validTo)) {
            throw new BusinessException(11004, "账号已过有效期");
        }
        Long userId = ((Number) row.get("id")).longValue();
        platformIamService.onSuccessfulLogin(userId, rawPassword);
        Map<String, Object> issueResult = refreshTokenService.issueWithSession(
                userId,
                null,
                AuthScope.PLATFORM,
                httpRequest.getRemoteAddr(),
                httpRequest.getHeader("User-Agent")
        );
        Long sessionId = issueResult.get("sessionId") == null ? null : ((Number) issueResult.get("sessionId")).longValue();
        String refreshToken = String.valueOf(issueResult.get("refreshToken"));
        String token = jwtTokenService.createPlatformToken(userId, request.getPhone(), sessionId);
        setRefreshCookie(httpResponse, refreshToken, false);
        Map<String, Object> data = buildPlatformAuthData(userId, String.valueOf(row.get("user_name")), request.getPhone(), token);
        return ResponseEntity.ok(ApiResponse.success(data));
    }

    @GetMapping("/password-setup/verify")
    public ApiResponse<Map<String, Object>> verifyPasswordSetup(@RequestParam("tenantCode") String tenantCode,
                                                                @RequestParam("token") String token) {
        return ApiResponse.success(passwordSetupService.verifyTenantPasswordSetupToken(tenantCode, token));
    }

    @PostMapping("/password-setup/complete")
    public ApiResponse<Map<String, Object>> completePasswordSetup(@RequestBody @Valid PasswordSetupCompleteRequest request) {
        return ApiResponse.success(passwordSetupService.completeTenantPasswordSetup(
                request.getTenantCode(),
                request.getToken(),
                request.getNewPassword()
        ));
    }

    /**
     * 构建登录失败响应（含剩余尝试次数结构化数据）。
     */
    private ResponseEntity<ApiErrorResponse> buildLoginFailResponse(String lockKey) {
        LoginAttemptService.LoginAttemptStatus status = loginAttemptService.recordFailure(lockKey);
        int remaining = status.getRemainingAttempts();
        Map<String, String> errors = new LinkedHashMap<>();
        errors.put("remainingAttempts", String.valueOf(remaining));
        if (status.isLocked()) {
            errors.put("lockRemainingSeconds", String.valueOf(status.getRemainingLockSeconds()));
        }
        String message = !status.isLocked() && remaining > 0
                ? "账号或密码错误，还可尝试" + remaining + "次"
                : "账号或密码错误，账号已被锁定";
        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
                .body(ApiErrorResponse.of(11001, message, errors));
    }

    private String resolveSubmittedPassword(String submittedPassword) {
        try {
            return loginPasswordCryptoService.unwrapPassword(submittedPassword);
        } catch (IllegalArgumentException ex) {
            return null;
        }
    }

    @PostMapping("/refresh")
    public ApiResponse<Map<String, Object>> refresh(HttpServletRequest request, HttpServletResponse response) {
        String currentRefreshToken = getCookieValue(request, refreshCookieName);
        Map<String, Object> rotateResult = refreshTokenService.rotate(currentRefreshToken, request.getRemoteAddr(), request.getHeader("User-Agent"));
        Long userId = ((Number) rotateResult.get("userId")).longValue();
        Long tenantId = rotateResult.get("tenantId") == null ? null : ((Number) rotateResult.get("tenantId")).longValue();
        AuthScope scope = (AuthScope) rotateResult.get("scope");
        String nextRefreshToken = String.valueOf(rotateResult.get("refreshToken"));
        try {
            Map<String, Object> data;
            if (scope == AuthScope.TENANT) {
                validateTenantSession(userId, tenantId);
                String phone = jdbcTemplate.queryForObject(
                        "SELECT phone FROM sys_user WHERE id=? AND tenant_id=? AND is_deleted=0 LIMIT 1",
                        String.class,
                        userId,
                        tenantId
                );
                String userName = jdbcTemplate.queryForObject(
                        "SELECT user_name FROM sys_user WHERE id=? AND tenant_id=? AND is_deleted=0 LIMIT 1",
                        String.class,
                        userId,
                        tenantId
                );
                Long sessionId = rotateResult.get("sessionId") == null ? null : ((Number) rotateResult.get("sessionId")).longValue();
                String token = jwtTokenService.createTenantToken(userId, tenantId, phone, sessionId);
                data = buildTenantAuthData(userId, tenantId, userName, phone, token);
            } else {
                validatePlatformSession(userId);
                String phone = jdbcTemplate.queryForObject(
                        "SELECT phone FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1",
                        String.class,
                        userId
                );
                String userName = jdbcTemplate.queryForObject(
                        "SELECT user_name FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1",
                        String.class,
                        userId
                );
                Long sessionId = rotateResult.get("sessionId") == null ? null : ((Number) rotateResult.get("sessionId")).longValue();
                String token = jwtTokenService.createPlatformToken(userId, phone, sessionId);
                data = buildPlatformAuthData(userId, userName, phone, token);
            }
            setRefreshCookie(response, nextRefreshToken, false);
            return ApiResponse.success(data);
        } catch (BusinessException ex) {
            refreshTokenService.revokeAllByPrincipal(userId, tenantId, scope, "REFRESH_VALIDATE_FAILED");
            setRefreshCookie(response, "", true);
            throw ex;
        }
    }

    @GetMapping("/switchable-tenants")
    @RequirePermission(value = "tenant.switch", dataScope = DataScopeType.TENANT, auditAction = "AUTH_SWITCHABLE_TENANTS")
    public ApiResponse<List<TenantSwitchOption>> switchableTenants() {
        Long currentUserId = AuthContext.userId();
        Long currentTenantId = AuthContext.requireTenantId();
        validateTenantSession(currentUserId, currentTenantId);
        return ApiResponse.success(loadSwitchableTenants(currentUserId, currentTenantId));
    }

    @PostMapping("/switch-tenant")
    @RequirePermission(value = "tenant.switch", dataScope = DataScopeType.TENANT, auditAction = "AUTH_SWITCH_TENANT")
    public ApiResponse<Map<String, Object>> switchTenant(@RequestBody @Valid SwitchTenantRequest request,
                                                         HttpServletRequest httpRequest,
                                                         HttpServletResponse httpResponse) {
        Long currentUserId = AuthContext.userId();
        Long currentTenantId = AuthContext.requireTenantId();
        validateTenantSession(currentUserId, currentTenantId);

        Map<String, Object> currentIdentity = loadCurrentTenantIdentity(currentUserId, currentTenantId);
        Long targetTenantId = request.getTenantId();
        if (targetTenantId == null) {
            throw new BusinessException(10001, "\u76ee\u6807\u79df\u6237\u4e0d\u80fd\u4e3a\u7a7a");
        }
        Map<String, Object> targetIdentity = loadSwitchTarget(
                targetTenantId,
                String.valueOf(currentIdentity.get("tenant_switch_account_key")),
                String.valueOf(currentIdentity.get("phone")),
                String.valueOf(currentIdentity.get("password_hash"))
        );

        Long targetUserId = ((Number) targetIdentity.get("user_id")).longValue();
        validateTenantSession(targetUserId, targetTenantId);

        Map<String, Object> issueResult = refreshTokenService.issueWithSession(
                targetUserId,
                targetTenantId,
                AuthScope.TENANT,
                httpRequest.getRemoteAddr(),
                httpRequest.getHeader("User-Agent")
        );
        Long sessionId = issueResult.get("sessionId") == null ? null : ((Number) issueResult.get("sessionId")).longValue();
        String refreshToken = String.valueOf(issueResult.get("refreshToken"));
        String phone = String.valueOf(targetIdentity.get("phone"));
        String token = jwtTokenService.createTenantToken(targetUserId, targetTenantId, phone, sessionId);
        setRefreshCookie(httpResponse, refreshToken, false);
        return ApiResponse.success(buildTenantAuthData(targetUserId, targetTenantId, String.valueOf(targetIdentity.get("user_name")), phone, token));
    }

    @PostMapping("/logout")
    public ApiResponse<Map<String, Object>> logout(HttpServletRequest request, HttpServletResponse response) {
        String token = getCookieValue(request, refreshCookieName);
        refreshTokenService.revokeCurrent(token, "LOGOUT");
        setRefreshCookie(response, "", true);
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("ok", Boolean.TRUE);
        return ApiResponse.success(data);
    }

    @PostMapping("/logout-all")
    public ApiResponse<Map<String, Object>> logoutAll(HttpServletRequest request, HttpServletResponse response) {
        String currentRefreshToken = getCookieValue(request, refreshCookieName);
        Map<String, Object> rotateResult = refreshTokenService.rotate(currentRefreshToken, request.getRemoteAddr(), request.getHeader("User-Agent"));
        Long userId = ((Number) rotateResult.get("userId")).longValue();
        Long tenantId = rotateResult.get("tenantId") == null ? null : ((Number) rotateResult.get("tenantId")).longValue();
        AuthScope scope = (AuthScope) rotateResult.get("scope");
        refreshTokenService.revokeAllByPrincipal(userId, tenantId, scope, "LOGOUT_ALL");
        setRefreshCookie(response, "", true);
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("ok", Boolean.TRUE);
        return ApiResponse.success(data);
    }

    private LocalDateTime toLocalDateTime(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof LocalDateTime) {
            return (LocalDateTime) value;
        }
        if (value instanceof java.sql.Timestamp) {
            return ((java.sql.Timestamp) value).toLocalDateTime();
        }
        if (value instanceof java.util.Date) {
            return new java.sql.Timestamp(((java.util.Date) value).getTime()).toLocalDateTime();
        }
        if (value instanceof String) {
            String text = String.valueOf(value).trim();
            if (text.isEmpty()) {
                return null;
            }
            try {
                return LocalDateTime.parse(text.replace(' ', 'T'));
            } catch (Exception ignored) {
            }
        }
        return null;
    }

    private Map<String, Object> loadTenantInfo(Long tenantId) {
        List<Map<String, Object>> tenantRows = jdbcTemplate.queryForList(
                "SELECT id, tenant_code, tenant_name, logo_url, status, is_deleted, created_by, created_at, updated_by, updated_at " +
                        "FROM tenant WHERE id=? LIMIT 1",
                tenantId
        );
        if (tenantRows.isEmpty()) {
            return new LinkedHashMap<String, Object>();
        }
        Map<String, Object> r = tenantRows.get(0);
        Map<String, Object> tenant = new LinkedHashMap<String, Object>();
        tenant.put("id", r.get("id"));
        tenant.put("tenantCode", r.get("tenant_code"));
        tenant.put("tenantName", r.get("tenant_name"));
        tenant.put("logoUrl", r.get("logo_url"));
        tenant.put("status", r.get("status"));
        tenant.put("isDeleted", r.get("is_deleted"));
        tenant.put("createdBy", r.get("created_by"));
        tenant.put("createdAt", formatDateTime(r.get("created_at")));
        tenant.put("updatedBy", r.get("updated_by"));
        tenant.put("updatedAt", formatDateTime(r.get("updated_at")));
        return tenant;
    }

    private String formatDateTime(Object value) {
        LocalDateTime dt = toLocalDateTime(value);
        if (dt == null) {
            return "";
        }
        return dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }

    private Map<String, Object> buildTenantAuthData(Long userId, Long tenantId, String userName, String phone, String token) {
        Map<String, Object> tenant = loadTenantInfo(tenantId);
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("token", token);
        data.put("scope", AuthScope.TENANT.name());
        data.put("userId", userId);
        data.put("tenantId", tenantId);
        data.put("tenantCode", tenant.get("tenantCode"));
        data.put("tenantName", tenant.get("tenantName"));
        data.put("tenant", tenant);
        data.put("userName", userName);
        data.put("roles", systemUserService.getUserRoles(userId, tenantId));
        data.put("permissions", permissionService.getPermissions(userId, tenantId));
        data.put("phone", phone);
        data.put("appearance", loadTenantPreferences(userId, tenantId));
        return data;
    }

    private Map<String, Object> buildPlatformAuthData(Long userId, String userName, String phone, String token) {
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("token", token);
        data.put("scope", AuthScope.PLATFORM.name());
        data.put("userId", userId);
        data.put("tenantId", null);
        data.put("userName", userName);
        data.put("roles", permissionService.getPlatformRoles(userId));
        data.put("permissions", permissionService.getPlatformPermissions(userId));
        data.put("phone", phone);
        data.put("appearance", loadPlatformPreferences(userId));
        return data;
    }

    private ProfilePreferencesInfo loadTenantPreferences(Long userId, Long tenantId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT ui_theme_mode, ui_density, ui_theme_scheme FROM sys_user WHERE id=? AND tenant_id=? AND is_deleted=0 LIMIT 1",
                userId,
                tenantId
        );
        if (rows.isEmpty()) {
            return new ProfilePreferencesInfo("SYSTEM", "COMFORTABLE", "SLATE");
        }
        Map<String, Object> row = rows.get(0);
        return new ProfilePreferencesInfo(
                systemUserService.normalizeThemeMode(row.get("ui_theme_mode") == null ? null : String.valueOf(row.get("ui_theme_mode"))),
                systemUserService.normalizeDensity(row.get("ui_density") == null ? null : String.valueOf(row.get("ui_density"))),
                systemUserService.normalizeThemeScheme(row.get("ui_theme_scheme") == null ? null : String.valueOf(row.get("ui_theme_scheme")))
        );
    }

    private ProfilePreferencesInfo loadPlatformPreferences(Long userId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT ui_theme_mode, ui_density, ui_theme_scheme FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1",
                userId
        );
        if (rows.isEmpty()) {
            return new ProfilePreferencesInfo("SYSTEM", "COMFORTABLE", "SLATE");
        }
        Map<String, Object> row = rows.get(0);
        return new ProfilePreferencesInfo(
                normalizeThemeMode(row.get("ui_theme_mode")),
                normalizeDensity(row.get("ui_density")),
                normalizeThemeScheme(row.get("ui_theme_scheme"))
        );
    }

    private String normalizeThemeMode(Object raw) {
        String value = raw == null ? "" : String.valueOf(raw).trim().toUpperCase();
        if ("LIGHT".equals(value) || "DARK".equals(value) || "SYSTEM".equals(value)) {
            return value;
        }
        return "SYSTEM";
    }

    private String normalizeDensity(Object raw) {
        String value = raw == null ? "" : String.valueOf(raw).trim().toUpperCase();
        if ("COMPACT".equals(value) || "COMFORTABLE".equals(value)) {
            return value;
        }
        return "COMFORTABLE";
    }

    private String normalizeThemeScheme(Object raw) {
        String value = raw == null ? "" : String.valueOf(raw).trim().toUpperCase();
        if (
                "SLATE".equals(value) ||
                "OCEAN".equals(value) ||
                "FOREST".equals(value) ||
                "GRAPHITE".equals(value) ||
                "AMBER".equals(value) ||
                "RUBY".equals(value) ||
                "MIST".equals(value) ||
                "SAGE".equals(value) ||
                "DAWN".equals(value)
        ) {
            return value;
        }
        return "SLATE";
    }

    private void validateTenantSession(Long userId, Long tenantId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT u.status, u.valid_from, u.valid_to, t.status AS tenant_status " +
                        "FROM sys_user u JOIN tenant t ON u.tenant_id=t.id " +
                        "WHERE u.id=? AND u.tenant_id=? AND u.is_deleted=0 AND t.is_deleted=0 LIMIT 1",
                userId,
                tenantId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        Map<String, Object> row = rows.get(0);
        String userStatus = String.valueOf(row.get("status"));
        String tenantStatus = String.valueOf(row.get("tenant_status"));
        if (!"ENABLED".equals(userStatus) || !"ENABLED".equals(tenantStatus)) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime validFrom = toLocalDateTime(row.get("valid_from"));
        LocalDateTime validTo = toLocalDateTime(row.get("valid_to"));
        if (validFrom != null && now.isBefore(validFrom)) {
            throw new BusinessException(ErrorCodes.ACCOUNT_EXPIRED, "账号尚未生效");
        }
        if (validTo != null && now.isAfter(validTo)) {
            throw new BusinessException(ErrorCodes.ACCOUNT_EXPIRED, "账号已过有效期");
        }
    }

    private void validatePlatformSession(Long userId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT status, valid_from, valid_to FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1",
                userId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        Map<String, Object> row = rows.get(0);
        String userStatus = String.valueOf(row.get("status"));
        if (!"ENABLED".equals(userStatus)) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime validFrom = toLocalDateTime(row.get("valid_from"));
        LocalDateTime validTo = toLocalDateTime(row.get("valid_to"));
        if (validFrom != null && now.isBefore(validFrom)) {
            throw new BusinessException(ErrorCodes.ACCOUNT_EXPIRED, "账号尚未生效");
        }
        if (validTo != null && now.isAfter(validTo)) {
            throw new BusinessException(ErrorCodes.ACCOUNT_EXPIRED, "账号已过有效期");
        }
    }

    private List<TenantSwitchOption> loadSwitchableTenants(Long currentUserId, Long currentTenantId) {
        Map<String, Object> currentIdentity = loadCurrentTenantIdentity(currentUserId, currentTenantId);
        String switchAccountKey = normalizeTenantSwitchAccountKey(currentIdentity.get("tenant_switch_account_key"));
        if (!switchAccountKey.isEmpty()) {
            return jdbcTemplate.query(
                    "SELECT u.tenant_id, t.tenant_code, t.tenant_name, t.logo_url " +
                            "FROM sys_user u " +
                            "JOIN tenant t ON u.tenant_id=t.id " +
                            "WHERE u.tenant_switch_account_key=? AND u.is_deleted=0 AND u.status='ENABLED' " +
                            "AND (u.valid_from IS NULL OR u.valid_from<=NOW()) " +
                            "AND (u.valid_to IS NULL OR u.valid_to>=NOW()) " +
                            "AND t.is_deleted=0 AND t.status='ENABLED' " +
                            "ORDER BY CASE WHEN u.tenant_id=? THEN 0 ELSE 1 END, t.tenant_name ASC, t.id ASC",
                    (rs, n) -> new TenantSwitchOption(
                            rs.getLong("tenant_id"),
                            rs.getString("tenant_code"),
                            rs.getString("tenant_name"),
                            rs.getString("logo_url"),
                            rs.getLong("tenant_id") == currentTenantId
                    ),
                    switchAccountKey,
                    currentTenantId
            );
        }
        String phone = String.valueOf(currentIdentity.get("phone"));
        String passwordHash = String.valueOf(currentIdentity.get("password_hash"));
        return jdbcTemplate.query(
                "SELECT u.tenant_id, t.tenant_code, t.tenant_name, t.logo_url " +
                        "FROM sys_user u " +
                        "JOIN tenant t ON u.tenant_id=t.id " +
                        "WHERE u.phone=? AND u.password_hash=? AND u.is_deleted=0 AND u.status='ENABLED' " +
                        "AND (u.valid_from IS NULL OR u.valid_from<=NOW()) " +
                        "AND (u.valid_to IS NULL OR u.valid_to>=NOW()) " +
                        "AND t.is_deleted=0 AND t.status='ENABLED' " +
                        "ORDER BY CASE WHEN u.tenant_id=? THEN 0 ELSE 1 END, t.tenant_name ASC, t.id ASC",
                (rs, n) -> new TenantSwitchOption(
                        rs.getLong("tenant_id"),
                        rs.getString("tenant_code"),
                        rs.getString("tenant_name"),
                        rs.getString("logo_url"),
                        rs.getLong("tenant_id") == currentTenantId
                ),
                phone,
                passwordHash,
                currentTenantId
        );
    }

    private Map<String, Object> loadCurrentTenantIdentity(Long userId, Long tenantId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT id, tenant_id, user_name, phone, password_hash, tenant_switch_account_key " +
                        "FROM sys_user WHERE id=? AND tenant_id=? AND is_deleted=0 LIMIT 1",
                userId,
                tenantId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "\u4f1a\u8bdd\u5931\u6548");
        }
        return rows.get(0);
    }

    private Map<String, Object> loadSwitchTarget(Long tenantId, String switchAccountKey, String phone, String passwordHash) {
        String normalizedSwitchAccountKey = normalizeTenantSwitchAccountKey(switchAccountKey);
        if (!normalizedSwitchAccountKey.isEmpty()) {
            List<Map<String, Object>> accountKeyRows = jdbcTemplate.queryForList(
                    "SELECT u.id AS user_id, u.tenant_id, u.user_name, u.phone, u.password_hash, u.tenant_switch_account_key " +
                            "FROM sys_user u " +
                            "JOIN tenant t ON u.tenant_id=t.id " +
                            "WHERE u.tenant_id=? AND u.tenant_switch_account_key=? " +
                            "AND u.is_deleted=0 AND u.status='ENABLED' AND t.is_deleted=0 AND t.status='ENABLED' " +
                            "LIMIT 1",
                    tenantId,
                    normalizedSwitchAccountKey
            );
            if (!accountKeyRows.isEmpty()) {
                return accountKeyRows.get(0);
            }
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT u.id AS user_id, u.tenant_id, u.user_name, u.phone, u.password_hash, u.tenant_switch_account_key " +
                        "FROM sys_user u " +
                        "JOIN tenant t ON u.tenant_id=t.id " +
                        "WHERE u.tenant_id=? AND u.phone=? AND u.password_hash=? " +
                        "AND u.is_deleted=0 AND u.status='ENABLED' AND t.is_deleted=0 AND t.status='ENABLED' " +
                        "LIMIT 1",
                tenantId,
                phone,
                passwordHash
        );
        if (rows.isEmpty()) {
            throw new BusinessException(10003, "\u76ee\u6807\u79df\u6237\u4e0d\u53ef\u5207\u6362");
        }
        return rows.get(0);
    }

    private String normalizeTenantSwitchAccountKey(Object raw) {
        return raw == null ? "" : String.valueOf(raw).trim();
    }

    private String getCookieValue(HttpServletRequest request, String cookieName) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null || cookies.length == 0) {
            return null;
        }
        for (Cookie cookie : cookies) {
            if (cookie == null) {
                continue;
            }
            if (cookieName.equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
        return null;
    }

    private void setRefreshCookie(HttpServletResponse response, String token, boolean clear) {
        String sameSite = refreshCookieSameSite == null || refreshCookieSameSite.trim().isEmpty()
                ? "Lax"
                : refreshCookieSameSite.trim();
        StringBuilder sb = new StringBuilder();
        sb.append(refreshCookieName).append("=").append(clear ? "" : token);
        sb.append("; Path=/api/auth");
        if (clear) {
            sb.append("; Max-Age=0");
            sb.append("; Expires=Thu, 01 Jan 1970 00:00:00 GMT");
        }
        sb.append("; HttpOnly");
        if (refreshCookieSecure) {
            sb.append("; Secure");
        }
        sb.append("; SameSite=").append(sameSite);
        response.addHeader("Set-Cookie", sb.toString());
    }
}
