"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.ExpensesService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const expense_entity_1 = require("../../database/entities/expense.entity");
const expense_split_entity_1 = require("../../database/entities/expense-split.entity");
const group_entity_1 = require("../../database/entities/group.entity");
const group_member_entity_1 = require("../../database/entities/group-member.entity");
const enums_1 = require("../../common/enums");
const split_calculator_service_1 = require("./split-calculator.service");
const activity_service_1 = require("../activity/activity.service");
const notifications_service_1 = require("../notifications/notifications.service");
let ExpensesService = class ExpensesService {
    constructor(expenseRepo, expenseSplitRepo, groupRepo, groupMemberRepo, dataSource, splitCalculator, activityService, notificationsService) {
        this.expenseRepo = expenseRepo;
        this.expenseSplitRepo = expenseSplitRepo;
        this.groupRepo = groupRepo;
        this.groupMemberRepo = groupMemberRepo;
        this.dataSource = dataSource;
        this.splitCalculator = splitCalculator;
        this.activityService = activityService;
        this.notificationsService = notificationsService;
    }
    async createExpense(groupId, createdById, dto) {
        const group = await this.groupRepo.findOne({
            where: { id: groupId, isActive: true },
            select: { id: true, currency: true },
        });
        if (!group)
            throw new common_1.NotFoundException('Group not found');
        await this.validateMembership(groupId, dto.paidById);
        await this.validateSplitMemberships(groupId, dto.splits.map((s) => s.userId));
        const computed = this.splitCalculator.compute(dto.amount, dto.splitType, dto.splits);
        const expenseEntity = this.expenseRepo.create({
            groupId,
            description: dto.description,
            amount: String(dto.amount),
            currency: group.currency,
            category: dto.category,
            paidById: dto.paidById,
            splitType: dto.splitType,
            date: new Date(dto.date),
            notes: dto.notes,
            receiptUrl: dto.receiptUrl,
            createdById,
        });
        await this.expenseRepo.save(expenseEntity);
        const splitEntities = computed.map((c) => this.expenseSplitRepo.create({
            expenseId: expenseEntity.id,
            userId: c.userId,
            amount: String(c.amount),
            percentage: c.percentage != null ? String(c.percentage) : null,
            shares: c.shares ?? null,
            isSettled: c.userId === dto.paidById,
        }));
        await this.expenseSplitRepo.save(splitEntities);
        const expense = await this.loadExpenseWithRelations(expenseEntity.id);
        if (!expense)
            throw new common_1.NotFoundException('Expense not found');
        const memberIds = expense.splits.map((s) => s.userId).filter((id) => id !== createdById);
        void this.activityService.log({
            actorId: createdById,
            action: enums_1.ActivityAction.ExpenseAdded,
            groupId: groupId ?? undefined,
            entityId: expense.id,
            entityType: enums_1.EntityType.Expense,
            metadata: { description: expense.description, amount: Number(expense.amount) },
        });
        void this.notificationsService.notifyMany(memberIds, {
            title: 'New expense added',
            body: `${expense.paidBy.name} added "${expense.description}" — ${expense.currency} ${Number(expense.amount)}`,
            type: enums_1.NotificationType.Expense,
            entityId: expense.id,
        });
        return this.toDto(expense);
    }
    async listGroupExpenses(groupId, query) {
        const page = query.page ?? 1;
        const limit = query.limit ?? 20;
        const skip = (page - 1) * limit;
        const where = this.buildExpenseWhere({
            groupId,
            isDeleted: false,
            category: query.category,
            from: query.from,
            to: query.to,
        });
        const sortBy = query.sortBy ?? 'date';
        const sortOrder = query.sortOrder ?? 'desc';
        const [expenses, total] = await Promise.all([
            this.expenseRepo.find({
                where,
                relations: { paidBy: true, splits: { user: true } },
                order: { [sortBy]: sortOrder },
                skip,
                take: limit,
            }),
            this.expenseRepo.count({ where }),
        ]);
        return this.toPaginated(expenses, total, page, limit);
    }
    async listMyExpenses(userId, query) {
        const page = query.page ?? 1;
        const limit = query.limit ?? 20;
        const skip = (page - 1) * limit;
        const qb = this.expenseRepo
            .createQueryBuilder('expense')
            .innerJoin('expense.splits', 'split', 'split.userId = :userId', { userId })
            .leftJoinAndSelect('expense.paidBy', 'paidBy')
            .leftJoinAndSelect('expense.splits', 'splits')
            .leftJoinAndSelect('splits.user', 'splitUser')
            .where('expense.isDeleted = false');
        if (query.category) {
            qb.andWhere('expense.category = :category', { category: query.category });
        }
        if (query.from) {
            qb.andWhere('expense.date >= :from', { from: new Date(query.from) });
        }
        if (query.to) {
            qb.andWhere('expense.date <= :to', { to: new Date(query.to) });
        }
        const sortBy = query.sortBy ?? 'date';
        const sortOrder = query.sortOrder ?? 'DESC';
        qb.orderBy(`expense.${sortBy}`, sortOrder.toUpperCase());
        qb.skip(skip).take(limit);
        const [expenses, total] = await Promise.all([
            qb.getMany(),
            qb.clone().skip(undefined).take(undefined).getCount(),
        ]);
        return this.toPaginated(expenses, total, page, limit);
    }
    async getExpense(expenseId, userId) {
        const expense = await this.findExpenseOrThrow(expenseId);
        await this.assertCanView(expense, userId);
        return this.toDto(expense);
    }
    async updateExpense(expenseId, userId, dto) {
        const expense = await this.findExpenseOrThrow(expenseId);
        await this.assertCanModify(expense, userId);
        const newAmount = dto.amount ?? Number(expense.amount);
        const newSplitType = dto.splitType ?? expense.splitType;
        const newSplits = dto.splits ?? expense.splits.map((s) => ({
            userId: s.userId,
            amount: Number(s.amount),
            percentage: s.percentage ? Number(s.percentage) : undefined,
            shares: s.shares ?? undefined,
        }));
        const computed = this.splitCalculator.compute(newAmount, newSplitType, newSplits);
        const paidById = dto.paidById ?? expense.paidById;
        const updated = await this.dataSource.transaction(async (manager) => {
            await manager.delete(expense_split_entity_1.ExpenseSplit, { expenseId });
            await manager.update(expense_entity_1.Expense, { id: expenseId }, {
                ...(dto.description !== undefined && { description: dto.description }),
                ...(dto.amount !== undefined && { amount: String(dto.amount) }),
                ...(dto.currency !== undefined && { currency: dto.currency }),
                ...(dto.category !== undefined && { category: dto.category }),
                ...(dto.paidById !== undefined && { paidById: dto.paidById }),
                ...(dto.splitType !== undefined && { splitType: dto.splitType }),
                ...(dto.date !== undefined && { date: new Date(dto.date) }),
                ...(dto.notes !== undefined && { notes: dto.notes }),
                ...(dto.receiptUrl !== undefined && { receiptUrl: dto.receiptUrl || null }),
            });
            const splitEntities = computed.map((c) => manager.create(expense_split_entity_1.ExpenseSplit, {
                expenseId,
                userId: c.userId,
                amount: String(c.amount),
                percentage: c.percentage != null ? String(c.percentage) : null,
                shares: c.shares ?? null,
                isSettled: c.userId === paidById,
            }));
            await manager.save(expense_split_entity_1.ExpenseSplit, splitEntities);
            return manager.findOne(expense_entity_1.Expense, {
                where: { id: expenseId },
                relations: { paidBy: true, splits: { user: true } },
            });
        });
        if (!updated)
            throw new common_1.NotFoundException('Expense not found');
        void this.activityService.log({
            actorId: userId,
            action: enums_1.ActivityAction.ExpenseUpdated,
            groupId: updated.groupId ?? undefined,
            entityId: updated.id,
            entityType: enums_1.EntityType.Expense,
            metadata: { description: updated.description },
        });
        return this.toDto(updated);
    }
    async deleteExpense(expenseId, userId) {
        const expense = await this.findExpenseOrThrow(expenseId);
        await this.assertCanModify(expense, userId);
        const settledByOthers = await this.expenseSplitRepo.count({
            where: {
                expenseId,
                isSettled: true,
                userId: (0, typeorm_2.Not)(expense.paidById),
            },
        });
        if (settledByOthers > 0) {
            throw new common_1.BadRequestException('Cannot delete an expense that has been partially or fully settled');
        }
        await this.expenseRepo.update({ id: expenseId }, { isDeleted: true });
        void this.activityService.log({
            actorId: userId,
            action: enums_1.ActivityAction.ExpenseDeleted,
            groupId: expense.groupId ?? undefined,
            entityId: expenseId,
            entityType: enums_1.EntityType.Expense,
            metadata: { description: expense.description },
        });
        return { message: 'Expense deleted successfully' };
    }
    async validateMembership(groupId, userId) {
        const member = await this.groupMemberRepo.findOne({
            where: { groupId, userId, leftAt: (0, typeorm_2.IsNull)() },
        });
        if (!member) {
            throw new common_1.BadRequestException(`User ${userId} is not a member of this group`);
        }
    }
    async validateSplitMemberships(groupId, userIds) {
        const unique = [...new Set(userIds)];
        for (const uid of unique) {
            await this.validateMembership(groupId, uid);
        }
    }
    async assertCanView(expense, userId) {
        const inSplits = expense.splits?.some((s) => s.userId === userId);
        if (inSplits)
            return;
        if (expense.groupId) {
            const member = await this.groupMemberRepo.findOne({
                where: { groupId: expense.groupId, userId, leftAt: (0, typeorm_2.IsNull)() },
            });
            if (member)
                return;
        }
        throw new common_1.ForbiddenException('You do not have access to this expense');
    }
    async assertCanModify(expense, userId) {
        if (expense.createdById === userId)
            return;
        if (expense.groupId) {
            const admin = await this.groupMemberRepo.findOne({
                where: { groupId: expense.groupId, userId, role: enums_1.GroupRole.Admin, leftAt: (0, typeorm_2.IsNull)() },
            });
            if (admin)
                return;
        }
        throw new common_1.ForbiddenException('Only the expense creator or a group admin can do this');
    }
    buildExpenseWhere(params) {
        const where = {
            isDeleted: params.isDeleted,
        };
        if (params.groupId)
            where.groupId = params.groupId;
        if (params.category)
            where.category = params.category;
        if (params.from && params.to) {
            where.date = (0, typeorm_2.Between)(new Date(params.from), new Date(params.to));
        }
        else if (params.from) {
            where.date = (0, typeorm_2.MoreThanOrEqual)(new Date(params.from));
        }
        else if (params.to) {
            where.date = (0, typeorm_2.LessThanOrEqual)(new Date(params.to));
        }
        return where;
    }
    async findExpenseOrThrow(expenseId) {
        const expense = await this.loadExpenseWithRelations(expenseId);
        if (!expense)
            throw new common_1.NotFoundException('Expense not found');
        return expense;
    }
    async loadExpenseWithRelations(expenseId) {
        const expense = await this.expenseRepo.findOne({
            where: { id: expenseId, isDeleted: false },
            relations: { paidBy: true, splits: { user: true } },
        });
        if (expense?.splits) {
            expense.splits.sort((a, b) => Number(b.amount) - Number(a.amount));
        }
        return expense;
    }
    toDto(expense) {
        return {
            id: expense.id,
            groupId: expense.groupId,
            description: expense.description,
            amount: Number(expense.amount),
            currency: expense.currency,
            category: expense.category,
            paidBy: {
                id: expense.paidBy.id,
                name: expense.paidBy.name,
                avatarUrl: expense.paidBy.avatarUrl,
            },
            splitType: expense.splitType,
            splits: (expense.splits ?? []).map((s) => ({
                userId: s.userId,
                name: s.user.name,
                avatarUrl: s.user.avatarUrl,
                amount: Number(s.amount),
                percentage: s.percentage ? Number(s.percentage) : null,
                shares: s.shares,
                isSettled: s.isSettled,
            })),
            date: expense.date,
            notes: expense.notes,
            receiptUrl: expense.receiptUrl,
            createdAt: expense.createdAt,
        };
    }
    toPaginated(expenses, total, page, limit) {
        const totalPages = Math.ceil(total / limit);
        return {
            data: expenses.map((e) => this.toDto(e)),
            total,
            page,
            limit,
            totalPages,
            hasNextPage: page < totalPages,
        };
    }
};
exports.ExpensesService = ExpensesService;
exports.ExpensesService = ExpensesService = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, typeorm_1.InjectRepository)(expense_entity_1.Expense)),
    __param(1, (0, typeorm_1.InjectRepository)(expense_split_entity_1.ExpenseSplit)),
    __param(2, (0, typeorm_1.InjectRepository)(group_entity_1.Group)),
    __param(3, (0, typeorm_1.InjectRepository)(group_member_entity_1.GroupMember)),
    __metadata("design:paramtypes", [typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.DataSource,
        split_calculator_service_1.SplitCalculatorService,
        activity_service_1.ActivityService,
        notifications_service_1.NotificationsService])
], ExpensesService);
//# sourceMappingURL=expenses.service.js.map