"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.OtpService = void 0;
const common_1 = require("@nestjs/common");
const mongoose_1 = require("@nestjs/mongoose");
const mongoose_2 = require("mongoose");
const crypto_1 = require("crypto");
const otp_management_schema_1 = require("./schemas/otp-management.schema");
const OTP_EXPIRY_MS = 10 * 60 * 1000;
const MAX_ATTEMPTS = 5;
const LOCKOUT_MS = 5 * 60 * 1000;
const RESEND_COOLDOWN_MS = 30 * 1000;
let OtpService = class OtpService {
    otpModel;
    constructor(otpModel) {
        this.otpModel = otpModel;
    }
    now() {
        return Date.now();
    }
    normalizeValue(channel, value) {
        if (channel === 'email')
            return (value || '').trim().toLowerCase();
        return (value || '').replace(/[^\d+]/g, '');
    }
    debugNormalizedValue(channel, value) {
        return this.normalizeValue(channel, value);
    }
    generateCode() {
        return (0, crypto_1.randomInt)(100000, 1000000).toString();
    }
    getExpiryMinutes() {
        return Math.round(OTP_EXPIRY_MS / 60000);
    }
    async create(channel, value) {
        const identifier = this.normalizeValue(channel, value);
        const code = this.generateCode();
        const expiresAt = new Date(this.now() + OTP_EXPIRY_MS);
        await this.otpModel.updateOne({ channel, identifier }, {
            $set: {
                code,
                expiresAt,
                attempts: 0,
                lockedUntil: null,
                lastSentAt: new Date(),
            },
            $setOnInsert: { channel, identifier },
        }, { upsert: true });
        return code;
    }
    async canResend(channel, value) {
        const identifier = this.normalizeValue(channel, value);
        const doc = await this.otpModel.findOne({ channel, identifier }).lean();
        if (!doc || !doc.lastSentAt)
            return true;
        return this.now() - new Date(doc.lastSentAt).getTime() >= RESEND_COOLDOWN_MS;
    }
    async markResent(channel, value) {
        const identifier = this.normalizeValue(channel, value);
        await this.otpModel.updateOne({ channel, identifier }, { $set: { lastSentAt: new Date() } });
    }
    async check(channel, value, otp) {
        const identifier = this.normalizeValue(channel, value);
        const doc = await this.otpModel.findOne({ channel, identifier });
        if (!doc)
            throw new common_1.BadRequestException('Invalid or expired OTP. Please try again.');
        const now = this.now();
        if (doc.lockedUntil && now < new Date(doc.lockedUntil).getTime()) {
            throw new common_1.ForbiddenException('Too many attempts. Try again later.');
        }
        if (now > new Date(doc.expiresAt).getTime()) {
            await this.otpModel.deleteOne({ _id: doc._id });
            throw new common_1.BadRequestException('OTP expired. Please request a new one.');
        }
        if (otp !== doc.code) {
            const attempts = (doc.attempts ?? 0) + 1;
            const update = { attempts };
            if (attempts >= MAX_ATTEMPTS) {
                update.lockedUntil = new Date(now + LOCKOUT_MS);
            }
            await this.otpModel.updateOne({ _id: doc._id }, { $set: update });
            throw new common_1.BadRequestException('Incorrect OTP.');
        }
        return true;
    }
    async verify(channel, value, otp) {
        const identifier = this.normalizeValue(channel, value);
        const doc = await this.otpModel.findOne({ channel, identifier });
        if (!doc)
            throw new common_1.BadRequestException('OTP not found. Please request a new one.');
        const now = this.now();
        if (doc.lockedUntil && now < new Date(doc.lockedUntil).getTime()) {
            throw new common_1.ForbiddenException('Too many attempts. Try again later.');
        }
        if (now > new Date(doc.expiresAt).getTime()) {
            await this.otpModel.deleteOne({ _id: doc._id });
            throw new common_1.BadRequestException('OTP expired. Please request a new one.');
        }
        if (otp !== doc.code) {
            const attempts = (doc.attempts ?? 0) + 1;
            const update = { attempts };
            if (attempts >= MAX_ATTEMPTS) {
                update.lockedUntil = new Date(now + LOCKOUT_MS);
            }
            await this.otpModel.updateOne({ _id: doc._id }, { $set: update });
            throw new common_1.BadRequestException('Incorrect OTP.');
        }
        await this.otpModel.deleteOne({ _id: doc._id });
        return true;
    }
    async getAttemptsInfo(channel, value) {
        const identifier = this.normalizeValue(channel, value);
        const doc = await this.otpModel.findOne({ channel, identifier }).lean();
        if (!doc)
            return { attemptsUsed: 0, lockedUntil: null };
        return {
            attemptsUsed: doc.attempts ?? 0,
            lockedUntil: doc.lockedUntil ? new Date(doc.lockedUntil).getTime() : null,
        };
    }
    async getResendLeftMs(channel, value) {
        const identifier = this.normalizeValue(channel, value);
        const doc = await this.otpModel.findOne({ channel, identifier }).lean();
        if (!doc || !doc.lastSentAt)
            return 0;
        const left = RESEND_COOLDOWN_MS - (this.now() - new Date(doc.lastSentAt).getTime());
        return Math.max(0, left);
    }
};
exports.OtpService = OtpService;
exports.OtpService = OtpService = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, mongoose_1.InjectModel)(otp_management_schema_1.OtpManagement.name)),
    __metadata("design:paramtypes", [mongoose_2.Model])
], OtpService);
//# sourceMappingURL=otp.service.js.map