package com.ffii.fpsms.modules.user.service;

import java.io.UnsupportedEncodingException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import com.ffii.fpsms.modules.user.entity.projections.UserCombo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.ffii.core.exception.NotFoundException;
import com.ffii.core.exception.UnprocessableEntityException;
import com.ffii.core.support.AbstractBaseEntityService;
import com.ffii.core.support.JdbcDao;
import com.ffii.core.utils.Params;
import com.ffii.core.utils.PasswordUtils;
import com.ffii.fpsms.modules.common.ErrorCodes;
import com.ffii.fpsms.modules.common.PasswordRule;
import com.ffii.fpsms.modules.settings.service.SettingsService;
import com.ffii.fpsms.modules.user.entity.User;
import com.ffii.fpsms.modules.user.entity.UserRepository;
import com.ffii.fpsms.modules.user.req.NewPublicUserReq;
import com.ffii.fpsms.modules.user.req.NewUserReq;
import com.ffii.fpsms.modules.user.req.SearchUserReq;
import com.ffii.fpsms.modules.user.req.UpdateUserReq;
import com.ffii.fpsms.modules.user.service.pojo.UserRecord;

@Service
public class UserService extends AbstractBaseEntityService<User, Long, UserRepository> {
    private static final String USER_AUTH_SQL = "SELECT a.authority"
            + " FROM `user` u"
            + " JOIN user_authority ua ON ua.userId = u.id"
            + " JOIN authority a ON a.id = ua.authId"
            + " WHERE u.deleted = 0"
            + " AND u.id = :userId";
    private static final String UNION_SQL = " UNION ";
    private static final String GROUP_AUTH_SQL = "SELECT a.authority"
            + " FROM `user` u"
            + " JOIN user_group ug ON ug.userId = u.id"
            + " JOIN `group` g ON g.deleted = 0 AND g.id = ug.groupId"
            + " JOIN group_authority ga ON ga.groupId = g.id"
            + " JOIN authority a ON a.id = ga.authId"
            + " WHERE u.deleted = 0"
            + " AND u.id = :userId";

    @Autowired
    private SettingsService settingsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    UserRepository userRepository;

    public UserService(JdbcDao jdbcDao, UserRepository userRepository) {
        super(jdbcDao, userRepository);
    }

    public Optional<User> loadUserOptByUsername(String username) {
        return findByUsername(username)
                .map(user -> {
                    Set<GrantedAuthority> auths = new LinkedHashSet<GrantedAuthority>();
                    auths.add(new SimpleGrantedAuthority("ROLE_USER"));
                    jdbcDao.queryForList(USER_AUTH_SQL + UNION_SQL + GROUP_AUTH_SQL, Map.of("userId", user.getId()))
                            .forEach(item -> auths.add(new SimpleGrantedAuthority((String) item.get("authority"))));

                    user.setAuthorities(auths);
                    return user;
                });
    }

    public User getUserById(Long id) {
        User user = userRepository.findById(id).orElseThrow();
        Set<GrantedAuthority> auths = new LinkedHashSet<GrantedAuthority>();
        auths.add(new SimpleGrantedAuthority("ROLE_USER"));
        jdbcDao.queryForList(USER_AUTH_SQL + UNION_SQL + GROUP_AUTH_SQL, Map.of("userId", user.getId()))
                .forEach(item -> auths.add(new SimpleGrantedAuthority((String) item.get("authority"))));
        user.setAuthorities(auths);
        return user;
    }
    public Optional<User> findByUsername(String username) {
        return userRepository.findByUsernameAndDeletedFalse(username);
    }

    // @Transactional(rollbackFor = Exception.class)
    public List<UserRecord> search(SearchUserReq req) {
        StringBuilder sql = new StringBuilder("SELECT"
                + " u.id,"
                + " u.created,"
                + " u.createdBy,"
                + " u.version,"
                + " u.modified,"
                + " u.modifiedBy,"
                + " u.username,"
                + " u.locked,"
                + " u.name,"
                + " u.locale,"
                + " u.firstname,"
                + " u.lastname,"
                + " u.title,"
                + " u.department,"
                + " u.email,"
                + " u.phone1,"
                + " u.phone2,"
                + " u.remarks,"
                + " u.staffNo"
                + " FROM `user` u"
                + " left join user_group ug on u.id = ug.userId"
                + " where u.deleted = false");

        if (req != null) {
            if (req.getId() != null)
                sql.append(" AND u.id = :id");

            if (req.getGroupId() != null)
                sql.append(" AND ug.groupId = :groupId");
            if (StringUtils.isNotBlank(req.getUsername())) {
                req.setUsername("%" + req.getUsername() + "%");
                sql.append(" AND u.username LIKE :username");
            }
            if (StringUtils.isNotBlank(req.getName())) {
                req.setName("%" + req.getName() + "%");
                sql.append(" AND u.name LIKE :name");
            }
            if (req.getLocked() != null) {
                sql.append(" AND u.locked = :locked");
            }
        }
        sql.append(" ORDER BY u.name");

        if (req != null) {
            if (req.getStart() != null && req.getLimit() != null)
                sql.append(" LIMIT :start, :limit");
        }

        return jdbcDao.queryForList(sql.toString(), req, UserRecord.class);
    }

    public List<Integer> listUserAuthId(long id) {
        return jdbcDao.queryForInts(
                "SELECT"
                        + " ua.authId"
                        + " FROM user_authority ua"
                        + " WHERE ua.userId = :id",
                Map.of(Params.ID, id));
    }

    public List<Integer> listUserGroupId(long id) {
        return jdbcDao.queryForInts(
                "SELECT"
                        + " gu.groupId"
                        + " FROM user_group gu"
                        + " INNER JOIN `group` g ON g.deleted = FALSE AND g.id = gu.groupId"
                        + " WHERE gu.userId = :id",
                Map.of(Params.ID, id));
    }

    private User saveOrUpdate(User instance, UpdateUserReq req) {

        if (instance.getId() == null){
            req.setLocked(false);
        }
        // Removed: req.setName(instance.getName()); - This was overwriting the new name with the old one
        BeanUtils.copyProperties(req,instance);
        instance = save(instance);

        long id = instance.getId();

        List<Map<String, Integer>> authBatchInsertValues = req.getAddAuthIds().stream()
                .map(authId -> Map.of("userId", (int)id, "authId", authId))
                .collect(Collectors.toList());
        List<Map<String, Integer>> authBatchDeleteValues = req.getRemoveAuthIds().stream()
                .map(authId -> Map.of("userId", (int)id, "authId", authId))
                .collect(Collectors.toList());
        if (!authBatchDeleteValues.isEmpty()) {
            jdbcDao.batchUpdate(
                    "DELETE FROM user_authority"
                            + " WHERE userId = :userId "
                            + " AND authId = :authId",
                    authBatchDeleteValues);
        }
        if (!authBatchInsertValues.isEmpty()) {
            jdbcDao.batchUpdate(
                    "INSERT IGNORE INTO user_authority (userId, authId)"
                            + " VALUES (:userId, :authId)",
                    authBatchInsertValues);
        }

        return instance;
    }

    @Transactional(rollbackFor = Exception.class)
    public User newRecord(NewUserReq req) throws UnsupportedEncodingException {
        if (findByUsername(req.getUsername()).isPresent()) {
            throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE);
        }

        System.out.println("Start Save");

        String password = req.getPassword();
        String pwdHash = passwordEncoder.encode(password);

        User instance = new User();
        instance.setPassword(pwdHash);
        instance.setUsername(req.getUsername());
        instance.setName(req.getName());
        instance.setStaffNo(req.getStaffNo());
        instance = save(instance);

        long id = instance.getId();

        List<Map<String, Integer>> authBatchInsertValues = req.getAddAuthIds().stream()
                .map(authId -> Map.of("userId", (int)id, "authId", authId))
                .collect(Collectors.toList());
        List<Map<String, Integer>> authBatchDeleteValues = req.getRemoveAuthIds().stream()
                .map(authId -> Map.of("userId", (int)id, "authId", authId))
                .collect(Collectors.toList());
        if (!authBatchDeleteValues.isEmpty()) {
            jdbcDao.batchUpdate(
                    "DELETE FROM user_authority"
                            + " WHERE userId = :userId "
                            + " AND authId = :authId",
                    authBatchDeleteValues);
        }
        if (!authBatchInsertValues.isEmpty()) {
            jdbcDao.batchUpdate(
                    "INSERT IGNORE INTO user_authority (userId, authId)"
                            + " VALUES (:userId, :authId)",
                    authBatchInsertValues);
        }

        return instance;
    }

    @Transactional(rollbackFor = Exception.class)
    public User newPublicUserRecord(NewPublicUserReq req) throws UnsupportedEncodingException {
        if (findByUsername(req.getUsername()).isPresent()) {
            throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE);
        }

        String submitedPassword = req.getPassword();
        String pwdHash = passwordEncoder.encode(submitedPassword);
        req.setPassword(pwdHash);
        User instance = new User();

        instance = saveOrUpdate(instance, req);
        return instance;
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateRecord(long id, UpdateUserReq req) {
        saveOrUpdate(
                find(id).orElseThrow(NotFoundException::new),
                req);
    }

    @Transactional(rollbackFor = Exception.class)
    public String resetPassword(long id) throws UnsupportedEncodingException {
        User instance = find(id).orElseThrow(NotFoundException::new);
        String randomPassword = PasswordUtils.genPwd(new PasswordRule(settingsService));

        instance.setPassword(passwordEncoder.encode(randomPassword));
        instance = save(instance);
        return randomPassword;
    }

    public List<UserCombo> getEscalationCombo() {
        return userRepository.findUserComboByTitleNotNullAndDepartmentNotNullAndNameNotNullAndDeletedFalse();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void markDelete(User entity) {
	    Assert.notNull(entity, "entity must not be null");
	    // Use direct SQL update to bypass JPA validation during soft delete
	    // This prevents ConstraintViolationException when password field has @NotBlank constraint
	    jdbcDao.executeUpdate(
		"UPDATE `user` SET deleted = 1 WHERE id = :id",
		    Map.of("id", entity.getId())
	    );
    }
}