"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 CaptiveService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CaptiveService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const business_entity_1 = require("../database/entities/business.entity");
const business_settings_entity_1 = require("../database/entities/business-settings.entity");
const system_settings_entity_1 = require("../database/entities/system-settings.entity");
const package_entity_1 = require("../database/entities/package.entity");
const package_router_assignment_entity_1 = require("../database/entities/package-router-assignment.entity");
const router_entity_1 = require("../database/entities/router.entity");
const customer_entity_1 = require("../database/entities/customer.entity");
const customer_device_entity_1 = require("../database/entities/customer-device.entity");
const wifi_session_entity_1 = require("../database/entities/wifi-session.entity");
const payment_entity_1 = require("../database/entities/payment.entity");
const mpesa_callback_entity_1 = require("../database/entities/mpesa-callback.entity");
const voucher_entity_1 = require("../database/entities/voucher.entity");
const mpesa_service_1 = require("../mpesa/mpesa.service");
const vouchers_service_1 = require("../vouchers/vouchers.service");
const notifications_service_1 = require("../notifications/notifications.service");
const phone_util_1 = require("../common/utils/phone.util");
const id_generator_util_1 = require("../common/utils/id-generator.util");
let CaptiveService = CaptiveService_1 = class CaptiveService {
    constructor(businessRepo, businessSettingsRepo, systemSettingsRepo, packageRepo, packageRouterAssignmentRepo, routerRepo, customerRepo, customerDeviceRepo, wifiSessionRepo, paymentRepo, mpesaCallbackRepo, voucherRepo, mpesaService, vouchersService, notificationsService) {
        this.businessRepo = businessRepo;
        this.businessSettingsRepo = businessSettingsRepo;
        this.systemSettingsRepo = systemSettingsRepo;
        this.packageRepo = packageRepo;
        this.packageRouterAssignmentRepo = packageRouterAssignmentRepo;
        this.routerRepo = routerRepo;
        this.customerRepo = customerRepo;
        this.customerDeviceRepo = customerDeviceRepo;
        this.wifiSessionRepo = wifiSessionRepo;
        this.paymentRepo = paymentRepo;
        this.mpesaCallbackRepo = mpesaCallbackRepo;
        this.voucherRepo = voucherRepo;
        this.mpesaService = mpesaService;
        this.vouchersService = vouchersService;
        this.notificationsService = notificationsService;
        this.logger = new common_1.Logger(CaptiveService_1.name);
        this.rateLimitMap = new Map();
        this.otpStore = new Map();
        setInterval(() => this.cleanupRateLimits(), 60_000);
        setInterval(() => this.cleanupOtps(), 60_000);
    }
    async identifyRouter(macAddress, ipAddress, routerId, accessPointId) {
        let router = null;
        if (routerId) {
            router = await this.routerRepo.findOne({
                where: { id: routerId, isActive: true },
                relations: ['business', 'location'],
            });
        }
        if (!router && accessPointId) {
            router = await this.routerRepo.findOne({
                where: { id: accessPointId, isActive: true },
                relations: ['business', 'location'],
            });
        }
        if (!router && macAddress) {
            const session = await this.wifiSessionRepo.findOne({
                where: { macAddress, status: wifi_session_entity_1.WifiSessionStatus.ACTIVE },
                relations: ['router', 'router.business', 'router.location'],
                order: { createdAt: 'DESC' },
            });
            if (session?.router) {
                router = session.router;
            }
        }
        if (!router && ipAddress) {
            const session = await this.wifiSessionRepo.findOne({
                where: { ipAddress, status: wifi_session_entity_1.WifiSessionStatus.ACTIVE },
                relations: ['router', 'router.business', 'router.location'],
                order: { createdAt: 'DESC' },
            });
            if (session?.router) {
                router = session.router;
            }
        }
        if (router) {
            return {
                router,
                business: router.business || null,
                identified: true,
            };
        }
        return {
            router: null,
            business: null,
            identified: false,
        };
    }
    async getPortalConfig(routerId) {
        const router = await this.routerRepo.findOne({
            where: { id: routerId, isActive: true },
            relations: ['business'],
        });
        if (!router) {
            throw new common_1.NotFoundException(`Router ${routerId} not found`);
        }
        const business = await this.businessRepo.findOne({
            where: { id: router.businessId, isActive: true, isSuspended: false },
        });
        if (!business) {
            throw new common_1.BadRequestException('Business not available');
        }
        const packages = await this.getPackagesForPortal(routerId);
        const [welcomeMessage, primaryColor, termsUrl, privacyPolicyUrl, supportPhone, whatsappNumber, languageSettings,] = await Promise.all([
            this.getBusinessSetting(business.id, 'portal_welcome_message', `Welcome to ${business.name} WiFi`),
            this.getBusinessSetting(business.id, 'portal_primary_color', '#2563eb'),
            this.getBusinessSetting(business.id, 'portal_terms_url', null),
            this.getBusinessSetting(business.id, 'portal_privacy_url', null),
            this.getBusinessSetting(business.id, 'portal_support_phone', business.phone || null),
            this.getBusinessSetting(business.id, 'portal_whatsapp_number', null),
            this.getBusinessSettingJson(business.id, 'portal_language_settings', { default: 'en' }),
        ]);
        return {
            businessName: business.name,
            logoUrl: business.logo || null,
            welcomeMessage,
            packages,
            themeSettings: {
                primaryColor,
                logo: business.logo || null,
            },
            termsUrl,
            privacyPolicyUrl,
            supportPhone,
            whatsappNumber,
            languageSettings,
        };
    }
    async getPackagesForPortal(routerId) {
        const router = await this.routerRepo.findOne({
            where: { id: routerId, isActive: true },
        });
        if (!router) {
            throw new common_1.NotFoundException(`Router ${routerId} not found`);
        }
        const assignments = await this.packageRouterAssignmentRepo.find({
            where: { routerId: router.id },
            relations: ['package'],
        });
        const assignedPackageIds = assignments.map((a) => a.packageId);
        let packages;
        if (assignedPackageIds.length > 0) {
            packages = await this.packageRepo.find({
                where: {
                    id: (0, typeorm_2.In)(assignedPackageIds),
                    businessId: router.businessId,
                    isActive: true,
                },
                order: { sortOrder: 'ASC', priceKes: 'ASC' },
            });
        }
        else {
            packages = await this.packageRepo.find({
                where: {
                    businessId: router.businessId,
                    isActive: true,
                },
                order: { sortOrder: 'ASC', priceKes: 'ASC' },
            });
        }
        return packages.map((pkg) => ({
            id: pkg.id,
            name: pkg.name,
            description: pkg.description,
            type: pkg.type,
            priceKes: pkg.priceKes,
            durationMinutes: pkg.durationMinutes,
            dataAllowanceMb: pkg.dataAllowanceMb ? Number(pkg.dataAllowanceMb) : null,
            uploadSpeedKbps: pkg.uploadSpeedKbps,
            downloadSpeedKbps: pkg.downloadSpeedKbps,
            maxDevices: pkg.maxDevices,
            isFeatured: pkg.isFeatured,
            sortOrder: pkg.sortOrder,
        }));
    }
    async getPackageDetail(packageId) {
        const pkg = await this.packageRepo.findOne({
            where: { id: packageId, isActive: true },
        });
        if (!pkg) {
            throw new common_1.NotFoundException(`Package ${packageId} not found`);
        }
        return {
            id: pkg.id,
            name: pkg.name,
            description: pkg.description,
            type: pkg.type,
            priceKes: pkg.priceKes,
            durationMinutes: pkg.durationMinutes,
            dataAllowanceMb: pkg.dataAllowanceMb ? Number(pkg.dataAllowanceMb) : null,
            uploadSpeedKbps: pkg.uploadSpeedKbps,
            downloadSpeedKbps: pkg.downloadSpeedKbps,
            maxDevices: pkg.maxDevices,
            simultaneousConnections: pkg.simultaneousConnections,
            validityPeriodDays: pkg.validityPeriodDays,
            isFeatured: pkg.isFeatured,
            gracePeriodMinutes: pkg.gracePeriodMinutes,
            fairUsagePolicyText: pkg.fairUsagePolicyText,
            autoRenew: pkg.autoRenew,
            taxRate: pkg.taxRate,
            taxInclusive: pkg.taxInclusive,
            sortOrder: pkg.sortOrder,
        };
    }
    async initiatePayment(dto, clientIp) {
        this.checkRateLimit(clientIp);
        const phoneNumber = (0, phone_util_1.formatForMpesa)(dto.phoneNumber);
        if (!(0, phone_util_1.validateKenyanPhone)(phoneNumber)) {
            throw new common_1.BadRequestException('Invalid Kenyan phone number');
        }
        let routerId = dto.routerId || undefined;
        let businessId;
        if (!routerId) {
            const identified = await this.identifyRouter(dto.macAddress, dto.ipAddress, dto.routerId, dto.accessPointId);
            if (identified.router) {
                routerId = identified.router.id;
                businessId = identified.router.businessId;
            }
            else {
                throw new common_1.BadRequestException('Could not identify router. Please provide a routerId or connect to a known WiFi network.');
            }
        }
        else {
            const router = await this.routerRepo.findOne({ where: { id: routerId, isActive: true } });
            if (!router) {
                throw new common_1.NotFoundException(`Router ${routerId} not found`);
            }
            businessId = router.businessId;
        }
        const pkg = await this.packageRepo.findOne({
            where: { id: dto.packageId, isActive: true, businessId },
        });
        if (!pkg) {
            throw new common_1.NotFoundException(`Package ${dto.packageId} not found or not available for this business`);
        }
        let customer = await this.customerRepo.findOne({
            where: { phoneNumber, businessId },
        });
        if (!customer) {
            customer = this.customerRepo.create({
                phoneNumber,
                businessId,
            });
            customer = await this.customerRepo.save(customer);
        }
        if (dto.macAddress) {
            let device = await this.customerDeviceRepo.findOne({
                where: { customerId: customer.id, macAddress: dto.macAddress },
            });
            if (!device) {
                device = this.customerDeviceRepo.create({
                    customerId: customer.id,
                    macAddress: dto.macAddress,
                    ipAddress: dto.ipAddress || undefined,
                    firstSeenAt: new Date(),
                    lastSeenAt: new Date(),
                });
                await this.customerDeviceRepo.save(device);
            }
            else {
                device.lastSeenAt = new Date();
                if (dto.ipAddress)
                    device.ipAddress = dto.ipAddress;
                await this.customerDeviceRepo.save(device);
            }
        }
        const mpesaPayload = {
            phoneNumber,
            amount: Number(pkg.priceKes),
            packageId: pkg.id,
            routerId,
            macAddress: dto.macAddress,
            ipAddress: dto.ipAddress,
            accountReference: `WiFi-${pkg.name}`,
            transactionDesc: `WiFi: ${pkg.name}`,
        };
        const result = await this.mpesaService.initiateStkPush(mpesaPayload, businessId, customer.id);
        this.logger.log(`Portal payment initiated: ${result.internalReference} for ${phoneNumber} on router ${routerId}`);
        return result;
    }
    async checkPaymentStatus(checkoutRequestId) {
        const payment = await this.paymentRepo.findOne({
            where: { checkoutRequestId },
            relations: ['package'],
        });
        if (!payment) {
            const callback = await this.mpesaCallbackRepo.findOne({
                where: { checkoutRequestId },
            });
            if (callback) {
                return {
                    status: callback.resultCode === 0 ? 'SUCCESSFUL' : 'FAILED',
                    success: callback.resultCode === 0,
                };
            }
            throw new common_1.NotFoundException(`Payment with checkout request ${checkoutRequestId} not found`);
        }
        const isSuccess = payment.status === payment_entity_1.PaymentStatus.SUCCESSFUL;
        let session;
        if (isSuccess) {
            const wifiSession = await this.wifiSessionRepo.findOne({
                where: {
                    customerId: payment.customerId,
                    packageId: payment.packageId,
                    routerId: payment.routerId,
                },
                order: { createdAt: 'DESC' },
                relations: ['package'],
            });
            if (wifiSession) {
                session = {
                    id: wifiSession.id,
                    status: wifiSession.status,
                    startTime: wifiSession.startTime,
                    expiryTime: wifiSession.expiryTime,
                    remainingDataMb: wifiSession.remainingDataMb,
                    macAddress: wifiSession.macAddress,
                    ipAddress: wifiSession.ipAddress,
                    package: wifiSession.package,
                };
            }
        }
        return {
            status: payment.status,
            success: isSuccess,
            payment: {
                id: payment.id,
                internalReference: payment.internalReference,
                mpesaReceipt: payment.mpesaReceipt,
                amount: payment.amount,
                phoneNumber: payment.phoneNumber,
                status: payment.status,
            },
            session,
        };
    }
    async getPaymentResult(internalReference) {
        const payment = await this.paymentRepo.findOne({
            where: { internalReference },
            relations: ['package'],
        });
        if (!payment) {
            throw new common_1.NotFoundException(`Payment reference ${internalReference} not found`);
        }
        const session = await this.wifiSessionRepo.findOne({
            where: {
                customerId: payment.customerId,
                packageId: payment.packageId,
                businessId: payment.businessId,
            },
            order: { createdAt: 'DESC' },
        });
        return {
            packageName: payment.package?.name || 'N/A',
            amount: Number(payment.amount),
            mpesaReceipt: payment.mpesaReceipt || null,
            startTime: session?.startTime || null,
            expiryTime: session?.expiryTime || null,
            remainingDataMb: session?.remainingDataMb ? Number(session.remainingDataMb) : null,
            connectedDeviceMac: session?.macAddress || null,
            connectedDeviceIp: session?.ipAddress || null,
        };
    }
    async redeemVoucher(dto) {
        let businessId;
        if (dto.routerId) {
            const router = await this.routerRepo.findOne({
                where: { id: dto.routerId, isActive: true },
            });
            if (!router) {
                throw new common_1.NotFoundException(`Router ${dto.routerId} not found`);
            }
            businessId = router.businessId;
        }
        else {
            const identified = await this.identifyRouter(dto.macAddress, dto.ipAddress, dto.routerId);
            if (identified.router) {
                businessId = identified.router.businessId;
            }
            else {
                const voucher = await this.voucherRepo.findOne({
                    where: { code: dto.code },
                });
                if (!voucher) {
                    throw new common_1.NotFoundException(`Voucher ${dto.code} not found`);
                }
                businessId = voucher.businessId;
            }
        }
        const redeemDto = {
            code: dto.code,
            macAddress: dto.macAddress || undefined,
            ipAddress: dto.ipAddress || undefined,
        };
        const result = await this.vouchersService.redeem(redeemDto, businessId);
        this.logger.log(`Portal voucher ${dto.code} redeemed for business ${businessId}`);
        return result;
    }
    async getCustomerSession(macAddress, ipAddress) {
        if (!macAddress && !ipAddress) {
            return { hasActiveSession: false, session: null };
        }
        const whereConditions = [];
        if (macAddress) {
            whereConditions.push({
                macAddress,
                status: (0, typeorm_2.In)([wifi_session_entity_1.WifiSessionStatus.ACTIVE, wifi_session_entity_1.WifiSessionStatus.PAUSED]),
            });
        }
        if (ipAddress) {
            whereConditions.push({
                ipAddress,
                status: (0, typeorm_2.In)([wifi_session_entity_1.WifiSessionStatus.ACTIVE, wifi_session_entity_1.WifiSessionStatus.PAUSED]),
            });
        }
        if (whereConditions.length === 0) {
            return { hasActiveSession: false, session: null };
        }
        const session = await this.wifiSessionRepo.findOne({
            where: whereConditions,
            relations: ['package', 'router', 'customer'],
            order: { createdAt: 'DESC' },
        });
        if (!session) {
            return { hasActiveSession: false, session: null };
        }
        return {
            hasActiveSession: true,
            session: {
                id: session.id,
                status: session.status,
                startTime: session.startTime,
                expiryTime: session.expiryTime,
                remainingDataMb: session.remainingDataMb,
                macAddress: session.macAddress,
                ipAddress: session.ipAddress,
                package: session.package,
                router: session.router,
            },
        };
    }
    async loginCustomer(phoneNumber) {
        const normalized = (0, phone_util_1.formatForMpesa)(phoneNumber);
        if (!(0, phone_util_1.validateKenyanPhone)(normalized)) {
            throw new common_1.BadRequestException('Invalid Kenyan phone number');
        }
        const otp = Math.floor(100000 + Math.random() * 900000).toString();
        const sessionId = (0, id_generator_util_1.generateSessionKey)();
        this.otpStore.set(sessionId, {
            otp,
            phoneNumber: normalized,
            expiresAt: Date.now() + 5 * 60 * 1000,
        });
        try {
            await this.notificationsService.sendSms(normalized, `Your HBS WiFi login code is: ${otp}. Valid for 5 minutes.`);
        }
        catch (error) {
            this.logger.warn(`Failed to send OTP SMS to ${normalized}: ${error.message}`);
        }
        this.logger.log(`OTP generated for ${normalized}, session ${sessionId}`);
        return {
            sessionId,
            message: 'Verification code sent to your phone',
        };
    }
    async verifyOtp(phoneNumber, otp, sessionId) {
        const normalized = (0, phone_util_1.formatForMpesa)(phoneNumber);
        const storedOtp = this.otpStore.get(sessionId);
        if (!storedOtp) {
            throw new common_1.BadRequestException('OTP session expired or invalid. Please request a new code.');
        }
        if (storedOtp.phoneNumber !== normalized) {
            throw new common_1.BadRequestException('Phone number does not match the OTP session.');
        }
        if (Date.now() > storedOtp.expiresAt) {
            this.otpStore.delete(sessionId);
            throw new common_1.BadRequestException('OTP has expired. Please request a new code.');
        }
        if (storedOtp.otp !== otp) {
            throw new common_1.BadRequestException('Invalid OTP code.');
        }
        this.otpStore.delete(sessionId);
        const businesses = await this.businessRepo.find({
            where: { isActive: true, isSuspended: false },
            take: 1,
        });
        let customer = null;
        for (const business of businesses) {
            customer = await this.customerRepo.findOne({
                where: { phoneNumber: normalized, businessId: business.id },
            });
            if (customer)
                break;
        }
        if (!customer && businesses.length > 0) {
            customer = this.customerRepo.create({
                phoneNumber: normalized,
                businessId: businesses[0].id,
            });
            customer = await this.customerRepo.save(customer);
        }
        const token = (0, id_generator_util_1.generateSessionKey)();
        this.logger.log(`Customer ${normalized} verified, token generated`);
        return {
            verified: true,
            customer: customer
                ? {
                    id: customer.id,
                    phoneNumber: customer.phoneNumber,
                    firstName: customer.firstName,
                    lastName: customer.lastName,
                    businessId: customer.businessId,
                }
                : null,
            token: customer ? token : null,
        };
    }
    async detectDevice(macAddress, ipAddress) {
        const result = await this.identifyRouter(macAddress, ipAddress);
        const pendingPayments = 0;
        let activeSession = null;
        if (macAddress || ipAddress) {
            const sessionResult = await this.getCustomerSession(macAddress || '', ipAddress || '');
            activeSession = sessionResult.session;
        }
        return {
            identified: result.identified,
            router: result.router
                ? {
                    id: result.router.id,
                    name: result.router.name,
                    businessId: result.router.businessId,
                    locationId: result.router.locationId,
                    isOnline: result.router.isOnline,
                }
                : null,
            pendingPayments,
            activeSession,
        };
    }
    checkRateLimit(clientIp) {
        const now = Date.now();
        const entry = this.rateLimitMap.get(clientIp);
        if (!entry || now > entry.resetAt) {
            this.rateLimitMap.set(clientIp, { count: 1, resetAt: now + 60_000 });
            return;
        }
        if (entry.count >= 3) {
            throw new common_1.BadRequestException('Too many payment attempts. Please wait a moment before trying again.');
        }
        entry.count++;
    }
    cleanupRateLimits() {
        const now = Date.now();
        for (const [key, entry] of this.rateLimitMap.entries()) {
            if (now > entry.resetAt) {
                this.rateLimitMap.delete(key);
            }
        }
    }
    cleanupOtps() {
        const now = Date.now();
        for (const [key, entry] of this.otpStore.entries()) {
            if (now > entry.expiresAt) {
                this.otpStore.delete(key);
            }
        }
    }
    async getBusinessSetting(businessId, key, defaultValue) {
        try {
            const setting = await this.businessSettingsRepo.findOne({
                where: { businessId, key },
            });
            if (setting)
                return setting.value;
        }
        catch { }
        try {
            const sysSetting = await this.systemSettingsRepo.findOne({
                where: { key },
            });
            if (sysSetting)
                return sysSetting.value;
        }
        catch { }
        return defaultValue;
    }
    async getBusinessSettingJson(businessId, key, defaultValue) {
        try {
            const setting = await this.businessSettingsRepo.findOne({
                where: { businessId, key },
            });
            if (setting) {
                try {
                    return JSON.parse(setting.value);
                }
                catch {
                    return defaultValue;
                }
            }
        }
        catch { }
        try {
            const sysSetting = await this.systemSettingsRepo.findOne({
                where: { key },
            });
            if (sysSetting) {
                try {
                    return JSON.parse(sysSetting.value);
                }
                catch {
                    return defaultValue;
                }
            }
        }
        catch { }
        return defaultValue;
    }
};
exports.CaptiveService = CaptiveService;
exports.CaptiveService = CaptiveService = CaptiveService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, typeorm_1.InjectRepository)(business_entity_1.Business)),
    __param(1, (0, typeorm_1.InjectRepository)(business_settings_entity_1.BusinessSettings)),
    __param(2, (0, typeorm_1.InjectRepository)(system_settings_entity_1.SystemSettings)),
    __param(3, (0, typeorm_1.InjectRepository)(package_entity_1.Package)),
    __param(4, (0, typeorm_1.InjectRepository)(package_router_assignment_entity_1.PackageRouterAssignment)),
    __param(5, (0, typeorm_1.InjectRepository)(router_entity_1.Router)),
    __param(6, (0, typeorm_1.InjectRepository)(customer_entity_1.Customer)),
    __param(7, (0, typeorm_1.InjectRepository)(customer_device_entity_1.CustomerDevice)),
    __param(8, (0, typeorm_1.InjectRepository)(wifi_session_entity_1.WifiSession)),
    __param(9, (0, typeorm_1.InjectRepository)(payment_entity_1.Payment)),
    __param(10, (0, typeorm_1.InjectRepository)(mpesa_callback_entity_1.MpesaCallback)),
    __param(11, (0, typeorm_1.InjectRepository)(voucher_entity_1.Voucher)),
    __metadata("design:paramtypes", [typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        mpesa_service_1.MpesaService,
        vouchers_service_1.VouchersService,
        notifications_service_1.NotificationsService])
], CaptiveService);
