package com.writeoff.module.system.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.common.api.PageResult;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.model.ImportResult;
import com.writeoff.common.util.ImportValidationUtils;
import com.writeoff.module.notification.dto.DispatchNotificationRequest;
import com.writeoff.module.notification.service.NotificationDispatchService;
import com.writeoff.module.system.dto.CreateRoleRequest;
import com.writeoff.module.system.dto.CreateUserRequest;
import com.writeoff.module.system.dto.BindRolePermissionsRequest;
import com.writeoff.module.system.dto.ImportUserItemRequest;
import com.writeoff.module.system.dto.ResetPasswordRequest;
import com.writeoff.module.system.dto.UpdateProfilePreferencesRequest;
import com.writeoff.module.system.dto.UpdateRoleRequest;
import com.writeoff.module.system.model.PermissionInfo;
import com.writeoff.module.system.model.ProfilePreferencesInfo;
import com.writeoff.module.system.model.RoleInfo;
import com.writeoff.module.system.model.SystemUser;
import com.writeoff.module.system.model.UserRoleHistory;
import com.writeoff.security.AuthContext;
import com.writeoff.security.PasswordCodecService;
import com.writeoff.security.PasswordPolicyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
public class SystemUserService {
    private static final Logger log = LoggerFactory.getLogger(SystemUserService.class);
    private final JdbcTemplate jdbcTemplate;
    private final DataPermissionService dataPermissionService;
    private final NotificationDispatchService notificationDispatchService;
    private final PasswordPolicyService passwordPolicyService;
    private final PasswordCodecService passwordCodecService;
    private final TransactionTemplate transactionTemplate;
    private final ObjectMapper objectMapper = new ObjectMapper();

    private static final RowMapper<SystemUser> USER_ROW_MAPPER = (rs, n) -> new SystemUser(
            rs.getLong("id"),
            rs.getString("user_name"),
            rs.getString("phone"),
            rs.getString("email"),
            rs.getString("status"),
            rs.getString("valid_from"),
            rs.getString("valid_to"),
            rs.getString("role_codes"),
            rs.getString("role_names"),
            rs.getInt("is_deleted") == 1
    );

    private static final RowMapper<RoleInfo> ROLE_ROW_MAPPER = (rs, n) -> new RoleInfo(
            rs.getLong("id"),
            rs.getString("role_code"),
            rs.getString("role_name"),
            rs.getString("status")
    );
    private static final RowMapper<PermissionInfo> PERMISSION_ROW_MAPPER = (rs, n) -> new PermissionInfo(
            rs.getLong("id"),
            rs.getString("permission_code"),
            rs.getString("permission_name"),
            rs.getString("module")
    );

    private static final RowMapper<UserRoleHistory> USER_ROLE_HISTORY_ROW_MAPPER = (rs, n) -> new UserRoleHistory(
            rs.getLong("id"),
            rs.getLong("user_id"),
            rs.getObject("old_role_id") == null ? null : rs.getLong("old_role_id"),
            rs.getLong("new_role_id"),
            rs.getString("action_type"),
            rs.getString("created_at")
    );

    public SystemUserService(JdbcTemplate jdbcTemplate,
                             DataPermissionService dataPermissionService,
                             NotificationDispatchService notificationDispatchService,
                             PasswordPolicyService passwordPolicyService,
                             PasswordCodecService passwordCodecService,
                             PlatformTransactionManager transactionManager) {
        this.jdbcTemplate = jdbcTemplate;
        this.dataPermissionService = dataPermissionService;
        this.notificationDispatchService = notificationDispatchService;
        this.passwordPolicyService = passwordPolicyService;
        this.passwordCodecService = passwordCodecService;
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public PageResult<SystemUser> listUsers(int pageNo, int pageSize, Boolean includeDeleted) {
        return listUsers(pageNo, pageSize, includeDeleted, null);
    }

    public PageResult<SystemUser> listUsers(int pageNo, int pageSize, Boolean includeDeleted, String keyword) {
        int safePage = Math.max(pageNo, 1);
        int safeSize = Math.min(Math.max(pageSize, 1), 100);
        int offset = (safePage - 1) * safeSize;
        boolean withDeleted = Boolean.TRUE.equals(includeDeleted);
        String normalizedKeyword = keyword == null ? "" : keyword.trim();

        StringBuilder totalWhereSql = new StringBuilder("WHERE u.tenant_id=?");
        StringBuilder dataWhereSql = new StringBuilder("WHERE u.tenant_id=?");
        List<Object> whereArgs = new ArrayList<Object>();
        whereArgs.add(tenantId());
        if (!withDeleted) {
            totalWhereSql.append(" AND u.is_deleted=0");
            dataWhereSql.append(" AND u.is_deleted=0");
        }
        if (!normalizedKeyword.isEmpty()) {
            String like = "%" + normalizedKeyword + "%";
            totalWhereSql.append(" AND (u.user_name LIKE ? OR u.phone LIKE ? OR u.email LIKE ?)");
            dataWhereSql.append(" AND (u.user_name LIKE ? OR u.phone LIKE ? OR u.email LIKE ?)");
            whereArgs.add(like);
            whereArgs.add(like);
            whereArgs.add(like);
        }

        Integer total = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM sys_user u " + totalWhereSql,
                Integer.class,
                whereArgs.toArray()
        );
        long totalCount = total == null ? 0 : total;

        List<Object> dataArgs = new ArrayList<Object>(whereArgs);
        dataArgs.add(safeSize);
        dataArgs.add(offset);
        List<SystemUser> list = jdbcTemplate.query(
                "SELECT u.id, u.user_name, u.phone, u.email, u.status, " +
                        "DATE_FORMAT(u.valid_from, '%Y-%m-%d %H:%i:%s') AS valid_from, " +
                        "DATE_FORMAT(u.valid_to, '%Y-%m-%d %H:%i:%s') AS valid_to, " +
                        "COALESCE(GROUP_CONCAT(DISTINCT r.role_code ORDER BY r.id SEPARATOR ','), '') AS role_codes, " +
                        "COALESCE(GROUP_CONCAT(DISTINCT r.role_name ORDER BY r.id SEPARATOR ','), '') AS role_names, " +
                        "u.is_deleted AS is_deleted " +
                        "FROM sys_user u " +
                        "LEFT JOIN user_role ur ON u.tenant_id=ur.tenant_id AND u.id=ur.user_id " +
                        "LEFT JOIN role r ON ur.tenant_id=r.tenant_id AND ur.role_id=r.id AND r.is_deleted=0 " +
                        dataWhereSql + " " +
                        "GROUP BY u.id, u.user_name, u.phone, u.email, u.status, u.valid_from, u.valid_to, u.is_deleted " +
                        "ORDER BY u.id DESC LIMIT ? OFFSET ?",
                USER_ROW_MAPPER,
                dataArgs.toArray()
        );
        if (dataPermissionService != null) {
            DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
            Map<Long, Long> creatorMap = listUserCreators(list.stream().map(SystemUser::getId).collect(Collectors.toList()));
            list = list.stream()
                    .filter(user -> dataPermissionService.canAccessUser(user.getId(), creatorMap.get(user.getId()), scope))
                    .collect(Collectors.toList());
        }
        return new PageResult<>(list, totalCount, safePage, safeSize);
    }

    public PageResult<SystemUser> listUsers(int pageNo, int pageSize) {
        return listUsers(pageNo, pageSize, false, null);
    }

    public SystemUser createUser(CreateUserRequest request) {
        final String userName = request.getUserName() == null ? "" : request.getUserName().trim();
        final String phone = request.getPhone() == null ? "" : request.getPhone().trim();
        final String email = request.getEmail() == null ? "" : request.getEmail().trim();
        final String rawPassword = request.getPassword() == null ? "" : request.getPassword().trim();
        if (rawPassword.isEmpty()) {
            throw new BusinessException(10001, "\u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a");
        }
        passwordPolicyService.validate(rawPassword);
        final String validFrom = request.getValidFrom() == null || request.getValidFrom().trim().isEmpty()
                ? LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
                : normalizeDateTimeString(request.getValidFrom());
        final String validTo = request.getValidTo() == null || request.getValidTo().trim().isEmpty()
                ? "2099-12-31 23:59:59"
                : normalizeDateTimeString(request.getValidTo());
        return transactionTemplate.execute(status -> {
            assertPhoneAvailable(phone, null);
            String passwordHash = passwordCodecService.encode(rawPassword);
            String tenantSwitchAccountKey = resolveTenantSwitchAccountKeyByPhone(phone, null);
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbcTemplate.update(connection -> {
                PreparedStatement ps = connection.prepareStatement(
                        "INSERT INTO sys_user (tenant_id, user_name, phone, email, password_hash, tenant_switch_account_key, status, valid_from, valid_to, created_by, updated_by) " +
                                "VALUES (?, ?, ?, ?, ?, ?, 'ENABLED', ?, ?, ?, ?)",
                        Statement.RETURN_GENERATED_KEYS
                );
                Timestamp validFromTimestamp = parseTimestamp(validFrom);
                Timestamp validToTimestamp = parseTimestamp(validTo);
                Long operator = safeUserId();
                ps.setLong(1, tenantId());
                ps.setString(2, userName);
                ps.setString(3, phone);
                ps.setString(4, email);
                ps.setString(5, passwordHash);
                ps.setString(6, tenantSwitchAccountKey);
                ps.setTimestamp(7, validFromTimestamp);
                ps.setTimestamp(8, validToTimestamp);
                ps.setLong(9, operator);
                ps.setLong(10, operator);
                return ps;
            }, keyHolder);
            Long id = keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
            autoAssignExecutorRoleWhenCreatorIsProjectExecutor(id);
            sendUserCreatedMail(id, userName, phone, email, validFrom, validTo);
            return new SystemUser(id, userName, phone, email, "ENABLED", validFrom, validTo, "", "");
        });
    }

    public SystemUser updateUser(Long userId, CreateUserRequest request) {
        assertUserExists(userId);
        assertPhoneAvailable(request.getPhone(), userId);
        String loadedTenantSwitchAccountKey = loadTenantSwitchAccountKey(userId);
        final String tenantSwitchAccountKey =
                loadedTenantSwitchAccountKey == null || loadedTenantSwitchAccountKey.trim().isEmpty()
                        ? resolveTenantSwitchAccountKeyByPhone(request.getPhone(), userId)
                        : loadedTenantSwitchAccountKey;
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(
                    "UPDATE sys_user SET user_name=?, phone=?, email=?, tenant_switch_account_key=?, valid_from=?, valid_to=?, updated_by=?, updated_at=CURRENT_TIMESTAMP" +
                            (request.getPassword() == null || request.getPassword().trim().isEmpty() ? "" : ", password_hash=?") +
                            " WHERE tenant_id=? AND id=?"
            );
            Timestamp validFrom = parseTimestamp(request.getValidFrom());
            Timestamp validTo = parseTimestamp(request.getValidTo());
            Long operator = safeUserId();
            int idx = 1;
            ps.setString(idx++, request.getUserName());
            ps.setString(idx++, request.getPhone());
            ps.setString(idx++, request.getEmail());
            ps.setString(idx++, tenantSwitchAccountKey);
            ps.setTimestamp(idx++, validFrom == null ? Timestamp.valueOf(LocalDateTime.now()) : validFrom);
            ps.setTimestamp(idx++, validTo == null ? Timestamp.valueOf(LocalDateTime.of(2099, 12, 31, 23, 59, 59)) : validTo);
            ps.setLong(idx++, operator);
            if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
                passwordPolicyService.validate(request.getPassword());
                ps.setString(idx++, passwordCodecService.encode(request.getPassword()));
            }
            ps.setLong(idx++, tenantId());
            ps.setLong(idx, userId);
            return ps;
        });
        String validFrom = request.getValidFrom() == null || request.getValidFrom().trim().isEmpty()
                ? LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
                : normalizeDateTimeString(request.getValidFrom());
        String validTo = request.getValidTo() == null || request.getValidTo().trim().isEmpty()
                ? "2099-12-31 23:59:59"
                : normalizeDateTimeString(request.getValidTo());
        return new SystemUser(userId, request.getUserName(), request.getPhone(), request.getEmail(), null, validFrom, validTo, "", "");
    }

    public ImportResult importUsers(List<ImportUserItemRequest> users) {
        ImportResult result = new ImportResult();
        result.setTotal(users == null ? 0 : users.size());
        if (users == null) {
            return result;
        }
        Set<String> batchPhones = new HashSet<String>();
        for (int i = 0; i < users.size(); i++) {
            ImportUserItemRequest item = users.get(i);
            int rowNo = i + 2;
            try {
                Long roleId = validateImportUser(item, batchPhones);
                CreateUserRequest request = new CreateUserRequest();
                request.setUserName(item.getUserName() == null ? null : item.getUserName().trim());
                request.setPhone(item.getPhone() == null ? null : item.getPhone().trim());
                request.setPassword(item.getPassword() == null ? null : item.getPassword().trim());
                request.setEmail(item.getEmail() == null ? null : item.getEmail().trim());
                request.setValidFrom(item.getValidFrom());
                request.setValidTo(item.getValidTo());
                SystemUser created = createUser(request);
                if (roleId != null) {
                    assignRole(created.getId(), roleId);
                }
                result.markSuccess();
            } catch (Exception ex) {
                result.addError(rowNo, buildUserIdentifier(item), resolveImportMessage(ex));
            }
        }
        return result;
    }

    public PageResult<RoleInfo> listRoles(int pageNo, int pageSize) {
        int safePage = Math.max(pageNo, 1);
        int safeSize = Math.min(Math.max(pageSize, 1), 100);
        int offset = (safePage - 1) * safeSize;

        Integer total = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM role WHERE tenant_id=? AND is_deleted=0",
                Integer.class,
                tenantId()
        );
        long totalCount = total == null ? 0 : total;

        List<RoleInfo> list = jdbcTemplate.query(
                "SELECT * FROM role WHERE tenant_id=? AND is_deleted=0 ORDER BY id DESC LIMIT ? OFFSET ?",
                ROLE_ROW_MAPPER,
                tenantId(),
                safeSize,
                offset
        );
        return new PageResult<>(list, totalCount, safePage, safeSize);
    }

    public void assignRole(Long userId, Long roleId) {
        Integer userExists = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM sys_user WHERE id=? AND is_deleted=0",
                Integer.class,
                userId
        );
        if (userExists == null || userExists == 0) {
            throw new BusinessException(10003, "用户不存在");
        }
        Integer roleExists = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM role WHERE id=? AND is_deleted=0",
                Integer.class,
                roleId
        );
        if (roleExists == null || roleExists == 0) {
            throw new BusinessException(10003, "角色不存在");
        }

        Integer relationExists = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM user_role WHERE tenant_id=? AND user_id=? AND role_id=?",
                Integer.class,
                tenantId(),
                userId,
                roleId
        );
        if (relationExists != null && relationExists > 0) {
            return;
        }
        Long oldRoleId = currentRoleId(userId);
        Long nextId = jdbcTemplate.queryForObject("SELECT IFNULL(MAX(id), 0) + 1 FROM user_role", Long.class);
        jdbcTemplate.update(
                "INSERT INTO user_role (id, tenant_id, user_id, role_id) VALUES (?, ?, ?, ?)",
                nextId,
                tenantId(),
                userId,
                roleId
        );
        jdbcTemplate.update(
                "INSERT INTO user_role_history (tenant_id, user_id, old_role_id, new_role_id, action_type, action_reason, created_by) VALUES (?, ?, ?, ?, 'ASSIGN', ?, 0)",
                tenantId(),
                userId,
                oldRoleId,
                roleId,
                "用户分配角色"
        );
    }

    public List<String> getUserRoles(Long userId) {
        return getUserRoles(userId, tenantId());
    }

    public List<String> getUserRoles(Long userId, Long tenantId) {
        if (tenantId == null) {
            throw new BusinessException(10001, "租户信息不能为空");
        }
        List<String> roleCodes = jdbcTemplate.queryForList(
                "SELECT r.role_code FROM user_role ur " +
                        "JOIN role r ON ur.role_id=r.id " +
                        "WHERE ur.user_id=? AND ur.tenant_id=? AND r.is_deleted=0",
                String.class,
                userId,
                tenantId
        );
        return roleCodes == null ? new ArrayList<String>() : roleCodes;
    }

    public void enableUser(Long userId) {
        assertUserExists(userId);
        jdbcTemplate.update("UPDATE sys_user SET status='ENABLED', updated_at=CURRENT_TIMESTAMP WHERE id=?", userId);
    }

    public void disableUser(Long userId) {
        assertUserExists(userId);
        jdbcTemplate.update("UPDATE sys_user SET status='DISABLED', updated_at=CURRENT_TIMESTAMP WHERE id=?", userId);
    }

    public void softDeleteUser(Long userId) {
        assertUserExists(userId);
        // 检查是否有待处理审核任务
        Integer pendingTasks = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM audit_task WHERE tenant_id=? AND assignee_user_id=? AND is_deleted=0 AND status='PENDING'",
                Integer.class,
                tenantId(),
                userId
        );
        if (pendingTasks != null && pendingTasks > 0) {
            throw new BusinessException(10001, "该用户还有待处理的审核任务，请先转交后再删除");
        }
        jdbcTemplate.update(
                "UPDATE sys_user SET is_deleted=1, status='DISABLED', updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                safeUserId(),
                tenantId(),
                userId
        );
    }

    public void resetPassword(Long userId, ResetPasswordRequest request) {
        assertUserExists(userId);
        passwordPolicyService.validate(request.getNewPassword());
        jdbcTemplate.update(
                "UPDATE sys_user SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
                passwordCodecService.encode(request.getNewPassword()),
                userId
        );
    }

    public void changeMyPassword(Long userId, String oldPassword, String newPassword) {
        assertUserExists(userId);
        passwordPolicyService.validate(newPassword);
        String currentPasswordHash = jdbcTemplate.queryForObject(
                "SELECT password_hash FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                String.class,
                tenantId(),
                userId
        );
        if (!passwordCodecService.matches(oldPassword, currentPasswordHash)) {
            throw new BusinessException(11001, "原密码不正确");
        }
        jdbcTemplate.update(
                "UPDATE sys_user SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                passwordCodecService.encode(newPassword),
                tenantId(),
                userId
        );
    }

    public void onSuccessfulLogin(Long userId, Long tenantId, String phone, String rawPassword) {
        upgradeStoredPasswordHash(userId, tenantId, rawPassword);
        ensureTenantSwitchAccountKeyForPassword(phone, rawPassword);
    }

    public ProfilePreferencesInfo getMyPreferences(Long userId) {
        assertUserExists(userId);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT ui_theme_mode, ui_density, ui_theme_scheme FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                tenantId(),
                userId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(10003, "用户不存在");
        }
        return new ProfilePreferencesInfo(
                normalizeThemeMode(rows.get(0).get("ui_theme_mode")),
                normalizeDensity(rows.get(0).get("ui_density")),
                normalizeThemeScheme(rows.get(0).get("ui_theme_scheme"))
        );
    }

    public ProfilePreferencesInfo updateMyPreferences(Long userId, UpdateProfilePreferencesRequest request) {
        assertUserExists(userId);
        String themeMode = normalizeThemeMode(request == null ? null : request.getThemeMode());
        String density = normalizeDensity(request == null ? null : request.getDensity());
        String themeScheme = normalizeThemeScheme(request == null ? null : request.getThemeScheme());
        jdbcTemplate.update(
                "UPDATE sys_user SET ui_theme_mode=?, ui_density=?, ui_theme_scheme=?, updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                themeMode,
                density,
                themeScheme,
                safeUserId(),
                tenantId(),
                userId
        );
        return new ProfilePreferencesInfo(themeMode, density, themeScheme);
    }

    public PageResult<UserRoleHistory> listUserRoleHistory(Long userId) {
        assertUserExists(userId);
        List<UserRoleHistory> list = jdbcTemplate.query(
                "SELECT id, user_id, old_role_id, new_role_id, action_type, DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at " +
                        "FROM user_role_history WHERE tenant_id=? AND user_id=? ORDER BY id DESC",
                USER_ROLE_HISTORY_ROW_MAPPER,
                tenantId(),
                userId
        );
        return new PageResult<>(list, list.size(), 1, 20);
    }

    public RoleInfo createRole(CreateRoleRequest request) {
        Integer exists = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM role WHERE tenant_id=? AND role_code=?",
                Integer.class,
                tenantId(),
                request.getRoleCode()
        );
        if (exists != null && exists > 0) {
            throw new BusinessException(10001, "角色编码已存在");
        }
        Long nextId = jdbcTemplate.queryForObject("SELECT IFNULL(MAX(id), 0) + 1 FROM role", Long.class);
        jdbcTemplate.update(
                "INSERT INTO role (id, tenant_id, role_code, role_name, status, is_deleted, created_by, updated_by) VALUES (?, ?, ?, ?, 'ENABLED', 0, 0, 0)",
                nextId,
                tenantId(),
                request.getRoleCode(),
                request.getRoleName()
        );
        return new RoleInfo(nextId, request.getRoleCode(), request.getRoleName(), "ENABLED");
    }

    public RoleInfo updateRole(Long roleId, UpdateRoleRequest request) {
        assertRoleExists(roleId);
        jdbcTemplate.update(
                "UPDATE role SET role_name=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                request.getRoleName(),
                tenantId(),
                roleId
        );
        return jdbcTemplate.queryForObject(
                "SELECT * FROM role WHERE tenant_id=? AND id=?",
                ROLE_ROW_MAPPER,
                tenantId(),
                roleId
        );
    }

    public void enableRole(Long roleId) {
        updateRoleStatus(roleId, "ENABLED");
    }

    public void disableRole(Long roleId) {
        updateRoleStatus(roleId, "DISABLED");
    }

    public void softDeleteRole(Long roleId) {
        assertRoleExists(roleId);
        // 检查是否有活跃用户绑定
        Integer activeBindings = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM user_role ur " +
                        "JOIN sys_user u ON ur.tenant_id=u.tenant_id AND ur.user_id=u.id " +
                        "WHERE ur.tenant_id=? AND ur.role_id=? AND u.is_deleted=0 AND u.status='ENABLED'",
                Integer.class,
                tenantId(),
                roleId
        );
        if (activeBindings != null && activeBindings > 0) {
            throw new BusinessException(10001, "该角色下仍有活跃用户，请先解绑后再删除");
        }
        jdbcTemplate.update(
                "UPDATE role SET is_deleted=1, status='DISABLED', updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                safeUserId(),
                tenantId(),
                roleId
        );
    }

    public PageResult<PermissionInfo> listPermissions() {
        List<PermissionInfo> list = jdbcTemplate.query("SELECT * FROM permission ORDER BY module ASC, id ASC", PERMISSION_ROW_MAPPER);
        return new PageResult<>(list, list.size(), 1, 200);
    }

    public List<Long> getRolePermissionIds(Long roleId) {
        assertRoleExists(roleId);
        return jdbcTemplate.queryForList(
                "SELECT permission_id FROM role_permission WHERE tenant_id=? AND role_id=? ORDER BY permission_id ASC",
                Long.class,
                tenantId(),
                roleId
        );
    }

    public void bindRolePermissions(Long roleId, BindRolePermissionsRequest request) {
        assertRoleExists(roleId);
        jdbcTemplate.update("DELETE FROM role_permission WHERE tenant_id=? AND role_id=?", tenantId(), roleId);
        List<Long> ids = request.getPermissionIds();
        if (ids == null || ids.isEmpty()) {
            return;
        }
        Long nextId = jdbcTemplate.queryForObject("SELECT IFNULL(MAX(id), 0) + 1 FROM role_permission", Long.class);
        long currentId = nextId == null ? 1L : nextId;
        for (Long permissionId : ids) {
            jdbcTemplate.update(
                    "INSERT INTO role_permission (id, tenant_id, role_id, permission_id) VALUES (?, ?, ?, ?)",
                    currentId++,
                    tenantId(),
                    roleId,
                    permissionId
            );
        }
    }

    private void updateRoleStatus(Long roleId, String status) {
        assertRoleExists(roleId);
        jdbcTemplate.update("UPDATE role SET status=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?", status, tenantId(), roleId);
    }

    private void assertUserExists(Long userId) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0",
                Integer.class,
                tenantId(),
                userId
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "用户不存在");
        }
    }

    private void assertRoleExists(Long roleId) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM role WHERE tenant_id=? AND id=? AND is_deleted=0",
                Integer.class,
                tenantId(),
                roleId
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "角色不存在");
        }
    }

    private Long currentRoleId(Long userId) {
        List<Long> roleIds = jdbcTemplate.queryForList(
                "SELECT role_id FROM user_role WHERE tenant_id=? AND user_id=? ORDER BY id DESC LIMIT 1",
                Long.class,
                tenantId(),
                userId
        );
        return roleIds.isEmpty() ? null : roleIds.get(0);
    }

    private void autoAssignExecutorRoleWhenCreatorIsProjectExecutor(Long createdUserId) {
        if (createdUserId == null || !currentUserHasRole("PROJECT_EXECUTOR")) {
            return;
        }
        Long executorRoleId = findRoleIdByCode("EXECUTOR");
        if (executorRoleId == null) {
            return;
        }
        assignRole(createdUserId, executorRoleId);
    }

    private boolean currentUserHasRole(String roleCode) {
        Long userId = AuthContext.userId();
        if (userId == null || roleCode == null || roleCode.trim().isEmpty()) {
            return false;
        }
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM user_role ur " +
                        "JOIN role r ON ur.tenant_id=r.tenant_id AND ur.role_id=r.id " +
                        "WHERE ur.tenant_id=? AND ur.user_id=? AND r.role_code=? AND r.is_deleted=0",
                Integer.class,
                tenantId(),
                userId,
                roleCode
        );
        return count != null && count > 0;
    }

    private Long findRoleIdByCode(String roleCode) {
        List<Long> ids = jdbcTemplate.queryForList(
                "SELECT id FROM role WHERE tenant_id=? AND role_code=? AND is_deleted=0 LIMIT 1",
                Long.class,
                tenantId(),
                roleCode
        );
        return ids.isEmpty() ? null : ids.get(0);
    }

    private void assertPhoneAvailable(String phone, Long excludeUserId) {
        if (phone == null || phone.trim().isEmpty()) {
            throw new BusinessException(10001, "手机号不能为空");
        }
        String normalizedPhone = phone.trim();
        Integer count;
        if (excludeUserId == null) {
            count = jdbcTemplate.queryForObject(
                    "SELECT COUNT(1) FROM sys_user WHERE tenant_id=? AND phone=? AND is_deleted=0",
                    Integer.class,
                    tenantId(),
                    normalizedPhone
            );
        } else {
            count = jdbcTemplate.queryForObject(
                    "SELECT COUNT(1) FROM sys_user WHERE tenant_id=? AND phone=? AND is_deleted=0 AND id<>?",
                    Integer.class,
                    tenantId(),
                    normalizedPhone,
                    excludeUserId
            );
        }
        if (count != null && count > 0) {
            throw new BusinessException(10001, "手机号已存在");
        }
    }

    private Long validateImportUser(ImportUserItemRequest item, Set<String> batchPhones) {
        if (item == null) {
            throw new BusinessException(10001, "导入行不能为空");
        }
        if (item.getUserName() == null || item.getUserName().trim().isEmpty()) {
            throw new BusinessException(10001, "用户名不能为空");
        }
        if (item.getPassword() == null || item.getPassword().trim().isEmpty()) {
            throw new BusinessException(10001, "密码不能为空");
        }
        ImportValidationUtils.validatePhone(item.getPhone());
        ImportValidationUtils.validateRequiredEmail(item.getEmail());
        ImportValidationUtils.validateDateRange(item.getValidFrom(), item.getValidTo());
        String phone = ImportValidationUtils.trim(item.getPhone());
        if (!batchPhones.add(phone)) {
            throw new BusinessException(10001, "批次内手机号重复");
        }
        passwordPolicyService.validate(item.getPassword().trim());
        String roleCode = ImportValidationUtils.trim(item.getRoleCode());
        if (roleCode.isEmpty()) {
            return null;
        }
        Long roleId = findRoleIdByCode(roleCode);
        if (roleId == null) {
            throw new BusinessException(10001, "\u89d2\u8272\u7f16\u7801\u4e0d\u5b58\u5728: " + roleCode);
        }
        return roleId;
    }

    private String buildUserIdentifier(ImportUserItemRequest item) {
        if (item == null) {
            return "";
        }
        String userName = item.getUserName() == null ? "" : item.getUserName().trim();
        String phone = item.getPhone() == null ? "" : item.getPhone().trim();
        if (!userName.isEmpty() && !phone.isEmpty()) {
            return userName + "/" + phone;
        }
        return !userName.isEmpty() ? userName : phone;
    }

    private String resolveImportMessage(Exception ex) {
        if (ex instanceof BusinessException) {
            return ex.getMessage();
        }
        return ex.getMessage() == null || ex.getMessage().trim().isEmpty()
                ? "导入失败"
                : ex.getMessage();
    }

    private void sendUserCreatedMail(Long userId, String userName, String phone, String email, String validFrom, String validTo) {
        if (userId == null || userId <= 0) {
            return;
        }
        try {
            List<Map<String, Object>> tenants = jdbcTemplate.queryForList(
                    "SELECT tenant_code, tenant_name FROM tenant WHERE id=? LIMIT 1",
                    tenantId()
            );
            String tenantCode = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_code"));
            String tenantName = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_name"));
            String loginPath = "/" + tenantCode + "/login";
            Map<String, Object> variables = new LinkedHashMap<String, Object>();
            variables.put("userId", userId);
            variables.put("targetUserId", userId);
            variables.put("userName", userName);
            variables.put("phone", phone);
            variables.put("email", email);
            variables.put("validFrom", validFrom);
            variables.put("validTo", validTo);
            variables.put("tenantCode", tenantCode);
            variables.put("tenantName", tenantName);
            variables.put("loginPath", loginPath);
            DispatchNotificationRequest request = new DispatchNotificationRequest();
            request.setIdempotencyKey("user-created-" + tenantId() + "-" + userId);
            request.setEventCode("USER_CREATED");
            request.setBizType("USER");
            request.setBizId("user-" + userId);
            request.setVariablesJson(objectMapper.writeValueAsString(variables));
            notificationDispatchService.dispatch(request);
        } catch (Exception ex) {
            log.warn("auto trigger user created notification failed, tenantId={}, userId={}, err={}",
                    tenantId(), userId, ex.getMessage(), ex);
        }
    }

    private Long tenantId() {
        return AuthContext.requireTenantId();
    }

    private void upgradeStoredPasswordHash(Long userId, Long tenantId, String rawPassword) {
        List<String> hashes = jdbcTemplate.queryForList(
                "SELECT password_hash FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                String.class,
                tenantId,
                userId
        );
        if (hashes.isEmpty()) {
            return;
        }
        String currentHash = hashes.get(0);
        if (passwordCodecService.isEncoded(currentHash) || !passwordCodecService.matches(rawPassword, currentHash)) {
            return;
        }
        jdbcTemplate.update(
                "UPDATE sys_user SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                passwordCodecService.encode(rawPassword),
                tenantId,
                userId
        );
    }

    private Long safeUserId() {
        Long userId = AuthContext.userId();
        return userId == null ? 0L : userId;
    }

    private Map<Long, Long> listUserCreators(List<Long> userIds) {
        Map<Long, Long> result = new LinkedHashMap<>();
        if (userIds == null || userIds.isEmpty()) {
            return result;
        }
        String placeholders = userIds.stream().map(i -> "?").collect(Collectors.joining(","));
        List<Object> args = new ArrayList<>();
        args.add(tenantId());
        args.addAll(userIds);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT id, created_by FROM sys_user WHERE tenant_id=? AND is_deleted=0 AND id IN (" + placeholders + ")",
                args.toArray()
        );
        for (Map<String, Object> row : rows) {
            result.put(((Number) row.get("id")).longValue(), ((Number) row.get("created_by")).longValue());
        }
        return result;
    }

    private Timestamp parseTimestamp(String raw) {
        if (raw == null || raw.trim().isEmpty()) {
            return null;
        }
        String normalized = normalizeDateTimeString(raw);
        return Timestamp.valueOf(LocalDateTime.parse(normalized, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }

    private String normalizeDateTimeString(String raw) {
        String value = raw.trim().replace("T", " ");
        if (value.length() == 16) {
            return value + ":00";
        }
        return value;
    }

    private String loadTenantSwitchAccountKey(Long userId) {
        List<String> keys = jdbcTemplate.queryForList(
                "SELECT tenant_switch_account_key FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                String.class,
                tenantId(),
                userId
        );
        return keys.isEmpty() ? "" : String.valueOf(keys.get(0) == null ? "" : keys.get(0)).trim();
    }

    private String resolveTenantSwitchAccountKeyByPhone(String phone, Long excludeUserId) {
        String normalizedPhone = phone == null ? "" : phone.trim();
        if (normalizedPhone.isEmpty()) {
            return "acct_" + UUID.randomUUID().toString().replace("-", "").toUpperCase();
        }
        String sql =
                "SELECT tenant_switch_account_key FROM sys_user " +
                "WHERE phone=? AND is_deleted=0 AND tenant_switch_account_key IS NOT NULL AND tenant_switch_account_key<>'' " +
                (excludeUserId == null ? "" : "AND id<>? ") +
                "ORDER BY id ASC LIMIT 1";
        List<String> keys = excludeUserId == null
                ? jdbcTemplate.queryForList(sql, String.class, normalizedPhone)
                : jdbcTemplate.queryForList(sql, String.class, normalizedPhone, excludeUserId);
        if (!keys.isEmpty()) {
            String existing = String.valueOf(keys.get(0) == null ? "" : keys.get(0)).trim();
            if (!existing.isEmpty()) {
                return existing;
            }
        }
        return "acct_" + UUID.randomUUID().toString().replace("-", "").toUpperCase();
    }

    private void ensureTenantSwitchAccountKeyForPassword(String phone, String rawPassword) {
        String normalizedPhone = phone == null ? "" : phone.trim();
        if (normalizedPhone.isEmpty() || rawPassword == null || rawPassword.isEmpty()) {
            return;
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT id, password_hash, tenant_switch_account_key FROM sys_user WHERE phone=? AND is_deleted=0",
                normalizedPhone
        );
        if (rows.isEmpty()) {
            return;
        }
        List<Long> matchedUserIds = new ArrayList<Long>();
        String sharedKey = "";
        for (Map<String, Object> row : rows) {
            String storedPassword = row.get("password_hash") == null ? null : String.valueOf(row.get("password_hash"));
            if (!passwordCodecService.matches(rawPassword, storedPassword)) {
                continue;
            }
            matchedUserIds.add(((Number) row.get("id")).longValue());
            String existingKey = row.get("tenant_switch_account_key") == null ? "" : String.valueOf(row.get("tenant_switch_account_key")).trim();
            if (sharedKey.isEmpty() && !existingKey.isEmpty()) {
                sharedKey = existingKey;
            }
        }
        if (matchedUserIds.isEmpty()) {
            return;
        }
        if (sharedKey.isEmpty()) {
            sharedKey = "acct_" + UUID.randomUUID().toString().replace("-", "").toUpperCase();
        }
        for (Long matchedUserId : matchedUserIds) {
            jdbcTemplate.update(
                    "UPDATE sys_user SET tenant_switch_account_key=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND (tenant_switch_account_key IS NULL OR tenant_switch_account_key<>?)",
                    sharedKey,
                    matchedUserId,
                    sharedKey
            );
        }
    }

    public 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";
    }

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

    public 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";
    }
}

