"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); }
};
var GroupsService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GroupsService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const typeorm_1 = require("@nestjs/typeorm");
const crypto = require("crypto");
const typeorm_2 = require("typeorm");
const group_entity_1 = require("../../database/entities/group.entity");
const group_member_entity_1 = require("../../database/entities/group-member.entity");
const group_invite_entity_1 = require("../../database/entities/group-invite.entity");
const user_entity_1 = require("../../database/entities/user.entity");
const expense_split_entity_1 = require("../../database/entities/expense-split.entity");
const settlement_entity_1 = require("../../database/entities/settlement.entity");
const enums_1 = require("../../common/enums");
const activity_service_1 = require("../activity/activity.service");
const notifications_service_1 = require("../notifications/notifications.service");
const mail_service_1 = require("../mail/mail.service");
let GroupsService = GroupsService_1 = class GroupsService {
    constructor(groupRepo, groupMemberRepo, groupInviteRepo, userRepo, expenseSplitRepo, settlementRepo, activityService, notificationsService, mailService, config) {
        this.groupRepo = groupRepo;
        this.groupMemberRepo = groupMemberRepo;
        this.groupInviteRepo = groupInviteRepo;
        this.userRepo = userRepo;
        this.expenseSplitRepo = expenseSplitRepo;
        this.settlementRepo = settlementRepo;
        this.activityService = activityService;
        this.notificationsService = notificationsService;
        this.mailService = mailService;
        this.config = config;
        this.logger = new common_1.Logger(GroupsService_1.name);
    }
    async listMyGroups(userId) {
        const memberships = await this.groupMemberRepo.find({
            where: { userId, leftAt: (0, typeorm_2.IsNull)(), group: { isActive: true } },
            relations: { group: true },
            order: { group: { updatedAt: 'DESC' } },
        });
        const results = [];
        for (const { group } of memberships) {
            if (!group)
                continue;
            const memberCount = await this.countActiveMembers(group.id);
            results.push(this.toGroupDto(group, memberCount));
        }
        return results;
    }
    async createGroup(userId, dto) {
        const group = this.groupRepo.create({
            name: dto.name,
            description: dto.description,
            currency: dto.currency ?? 'USD',
            imageUrl: dto.imageUrl,
            createdById: userId,
        });
        await this.groupRepo.save(group);
        const member = this.groupMemberRepo.create({
            groupId: group.id,
            userId,
            role: enums_1.GroupRole.Admin,
        });
        await this.groupMemberRepo.save(member);
        return this.getGroup(group.id);
    }
    async getGroup(groupId) {
        const group = await this.groupRepo.findOne({
            where: { id: groupId, isActive: true },
            relations: { members: { user: true } },
        });
        if (!group)
            throw new common_1.NotFoundException('Group not found');
        const activeMembers = (group.members ?? [])
            .filter((m) => m.leftAt === null)
            .sort((a, b) => a.joinedAt.getTime() - b.joinedAt.getTime());
        const memberCount = activeMembers.length;
        return this.toGroupDetailDto({
            ...group,
            members: activeMembers.map((m) => ({ ...m, user: m.user })),
        }, memberCount);
    }
    async updateGroup(groupId, dto) {
        await this.groupRepo.update({ id: groupId }, {
            ...(dto.name !== undefined && { name: dto.name }),
            ...(dto.description !== undefined && { description: dto.description }),
            ...(dto.currency !== undefined && { currency: dto.currency }),
            ...(dto.imageUrl !== undefined && { imageUrl: dto.imageUrl }),
        });
        return this.getGroup(groupId);
    }
    async deleteGroup(groupId) {
        const unsettled = await this.expenseSplitRepo
            .createQueryBuilder('split')
            .innerJoin('split.expense', 'expense')
            .where('expense.groupId = :groupId', { groupId })
            .andWhere('expense.isDeleted = false')
            .andWhere('split.isSettled = false')
            .getCount();
        if (unsettled > 0) {
            throw new common_1.BadRequestException('Settle all outstanding balances before deleting the group');
        }
        await this.groupRepo.update({ id: groupId }, { isActive: false });
        return { message: 'Group deleted successfully' };
    }
    async listMembers(groupId) {
        const members = await this.groupMemberRepo.find({
            where: { groupId, leftAt: (0, typeorm_2.IsNull)() },
            relations: { user: true },
            order: { joinedAt: 'ASC' },
        });
        return members.map((m) => this.toMemberDto({ ...m, user: m.user }));
    }
    async inviteMember(groupId, dto, invitedById) {
        const email = dto.email.trim().toLowerCase();
        const group = await this.groupRepo.findOne({
            where: { id: groupId, isActive: true },
            select: { id: true, name: true },
        });
        if (!group)
            throw new common_1.NotFoundException('Group not found');
        const user = await this.userRepo.findOne({
            where: { email },
            select: { id: true, name: true, email: true, avatarUrl: true },
        });
        if (user) {
            const existing = await this.groupMemberRepo.findOne({
                where: { groupId, userId: user.id },
            });
            if (existing && existing.leftAt === null) {
                throw new common_1.ConflictException('User is already a member of this group');
            }
            let member;
            if (existing && existing.leftAt !== null) {
                existing.leftAt = null;
                existing.role = enums_1.GroupRole.Member;
                existing.joinedAt = new Date();
                member = await this.groupMemberRepo.save(existing);
            }
            else {
                member = this.groupMemberRepo.create({
                    groupId,
                    userId: user.id,
                    role: enums_1.GroupRole.Member,
                });
                member = await this.groupMemberRepo.save(member);
            }
            member = await this.groupMemberRepo.findOne({
                where: { id: member.id },
                relations: { user: true },
            });
            await this.groupInviteRepo.update({ groupId, email, status: enums_1.InviteStatus.Pending }, { status: enums_1.InviteStatus.Accepted, acceptedAt: new Date() });
            void this.activityService.log({
                actorId: invitedById,
                action: enums_1.ActivityAction.MemberJoined,
                groupId,
                targetUserId: member.userId,
                entityId: groupId,
                entityType: enums_1.EntityType.Group,
            });
            return {
                type: 'member',
                message: `${member.user.name} was added to the group`,
                member: this.toMemberDto({ ...member, user: member.user }),
            };
        }
        const existingInvite = await this.groupInviteRepo.findOne({
            where: { groupId, email },
        });
        if (existingInvite?.status === enums_1.InviteStatus.Pending && existingInvite.expiresAt > new Date()) {
            const invite = this.toPendingInviteDto(existingInvite, group.name);
            await this.dispatchInviteEmail(invite, invitedById);
            return {
                type: 'invite',
                message: `Invite already sent to ${email}. We re-sent the join email.`,
                invite,
            };
        }
        const token = crypto.randomBytes(24).toString('hex');
        const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
        let inviteRow;
        if (existingInvite) {
            existingInvite.token = token;
            existingInvite.status = enums_1.InviteStatus.Pending;
            existingInvite.invitedById = invitedById;
            existingInvite.expiresAt = expiresAt;
            existingInvite.acceptedAt = null;
            existingInvite.createdAt = new Date();
            inviteRow = await this.groupInviteRepo.save(existingInvite);
        }
        else {
            inviteRow = this.groupInviteRepo.create({
                groupId,
                email,
                token,
                invitedById,
                expiresAt,
            });
            inviteRow = await this.groupInviteRepo.save(inviteRow);
        }
        const invite = this.toPendingInviteDto(inviteRow, group.name);
        await this.dispatchInviteEmail(invite, invitedById);
        return {
            type: 'invite',
            message: `Invite emailed to ${email}. They'll join after creating a Divyourday account.`,
            invite,
        };
    }
    async listPendingInvites(groupId) {
        const group = await this.groupRepo.findOne({
            where: { id: groupId, isActive: true },
            select: { name: true },
        });
        if (!group)
            throw new common_1.NotFoundException('Group not found');
        const invites = await this.groupInviteRepo.find({
            where: {
                groupId,
                status: enums_1.InviteStatus.Pending,
                expiresAt: (0, typeorm_2.MoreThan)(new Date()),
            },
            order: { createdAt: 'DESC' },
        });
        return invites.map((i) => this.toPendingInviteDto(i, group.name));
    }
    async cancelInvite(groupId, inviteId) {
        const invite = await this.groupInviteRepo.findOne({
            where: { id: inviteId, groupId, status: enums_1.InviteStatus.Pending },
        });
        if (!invite)
            throw new common_1.NotFoundException('Invite not found');
        await this.groupInviteRepo.update({ id: inviteId }, { status: enums_1.InviteStatus.Cancelled });
        return { message: 'Invite cancelled' };
    }
    async getInvitePreview(token) {
        const invite = await this.groupInviteRepo.findOne({
            where: { token },
            relations: { group: true, invitedBy: true },
        });
        if (!invite || !invite.group?.isActive) {
            throw new common_1.NotFoundException('Invite not found or no longer valid');
        }
        const isExpired = invite.status !== enums_1.InviteStatus.Pending || invite.expiresAt.getTime() < Date.now();
        if (invite.status === enums_1.InviteStatus.Pending && invite.expiresAt.getTime() < Date.now()) {
            await this.groupInviteRepo.update({ id: invite.id }, { status: enums_1.InviteStatus.Expired });
        }
        return {
            groupId: invite.group.id,
            groupName: invite.group.name,
            groupDescription: invite.group.description,
            invitedEmail: invite.email,
            invitedByName: invite.invitedBy.name,
            status: isExpired && invite.status === enums_1.InviteStatus.Pending ? enums_1.InviteStatus.Expired : invite.status,
            expiresAt: invite.expiresAt,
            isExpired,
        };
    }
    async acceptInvite(token, userId) {
        const invite = await this.groupInviteRepo.findOne({
            where: { token },
            relations: { group: true },
        });
        if (!invite || !invite.group?.isActive) {
            throw new common_1.NotFoundException('Invite not found');
        }
        if (invite.status !== enums_1.InviteStatus.Pending) {
            throw new common_1.BadRequestException('This invite is no longer valid');
        }
        if (invite.expiresAt.getTime() < Date.now()) {
            await this.groupInviteRepo.update({ id: invite.id }, { status: enums_1.InviteStatus.Expired });
            throw new common_1.BadRequestException('This invite has expired');
        }
        const user = await this.userRepo.findOne({
            where: { id: userId },
            select: { id: true, email: true },
        });
        if (!user)
            throw new common_1.NotFoundException('User not found');
        if (user.email.toLowerCase() !== invite.email.toLowerCase()) {
            throw new common_1.ForbiddenException(`This invite was sent to ${invite.email}. Sign in with that email to join.`);
        }
        await this.addUserToGroup(invite.groupId, user.id, invite.invitedById);
        await this.groupInviteRepo.update({ id: invite.id }, { status: enums_1.InviteStatus.Accepted, acceptedAt: new Date() });
        return {
            message: `You joined ${invite.group.name}`,
            groupId: invite.groupId,
        };
    }
    async acceptPendingInvitesForEmail(email, userId) {
        const pending = await this.groupInviteRepo.find({
            where: {
                email: email.toLowerCase(),
                status: enums_1.InviteStatus.Pending,
                expiresAt: (0, typeorm_2.MoreThan)(new Date()),
            },
        });
        for (const invite of pending) {
            try {
                await this.addUserToGroup(invite.groupId, userId, invite.invitedById);
                await this.groupInviteRepo.update({ id: invite.id }, { status: enums_1.InviteStatus.Accepted, acceptedAt: new Date() });
            }
            catch (err) {
                this.logger.warn(`Failed to auto-accept invite ${invite.id}: ${String(err)}`);
            }
        }
        return pending.length;
    }
    async addUserToGroup(groupId, userId, actorId) {
        const existing = await this.groupMemberRepo.findOne({
            where: { groupId, userId },
        });
        if (existing && existing.leftAt === null)
            return;
        if (existing) {
            existing.leftAt = null;
            existing.role = enums_1.GroupRole.Member;
            existing.joinedAt = new Date();
            await this.groupMemberRepo.save(existing);
        }
        else {
            const member = this.groupMemberRepo.create({ groupId, userId, role: enums_1.GroupRole.Member });
            await this.groupMemberRepo.save(member);
        }
        void this.activityService.log({
            actorId,
            action: enums_1.ActivityAction.MemberJoined,
            groupId,
            targetUserId: userId,
            entityId: groupId,
            entityType: enums_1.EntityType.Group,
        });
    }
    toPendingInviteDto(invite, groupName) {
        const frontend = this.config.get('app.appOrigin', 'http://localhost:5173');
        return {
            id: invite.id,
            email: invite.email,
            groupId: invite.groupId,
            groupName,
            inviteUrl: `${frontend}/join/${invite.token}`,
            status: invite.status,
            expiresAt: invite.expiresAt,
            createdAt: invite.createdAt,
        };
    }
    async dispatchInviteEmail(invite, invitedById) {
        const inviter = await this.userRepo.findOne({
            where: { id: invitedById },
            select: { name: true },
        });
        const sent = await this.mailService.sendGroupInvite({
            to: invite.email,
            groupName: invite.groupName,
            invitedByName: inviter?.name ?? 'Someone',
            inviteUrl: invite.inviteUrl,
            expiresAt: new Date(invite.expiresAt),
        });
        if (!sent) {
            this.logger.warn(`[DEV] Group invite for ${invite.email}: ${invite.inviteUrl}`);
        }
    }
    async removeMember(groupId, targetUserId, requesterId) {
        if (targetUserId === requesterId) {
            throw new common_1.BadRequestException('Use the leave endpoint to remove yourself');
        }
        const membership = await this.groupMemberRepo.findOne({
            where: { groupId, userId: targetUserId, leftAt: (0, typeorm_2.IsNull)() },
        });
        if (!membership)
            throw new common_1.NotFoundException('Member not found in this group');
        const unsettled = await this.expenseSplitRepo
            .createQueryBuilder('split')
            .innerJoin('split.expense', 'expense')
            .where('split.userId = :targetUserId', { targetUserId })
            .andWhere('split.isSettled = false')
            .andWhere('expense.groupId = :groupId', { groupId })
            .andWhere('expense.isDeleted = false')
            .andWhere('expense.paidById != :targetUserId', { targetUserId })
            .getCount();
        if (unsettled > 0) {
            throw new common_1.BadRequestException('Member has unsettled balances — settle first');
        }
        await this.groupMemberRepo.update({ id: membership.id }, { leftAt: new Date() });
        return { message: 'Member removed successfully' };
    }
    async leaveGroup(groupId, userId) {
        const membership = await this.groupMemberRepo.findOne({
            where: { groupId, userId, leftAt: (0, typeorm_2.IsNull)() },
        });
        if (!membership)
            throw new common_1.NotFoundException('You are not a member of this group');
        if (membership.role === enums_1.GroupRole.Admin) {
            const adminCount = await this.groupMemberRepo.count({
                where: { groupId, role: enums_1.GroupRole.Admin, leftAt: (0, typeorm_2.IsNull)() },
            });
            if (adminCount === 1) {
                throw new common_1.BadRequestException('You are the only admin. Assign another admin before leaving.');
            }
        }
        const unsettled = await this.expenseSplitRepo
            .createQueryBuilder('split')
            .innerJoin('split.expense', 'expense')
            .where('split.userId = :userId', { userId })
            .andWhere('split.isSettled = false')
            .andWhere('expense.groupId = :groupId', { groupId })
            .andWhere('expense.isDeleted = false')
            .andWhere('expense.paidById != :userId', { userId })
            .getCount();
        if (unsettled > 0) {
            throw new common_1.BadRequestException('You have unsettled balances — settle them before leaving');
        }
        await this.groupMemberRepo.update({ id: membership.id }, { leftAt: new Date() });
        return { message: 'You have left the group' };
    }
    async getBalances(groupId) {
        const members = await this.groupMemberRepo.find({
            where: { groupId, leftAt: (0, typeorm_2.IsNull)() },
            relations: { user: true },
        });
        const results = [];
        for (const m of members) {
            const uid = m.userId;
            const [rawOwes, rawOwed, settled, received] = await Promise.all([
                this.sumSplitAmount(uid, groupId, 'owes'),
                this.sumSplitAmount(uid, groupId, 'owed'),
                this.sumSettlements(groupId, uid, 'payerId'),
                this.sumSettlements(groupId, uid, 'payeeId'),
            ]);
            const owes = Math.max(0, rawOwes - settled);
            const owed = Math.max(0, rawOwed - received);
            results.push({
                userId: uid,
                name: m.user.name,
                avatarUrl: m.user.avatarUrl,
                owes,
                owed,
                net: owed - owes,
            });
        }
        return results;
    }
    async countActiveMembers(groupId) {
        return this.groupMemberRepo.count({ where: { groupId, leftAt: (0, typeorm_2.IsNull)() } });
    }
    async sumSplitAmount(userId, groupId, mode) {
        const qb = this.expenseSplitRepo
            .createQueryBuilder('split')
            .innerJoin('split.expense', 'expense')
            .select('COALESCE(SUM(split.amount), 0)', 'total')
            .where('expense.groupId = :groupId', { groupId })
            .andWhere('expense.isDeleted = false');
        if (mode === 'owes') {
            qb.andWhere('split.userId = :userId', { userId })
                .andWhere('expense.paidById != :userId', { userId });
        }
        else {
            qb.andWhere('split.userId != :userId', { userId })
                .andWhere('expense.paidById = :userId', { userId });
        }
        const result = await qb.getRawOne();
        return Number(result?.total ?? 0);
    }
    async sumSettlements(groupId, userId, field) {
        const qb = this.settlementRepo
            .createQueryBuilder('s')
            .select('COALESCE(SUM(s.amount), 0)', 'total')
            .where('s.groupId = :groupId', { groupId })
            .andWhere('s.isConfirmed = true');
        if (field === 'payerId') {
            qb.andWhere('s.payerId = :userId', { userId });
        }
        else {
            qb.andWhere('s.payeeId = :userId', { userId });
        }
        const result = await qb.getRawOne();
        return Number(result?.total ?? 0);
    }
    toGroupDto(group, memberCount) {
        return {
            id: group.id,
            name: group.name,
            description: group.description,
            imageUrl: group.imageUrl,
            currency: group.currency,
            createdById: group.createdById,
            memberCount,
            createdAt: group.createdAt,
        };
    }
    toGroupDetailDto(group, memberCount) {
        return {
            ...this.toGroupDto(group, memberCount),
            members: group.members.map((m) => this.toMemberDto(m)),
        };
    }
    toMemberDto(m) {
        return {
            userId: m.userId,
            name: m.user.name,
            email: m.user.email,
            avatarUrl: m.user.avatarUrl,
            role: m.role,
            joinedAt: m.joinedAt,
        };
    }
};
exports.GroupsService = GroupsService;
exports.GroupsService = GroupsService = GroupsService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, typeorm_1.InjectRepository)(group_entity_1.Group)),
    __param(1, (0, typeorm_1.InjectRepository)(group_member_entity_1.GroupMember)),
    __param(2, (0, typeorm_1.InjectRepository)(group_invite_entity_1.GroupInvite)),
    __param(3, (0, typeorm_1.InjectRepository)(user_entity_1.User)),
    __param(4, (0, typeorm_1.InjectRepository)(expense_split_entity_1.ExpenseSplit)),
    __param(5, (0, typeorm_1.InjectRepository)(settlement_entity_1.Settlement)),
    __metadata("design:paramtypes", [typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        activity_service_1.ActivityService,
        notifications_service_1.NotificationsService,
        mail_service_1.MailService,
        config_1.ConfigService])
], GroupsService);
//# sourceMappingURL=groups.service.js.map