"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 SessionService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionService = void 0;
const api_locale_1 = require("@hed-hog/api-locale");
const api_pagination_1 = require("@hed-hog/api-pagination");
const api_prisma_1 = require("@hed-hog/api-prisma");
const axios_1 = require("@nestjs/axios");
const common_1 = require("@nestjs/common");
const rxjs_1 = require("rxjs");
const security_service_1 = require("../security/security.service");
const setting_service_1 = require("../setting/setting.service");
const token_service_1 = require("../token/token.service");
const user_service_1 = require("../user/user.service");
let SessionService = SessionService_1 = class SessionService {
    constructor(prisma, security, setting, token, http, user, paginationService) {
        this.prisma = prisma;
        this.security = security;
        this.setting = setting;
        this.token = token;
        this.http = http;
        this.user = user;
        this.paginationService = paginationService;
        this.logger = new common_1.Logger(SessionService_1.name);
    }
    async create(locale, userId, ipAddress, userAgent) {
        if (!userId) {
            throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('session.userIdRequired', locale, 'User ID is required to create a session.'));
        }
        if (!ipAddress) {
            throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('session.ipAddressRequired', locale, 'IP address is required to create a session.'));
        }
        if (!userAgent) {
            throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('session.userAgentRequired', locale, 'User agent is required to create a session.'));
        }
        const settings = await this.setting.getSettingValues([
            'refresh-token-expiration-minutes',
            'max-concurrent-sessions'
        ]);
        const token = await this.token.createOpaqueToken();
        const hash = this.security.hashWithPepper(token);
        const maxSessions = Number(settings['max-concurrent-sessions']) || 0;
        if (maxSessions > 0) {
            await this.enforceSessionLimit(userId, maxSessions);
        }
        const session = await this.prisma.user_session.create({
            data: {
                user_id: userId,
                ip_address: ipAddress,
                user_agent: userAgent,
                expires_at: new Date(Date.now() + ((settings['refresh-token-expiration-minutes'] || 43200) * 60 * 1000)),
                hash
            },
        });
        return { token, session };
    }
    async refresh(locale, refreshToken, ipAddress, userAgent) {
        if (!refreshToken) {
            throw new common_1.UnauthorizedException((0, api_locale_1.getLocaleText)('accessDenied', locale, 'Access denied.'));
        }
        const hash = this.security.hashWithPepper(refreshToken);
        const session = await this.prisma.user_session.findFirst({
            where: {
                hash,
                revoked_at: null,
                expires_at: {
                    gt: new Date(),
                },
            },
            select: {
                id: true,
                user_id: true,
                ip_address: true,
                user_agent: true,
            },
        });
        if (!session) {
            this.logger.warn(`Refresh rejected: session not found or expired for token hash prefix ${hash.slice(0, 8)}`);
            throw new common_1.UnauthorizedException((0, api_locale_1.getLocaleText)('accessDenied', locale, 'Access denied.'));
        }
        const settings = await this.setting.getSettingValues([
            'refresh-token-expiration-minutes',
            'max-concurrent-sessions',
        ]);
        const maxSessions = Number(settings['max-concurrent-sessions']) || 0;
        if (maxSessions > 0) {
            await this.enforceSessionLimit(session.user_id, maxSessions, true);
        }
        const nextRefreshToken = await this.token.createOpaqueToken();
        const nextHash = this.security.hashWithPepper(nextRefreshToken);
        const expiresAt = new Date(Date.now() + ((settings['refresh-token-expiration-minutes'] || 43200) * 60 * 1000));
        const nextIpAddress = (ipAddress === null || ipAddress === void 0 ? void 0 : ipAddress.trim()) || session.ip_address || 'unknown';
        const nextUserAgent = (userAgent === null || userAgent === void 0 ? void 0 : userAgent.trim()) || session.user_agent || 'unknown';
        const { updateResult, newSession } = await this.prisma.$transaction(async (tx) => {
            const updateResult = await tx.user_session.updateMany({
                where: {
                    id: session.id,
                    revoked_at: null,
                },
                data: {
                    revoked_at: new Date(),
                },
            });
            const newSession = await tx.user_session.create({
                data: {
                    user_id: session.user_id,
                    ip_address: nextIpAddress,
                    user_agent: nextUserAgent,
                    expires_at: expiresAt,
                    hash: nextHash,
                },
            });
            return { updateResult, newSession };
        });
        if (updateResult.count === 0) {
            this.logger.warn(`Refresh rejected: session ${session.id} already rotated`);
            throw new common_1.UnauthorizedException((0, api_locale_1.getLocaleText)('accessDenied', locale, 'Access denied.'));
        }
        return {
            token: nextRefreshToken,
            session: newSession,
        };
    }
    async deleteRefreshTokenCookie(res, refreshToken) {
        var _a;
        await this.token.removeRefreshTokenCookie(res);
        if (!refreshToken) {
            return { count: 0, userId: null };
        }
        const hash = this.security.hashWithPepper(refreshToken);
        const session = await this.prisma.user_session.findFirst({
            where: {
                revoked_at: null,
                hash,
            },
            select: {
                user_id: true,
            },
        });
        const result = await this.prisma.user_session.updateMany({
            where: {
                revoked_at: null,
                hash,
            },
            data: {
                revoked_at: new Date(),
            },
        });
        return {
            count: result.count,
            userId: (_a = session === null || session === void 0 ? void 0 : session.user_id) !== null && _a !== void 0 ? _a : null,
        };
    }
    async getUserSessions(paginationParams, userId, locale) {
        var _a, _b, _c;
        const userExists = await this.prisma.user.findUnique({
            where: { id: userId },
            select: { id: true },
        });
        if (!userExists) {
            throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('session.userNotFound', locale, 'User not found.'));
        }
        try {
            const paginate = await this.paginationService.paginatePrismaModel(this.prisma.user_session, Object.assign(Object.assign({}, paginationParams), { where: { user_id: userId } }));
            const itemsWithLocation = await Promise.all(paginate.data.map(async (s) => {
                const ip = s.ip_address || s.ip || null;
                let location = null;
                if (ip && ip !== '127.0.0.1' && ip !== '::1') {
                    try {
                        location = await this.fetchGeoByIp(ip);
                    }
                    catch (_a) {
                        location = { ip, raw: null };
                    }
                }
                else if (ip) {
                    location = { ip: '127.0.0.1', country: 'Localhost', region: '', city: '' };
                }
                return Object.assign(Object.assign({}, s), { location });
            }));
            return {
                data: itemsWithLocation,
                total: paginate.total || 0,
                lastPage: Math.ceil((paginate.total || 0) / (paginate.pageSize || 1)),
                page: (_a = paginate.page) !== null && _a !== void 0 ? _a : 1,
                pageSize: (_c = (_b = paginate.pageSize) !== null && _b !== void 0 ? _b : paginationParams.pageSize) !== null && _c !== void 0 ? _c : 10,
            };
        }
        catch (err) {
            throw new common_1.HttpException((0, api_locale_1.getLocaleText)('session.errorFetchingSessions', locale, 'Error fetching user sessions'), common_1.HttpStatus.SERVICE_UNAVAILABLE);
        }
    }
    async getUserSessionsActive(paginationParams, userId, locale) {
        var _a, _b, _c;
        const userExists = await this.prisma.user.findUnique({
            where: { id: userId },
            select: { id: true },
        });
        if (!userExists) {
            throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('session.userNotFound', locale, 'User not found.'));
        }
        try {
            const paginate = await this.paginationService.paginatePrismaModel(this.prisma.user_session, Object.assign(Object.assign({}, paginationParams), { where: { user_id: userId, revoked_at: null, expires_at: { gt: new Date() } } }));
            const itemsWithLocation = await Promise.all(paginate.data.map(async (s) => {
                const ip = s.ip_address || s.ip || null;
                let location = null;
                if (ip && ip !== '127.0.0.1' && ip !== '::1') {
                    try {
                        location = await this.fetchGeoByIp(ip);
                    }
                    catch (_a) {
                        location = { ip, raw: null };
                    }
                }
                else if (ip) {
                    location = { ip: '127.0.0.1', country: 'Localhost', region: '', city: '' };
                }
                return Object.assign(Object.assign({}, s), { location });
            }));
            return {
                data: itemsWithLocation,
                total: paginate.total || 0,
                lastPage: Math.ceil((paginate.total || 0) / (paginate.pageSize || 1)),
                page: (_a = paginate.page) !== null && _a !== void 0 ? _a : 1,
                pageSize: (_c = (_b = paginate.pageSize) !== null && _b !== void 0 ? _b : paginationParams.pageSize) !== null && _c !== void 0 ? _c : 10,
            };
        }
        catch (err) {
            throw new common_1.HttpException((0, api_locale_1.getLocaleText)('session.errorFetchingSessions', locale, 'Error fetching user sessions'), common_1.HttpStatus.SERVICE_UNAVAILABLE);
        }
    }
    async fetchGeoByIp(ip) {
        const url = `http://ip-api.com/json/${encodeURIComponent(ip)}?fields=status,country,regionName,city,lat,lon,isp,message`;
        const response = await (0, rxjs_1.firstValueFrom)(this.http.get(url));
        const data = response.data;
        if ((data === null || data === void 0 ? void 0 : data.status) !== 'success') {
            return { ip, raw: data };
        }
        return {
            ip,
            country: data.country,
            region: data.regionName,
            city: data.city,
            lat: data.lat,
            lon: data.lon,
            isp: data.isp,
            raw: data,
        };
    }
    async markRevokedByFilter(userId, filter, activity) {
        await this.user.registerUserActivity(userId, activity);
        return this.prisma.user_session.updateMany({
            where: Object.assign({ user_id: userId }, filter),
            data: { revoked_at: new Date() },
        });
    }
    async revokeAllOtherSessions(userId, sessionId) {
        const latestSession = await this.prisma.user_session.findUnique({
            where: {
                id: sessionId
            },
            select: { id: true },
        });
        if (!latestSession) {
            return { count: 0 };
        }
        return this.markRevokedByFilter(userId, {
            NOT: { id: latestSession.id },
            revoked_at: null
        }, 'revokeAllOtherSessions');
    }
    async revokeAllSessions(userId) {
        return this.markRevokedByFilter(userId, { revoked_at: null }, 'revokeAllSessions');
    }
    async revokeUserSession(userId, sessionId, locale) {
        const session = await this.prisma.user_session.findFirst({
            where: {
                id: sessionId,
                user_id: userId
            }
        });
        if (!session) {
            throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('session.notFound', locale, 'Session not found or does not belong to user'));
        }
        await this.user.registerUserActivity(userId, "revokeSession");
        return this.prisma.user_session.update({
            where: {
                id: sessionId
            },
            data: {
                revoked_at: new Date()
            }
        });
    }
    /**
     * Enforce maximum concurrent sessions limit for a user.
     * If the limit is reached, expires the oldest sessions to make room for new ones.
     *
     * @param userId - The user ID to check
     * @param maxSessions - Maximum number of concurrent sessions allowed
     * @param isRotation - When true, the calling flow is replacing an existing session (refresh),
     *   not adding a new one, so no extra slot needs to be freed.
     */
    async enforceSessionLimit(userId, maxSessions, isRotation = false) {
        const activeSessions = await this.prisma.user_session.findMany({
            where: {
                user_id: userId,
                revoked_at: null,
                expires_at: {
                    gt: new Date()
                }
            },
            orderBy: {
                created_at: 'asc'
            },
            select: {
                id: true
            }
        });
        // During a rotation the current session is replaced (not added), so the net change is 0.
        // During creation a new session is added, so we need one extra free slot (+1).
        const slotsNeeded = isRotation ? 0 : 1;
        const sessionsToExpire = activeSessions.length - maxSessions + slotsNeeded;
        if (sessionsToExpire > 0) {
            const sessionIdsToExpire = activeSessions
                .slice(0, sessionsToExpire)
                .map(s => s.id);
            await this.prisma.user_session.updateMany({
                where: {
                    id: {
                        in: sessionIdsToExpire
                    }
                },
                data: {
                    expires_at: new Date()
                }
            });
        }
    }
};
exports.SessionService = SessionService;
exports.SessionService = SessionService = SessionService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(1, (0, common_1.Inject)((0, common_1.forwardRef)(() => security_service_1.SecurityService))),
    __param(2, (0, common_1.Inject)((0, common_1.forwardRef)(() => setting_service_1.SettingService))),
    __param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => token_service_1.TokenService))),
    __param(4, (0, common_1.Inject)((0, common_1.forwardRef)(() => axios_1.HttpService))),
    __param(5, (0, common_1.Inject)((0, common_1.forwardRef)(() => user_service_1.UserService))),
    __param(6, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_pagination_1.PaginationService))),
    __metadata("design:paramtypes", [api_prisma_1.PrismaService,
        security_service_1.SecurityService,
        setting_service_1.SettingService,
        token_service_1.TokenService,
        axios_1.HttpService,
        user_service_1.UserService,
        api_pagination_1.PaginationService])
], SessionService);
//# sourceMappingURL=session.service.js.map