"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const user_entity_1 = require("../entities/user.entity");
const event_emitter_1 = require("@nestjs/event-emitter");
const auth_constants_1 = require("../../auth.constants");
const user_updated_event_1 = require("../events/user-updated.event");
const user_deleted_event_1 = require("../events/user-deleted.event");
const user_created_event_1 = require("../events/user-created.event");
const tenant_1 = require("../../tenant");
const debug_logger_service_1 = require("../../core/services/debug-logger.service");
const auth_config_service_1 = require("../../core/services/auth-config.service");
const user_access_entity_1 = require("../entities/user-access.entity");
const role_entity_1 = require("../../role/entities/role.entity");
const nest_auth_contracts_1 = require("@ackplus/nest-auth-contracts");
const utils_1 = require("../../utils");
let UserService = class UserService {
    userRepository;
    userAccessRepository;
    dataSource;
    tenantService;
    eventEmitter;
    authConfigService;
    debugLogger;
    constructor(userRepository, userAccessRepository, dataSource, tenantService, eventEmitter, authConfigService, debugLogger) {
        this.userRepository = userRepository;
        this.userAccessRepository = userAccessRepository;
        this.dataSource = dataSource;
        this.tenantService = tenantService;
        this.eventEmitter = eventEmitter;
        this.authConfigService = authConfigService;
        this.debugLogger = debugLogger;
    }
    async runInTransaction(fn) {
        return this.dataSource.transaction(fn);
    }
    getUserRepo(manager) {
        return manager ? manager.getRepository(user_entity_1.NestAuthUser) : this.userRepository;
    }
    getUserAccessRepo(manager) {
        return manager ? manager.getRepository(user_access_entity_1.NestAuthUserAccess) : this.userAccessRepository;
    }
    async createUser(data, tenantId, context, manager) {
        if (manager) {
            return this.createUserCore(data, tenantId, context, manager);
        }
        const created = await this.runInTransaction((m) => this.createUserCore(data, tenantId, context, m));
        await this.eventEmitter.emitAsync(auth_constants_1.NestAuthEvents.USER_CREATED, new user_created_event_1.UserCreatedEvent({
            user: created,
            input: context,
            tenantId,
        }));
        return created;
    }
    async createUserCore(data, tenantId, context, manager) {
        const config = this.authConfigService.getConfig();
        const userRepo = this.getUserRepo(manager);
        try {
            const email = (0, utils_1.normalizedEmail)(data.email);
            const phone = (0, utils_1.normalizedPhone)(data.phone);
            await this.tenantService.resolveTenantId(tenantId);
            if (email) {
                const existingUser = await this.getUserByEmail(email, tenantId, undefined, manager);
                if (existingUser) {
                    throw new common_1.ConflictException({
                        message: 'User with this email already exists',
                        code: 'USER_ALREADY_EXISTS'
                    });
                }
            }
            if (phone) {
                const existingUser = await this.getUserByPhone(phone, tenantId, undefined, manager);
                if (existingUser) {
                    throw new common_1.ConflictException({
                        message: 'User with this phone number already exists',
                        code: 'USER_ALREADY_EXISTS'
                    });
                }
            }
            if (config.user?.beforeCreate) {
                this.debugLogger.debug('Applying user.beforeCreate hook', 'UserService');
                data = await config.user.beforeCreate?.(data, context) ?? data;
            }
            const user = userRepo.create({
                ...data,
                ...(email != null && { email: email }),
                ...(phone != null && { phone: phone }),
            });
            if (data.password) {
                await user.setPassword(data.password);
            }
            await userRepo.save(user);
            await this.ensureUserAccess(user.id, tenantId, manager);
            this.debugLogger.info('User created successfully', 'UserService', { userId: user.id });
            if (email && config.emailAuth?.enabled !== false) {
                await user.findOrCreateIdentity(auth_constants_1.EMAIL_AUTH_PROVIDER, email, manager);
            }
            if (phone && config.phoneAuth?.enabled === true) {
                await user.findOrCreateIdentity(auth_constants_1.PHONE_AUTH_PROVIDER, phone, manager);
            }
            if (config.user?.afterCreate) {
                await config.user.afterCreate?.(user, context, manager);
            }
            this.debugLogger.logFunctionExit('createUser', 'UserService', { userId: user.id });
            return user;
        }
        catch (error) {
            this.debugLogger.logError(error, 'createUser', { email: data.email, phone: data.phone });
            throw error;
        }
    }
    async getUserById(id, options, manager) {
        if (!id) {
            return null;
        }
        const user = await this.getUserRepo(manager).findOne({
            ...(options ? options : {}),
            where: { id }
        });
        if (!user) {
            this.debugLogger.warn('User not found', 'UserService', { userId: id });
            return null;
        }
        return user;
    }
    async getUserByEmail(email, tenantId, options, manager) {
        this.debugLogger.debug('Getting user by email', 'UserService', { email: !!email, tenantId });
        const emailNorm = (0, utils_1.normalizedEmail)(email);
        if (!emailNorm) {
            this.debugLogger.warn('No email provided for user lookup', 'UserService');
            return null;
        }
        const tenantRequired = await this.tenantService.checkRequiredTenant(tenantId);
        const user = await this.getUserRepo(manager).findOne({
            ...(options ? options : {}),
            relations: ['userAccesses', ...(Array.isArray(options?.relations) ? options.relations : [])],
            where: {
                email: emailNorm,
                ...(tenantRequired ? { userAccesses: { tenantId: tenantId } } : {}),
            },
        });
        return user;
    }
    async getUserByPhone(phone, tenantId, options, manager) {
        this.debugLogger.debug('Getting user by phone', 'UserService', { phone: !!phone, tenantId });
        const phoneNorm = (0, utils_1.normalizedPhone)(phone);
        if (!phoneNorm) {
            this.debugLogger.warn('No phone provided for user lookup', 'UserService');
            return null;
        }
        const tenantRequired = await this.tenantService.checkRequiredTenant(tenantId);
        const user = await this.getUserRepo(manager).findOne({
            ...(options ? options : {}),
            relations: ['userAccesses', ...(Array.isArray(options?.relations) ? options.relations : [])],
            where: {
                phone: phoneNorm,
                ...(tenantRequired ? { userAccesses: { tenantId: tenantId } } : {}),
            },
        });
        return user;
    }
    async getUsers(options, manager) {
        return this.getUserRepo(manager).find(options);
    }
    async updateUser(id, data, manager) {
        this.debugLogger.logFunctionEntry('updateUser', 'UserService', { userId: id, fields: Object.keys(data) });
        try {
            const user = await this.getUserById(id, undefined, manager);
            if (!user) {
                throw new common_1.NotFoundException({
                    message: `User with ID ${id} not found`,
                    code: 'USER_NOT_FOUND'
                });
            }
            if (data.email || data.phone) {
                const config = this.authConfigService.getConfig();
                const userWithMemberships = await this.getUserById(id, {
                    relations: ['userAccesses'],
                }, manager);
                let tenantId = null;
                if (config.tenant?.mode === nest_auth_contracts_1.TenantModeEnum.ISOLATED) {
                    tenantId = userWithMemberships?.userAccesses?.[0]?.tenantId;
                }
                let existingUser = null;
                const phone = (0, utils_1.normalizedPhone)(data.phone);
                const email = (0, utils_1.normalizedEmail)(data.email);
                if (data.email != null)
                    data.email = email;
                if (data.phone != null)
                    data.phone = phone;
                if (phone != null) {
                    const userByPhone = await this.getUserByPhone(phone, tenantId, { select: ['id'] }, manager);
                    if (userByPhone && userByPhone.id !== id) {
                        existingUser = userByPhone;
                    }
                }
                if (!existingUser && email != null) {
                    const userByEmail = await this.getUserByEmail(email, tenantId, { select: ['id'] }, manager);
                    if (userByEmail && userByEmail.id !== id) {
                        existingUser = userByEmail;
                    }
                }
                if (existingUser) {
                    throw new common_1.ConflictException({
                        message: `User with ${data.email ? `email ${data.email}` : ''}${data.email && data.phone ? ' or ' : ''}${data.phone ? `phone ${data.phone}` : ''} already exists.`,
                        code: auth_constants_1.ERROR_CODES.USER_ALREADY_EXISTS,
                    });
                }
            }
            this.debugLogger.debug('Updating user data', 'UserService', { userId: id, fields: Object.keys(data) });
            const updateConfig = this.authConfigService.getConfig();
            const applyUpdate = async (m) => {
                if (updateConfig.user?.beforeUpdate) {
                    const modified = await updateConfig.user.beforeUpdate(user, data, m);
                    if (modified)
                        data = { ...data, ...modified };
                }
                Object.assign(user, data);
                const saved = await this.getUserRepo(m).save(user);
                if (data.email && updateConfig.emailAuth?.enabled !== false) {
                    this.debugLogger.debug('Updating email identity', 'UserService', { userId: id });
                    await user.updateOrCreateIdentity(auth_constants_1.EMAIL_AUTH_PROVIDER, { providerId: data.email }, m);
                }
                if (data.phone && updateConfig.phoneAuth?.enabled === true) {
                    this.debugLogger.debug('Updating phone identity', 'UserService', { userId: id });
                    await user.updateOrCreateIdentity(auth_constants_1.PHONE_AUTH_PROVIDER, { providerId: data.phone }, m);
                }
                if (updateConfig.user?.afterUpdate) {
                    await updateConfig.user.afterUpdate(saved, data, m);
                }
                return saved;
            };
            const updatedUser = manager
                ? await applyUpdate(manager)
                : await this.runInTransaction(applyUpdate);
            this.debugLogger.debug('Emitting user updated event', 'UserService', { userId: updatedUser.id });
            await this.eventEmitter.emitAsync(auth_constants_1.NestAuthEvents.USER_UPDATED, new user_updated_event_1.UserUpdatedEvent({
                user: updatedUser,
                updatedFields: Object.keys(data)
            }));
            this.debugLogger.logFunctionExit('updateUser', 'UserService', { userId: updatedUser.id });
            return updatedUser;
        }
        catch (error) {
            throw error;
        }
    }
    async ensureUserAccess(userId, tenantId, manager) {
        if (!userId) {
            throw new common_1.BadRequestException({
                message: 'User ID is required',
                code: 'USER_ID_REQUIRED'
            });
        }
        const repo = this.getUserAccessRepo(manager);
        const existing = await repo.findOne({
            where: { userId, tenantId: tenantId || (0, typeorm_2.IsNull)() }
        });
        if (existing) {
            return existing;
        }
        const access = repo.create({
            userId,
            ...tenantId ? { tenantId } : {},
        });
        return await repo.save(access);
    }
    async isUserInTenant(userId, tenantId, manager) {
        if (!userId || !tenantId) {
            return false;
        }
        const access = await this.getUserRepo(manager).createQueryBuilder('u')
            .innerJoin('u.userAccesses', 'm', 'm.tenantId = :tenantId', {
            tenantId,
        })
            .where('u.id = :userId', { userId })
            .select('m.id')
            .getRawOne();
        return !!access;
    }
    async setUserAccessRoles(userId, tenantId, roleIds, manager) {
        const accessRepo = this.getUserAccessRepo(manager);
        const roleRepo = manager ? manager.getRepository(role_entity_1.NestAuthRole) : role_entity_1.NestAuthRole.getRepository();
        let access = await accessRepo.findOne({
            where: { userId, tenantId: tenantId || (0, typeorm_2.IsNull)() },
            relations: ['roles'],
        });
        if (!access) {
            access = await this.ensureUserAccess(userId, tenantId, manager);
        }
        if (!roleIds?.length) {
            access.roles = [];
        }
        else {
            const roleEntities = await roleRepo.find({ where: { id: (0, typeorm_2.In)(roleIds) } });
            access.roles = roleEntities;
        }
        return accessRepo.save(access);
    }
    async getUserTenants(userId, manager) {
        if (!userId) {
            return [];
        }
        const accessList = await this.getUserAccessRepo(manager).find({
            where: { userId, isActive: true },
            relations: ['tenant']
        });
        return accessList
            .map(a => a.tenant)
            .filter(Boolean);
    }
    async deleteUserAccess(userId, tenantId, manager) {
        if (!userId || !tenantId) {
            return;
        }
        const repo = this.getUserAccessRepo(manager);
        const access = await repo.findOne({
            where: { userId, tenantId },
        });
        if (access) {
            await repo.remove(access);
            this.debugLogger.debug('User access deleted', 'UserService', { userId, tenantId });
        }
    }
    async deleteUser(id, manager) {
        this.debugLogger.logFunctionEntry('deleteUser', 'UserService', { userId: id });
        try {
            const user = await this.getUserById(id, undefined, manager);
            if (!user) {
                this.debugLogger.error('User not found for deletion', 'UserService', { userId: id });
                throw new common_1.NotFoundException({
                    message: `User with ID ${id} not found`,
                    code: 'USER_NOT_FOUND'
                });
            }
            const config = this.authConfigService.getConfig();
            const snapshot = Object.assign(Object.create(Object.getPrototypeOf(user)), user);
            const doDelete = async (m) => {
                if (config.user?.beforeDelete) {
                    await config.user.beforeDelete(user, m);
                }
                this.debugLogger.debug('Deleting user from database', 'UserService', { userId: id });
                await this.getUserRepo(m).remove(user);
                if (config.user?.afterDelete) {
                    await config.user.afterDelete(snapshot, m);
                }
            };
            if (manager) {
                await doDelete(manager);
            }
            else {
                await this.runInTransaction(doDelete);
            }
            this.debugLogger.info('User deleted successfully', 'UserService', { userId: id });
            this.debugLogger.debug('Emitting user deleted event', 'UserService', { userId: id });
            await this.eventEmitter.emitAsync(auth_constants_1.NestAuthEvents.USER_DELETED, new user_deleted_event_1.UserDeletedEvent({
                user: snapshot,
            }));
            this.debugLogger.logFunctionExit('deleteUser', 'UserService', { userId: id });
        }
        catch (error) {
            this.debugLogger.logError(error, 'deleteUser', { userId: id });
            throw error;
        }
    }
    async verifyUser(id, verificationType, manager) {
        const user = await this.getUserById(id, undefined, manager);
        if (!user) {
            throw new common_1.NotFoundException({
                message: `User with ID ${id} not found`,
                code: 'USER_NOT_FOUND'
            });
        }
        if (verificationType === 'email') {
            user.emailVerifiedAt = new Date();
        }
        else if (verificationType === 'phone') {
            user.phoneVerifiedAt = new Date();
        }
        return this.getUserRepo(manager).save(user);
    }
    async unverifyUser(id, verificationType, manager) {
        const user = await this.getUserById(id, undefined, manager);
        if (!user) {
            throw new common_1.NotFoundException({
                message: `User with ID ${id} not found`,
                code: 'USER_NOT_FOUND'
            });
        }
        if (verificationType === 'email') {
            user.emailVerifiedAt = null;
        }
        else if (verificationType === 'phone') {
            user.phoneVerifiedAt = null;
        }
        return this.getUserRepo(manager).save(user);
    }
    async updateUserStatus(id, isActive, manager) {
        const user = await this.getUserById(id, undefined, manager);
        if (!user) {
            throw new common_1.NotFoundException({
                message: `User with ID ${id} not found`,
                code: 'USER_NOT_FOUND'
            });
        }
        user.isActive = isActive;
        const updatedUser = await this.getUserRepo(manager).save(user);
        this.debugLogger.debug('Emitting user updated event (status change)', 'UserService', { userId: id, isActive });
        await this.eventEmitter.emitAsync(auth_constants_1.NestAuthEvents.USER_UPDATED, new user_updated_event_1.UserUpdatedEvent({
            user: updatedUser,
            updatedFields: ['isActive']
        }));
        return updatedUser;
    }
    async updateUserMetadata(id, metadata, manager) {
        const user = await this.getUserById(id, undefined, manager);
        if (!user) {
            throw new common_1.NotFoundException({
                message: `User with ID ${id} not found`,
                code: 'USER_NOT_FOUND'
            });
        }
        user.metadata = {
            ...user.metadata,
            ...metadata
        };
        const updatedUser = await this.getUserRepo(manager).save(user);
        this.debugLogger.debug('Emitting user updated event (metadata)', 'UserService', { userId: id });
        await this.eventEmitter.emitAsync(auth_constants_1.NestAuthEvents.USER_UPDATED, new user_updated_event_1.UserUpdatedEvent({
            user: updatedUser,
            updatedFields: ['metadata']
        }));
        return updatedUser;
    }
    async countUsers(options, manager) {
        return this.getUserRepo(manager).count(options);
    }
    async getUsersAndCount(options, manager) {
        return this.getUserRepo(manager).findAndCount(options);
    }
    async getUsersByRole(roleName, guard, tenantId, manager) {
        this.debugLogger.debug('Getting users by role', 'UserService', { roleName, guard });
        const usersQuery = this.getUserRepo(manager)
            .createQueryBuilder('user')
            .innerJoin('user.userAccesses', 'm')
            .innerJoin('m.roles', 'role', 'role.name = :roleName AND role.guard = :guard', {
            roleName,
            guard,
        });
        if (tenantId) {
            usersQuery.andWhere('m.tenantId = :tenantId', { tenantId });
        }
        const users = await usersQuery.getMany();
        this.debugLogger.debug('Found users with role', 'UserService', { roleName, count: users.length });
        return users;
    }
};
exports.UserService = UserService;
exports.UserService = UserService = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, typeorm_1.InjectRepository)(user_entity_1.NestAuthUser)),
    __param(1, (0, typeorm_1.InjectRepository)(user_access_entity_1.NestAuthUserAccess)),
    __param(2, (0, typeorm_1.InjectDataSource)()),
    __metadata("design:paramtypes", [typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.DataSource,
        tenant_1.TenantService,
        event_emitter_1.EventEmitter2,
        auth_config_service_1.AuthConfigService,
        debug_logger_service_1.DebugLoggerService])
], UserService);
//# sourceMappingURL=user.service.js.map