import PushDeviceType from "../../Types/PushNotification/PushDeviceType";
import logger from "../Utils/Logger";
import UserPushService from "./UserPushService";
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
import UserNotificationStatus from "../../Types/UserNotification/UserNotificationStatus";
import { VapidPublicKey, VapidPrivateKey, VapidSubject, ExpoAccessToken, PushNotificationRelayUrl, } from "../EnvironmentConfig";
import webpush from "web-push";
import { Expo } from "expo-server-sdk";
import API from "../../Utils/API";
import URL from "../../Types/API/URL";
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
import PushNotificationUtil from "../Utils/PushNotificationUtil";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import PushNotificationLog from "../../Models/DatabaseModels/PushNotificationLog";
import PushNotificationLogService from "./PushNotificationLogService";
import PushStatus from "../../Types/PushNotification/PushStatus";
class PushNotificationService {
    static initializeWebPush() {
        if (this.isWebPushInitialized) {
            return;
        }
        if (!VapidPublicKey || !VapidPrivateKey) {
            logger.warn("VAPID keys not configured. Web push notifications will not work.");
            logger.warn(`VapidPublicKey present: ${Boolean(VapidPublicKey)}`);
            logger.warn(`VapidPrivateKey present: ${Boolean(VapidPrivateKey)}`);
            logger.warn(`VapidSubject: ${VapidSubject}`);
            return;
        }
        logger.info(`Initializing web push with VAPID subject: ${VapidSubject}`);
        webpush.setVapidDetails(VapidSubject, VapidPublicKey, VapidPrivateKey);
        this.isWebPushInitialized = true;
        logger.info("Web push notifications initialized successfully");
    }
    static async sendPushNotification(request, options = {}) {
        var _a, _b, _c, _d;
        logger.info(`Sending push notification to ${(_a = request.devices) === null || _a === void 0 ? void 0 : _a.length} devices`);
        if (!request.devices || request.devices.length === 0) {
            logger.error("No devices provided for push notification");
            throw new Error("No devices provided");
        }
        logger.info(`Sending ${request.deviceType} push notifications to ${request.devices.length} devices`);
        logger.info(`Notification message: ${JSON.stringify(request.message)}`);
        const deviceNames = request.devices
            .map((device) => {
            return device.name;
        })
            .filter(Boolean);
        if (deviceNames.length > 0) {
            logger.info(`Device names: ${deviceNames.join(", ")}`);
        }
        const promises = [];
        for (const device of request.devices) {
            if (request.deviceType === PushDeviceType.Web) {
                promises.push(this.sendWebPushNotification(device.token, request.message, options));
            }
            else if (request.deviceType === PushDeviceType.iOS ||
                request.deviceType === PushDeviceType.Android) {
                promises.push(this.sendExpoPushNotification(device.token, request.message, request.deviceType, options));
            }
            else {
                logger.error(`Unsupported device type: ${request.deviceType}`);
            }
        }
        const results = await Promise.allSettled(promises);
        let successCount = 0;
        let errorCount = 0;
        results.forEach((result, index) => {
            const device = request.devices[index];
            const deviceInfo = (device === null || device === void 0 ? void 0 : device.name)
                ? `device "${device.name}" (${index + 1})`
                : `device ${index + 1}`;
            if (result.status === "fulfilled") {
                successCount++;
                logger.info(`${deviceInfo}: Notification sent successfully`);
            }
            else {
                errorCount++;
                logger.error(`Failed to send notification to ${deviceInfo}: ${result.reason}`);
            }
        });
        logger.info(`Push notification results: ${successCount} successful, ${errorCount} failed`);
        // Create one push log per device if projectId provided
        if (options.projectId) {
            for (let i = 0; i < results.length; i++) {
                const result = results[i];
                const device = request.devices[i];
                const log = new PushNotificationLog();
                log.projectId = options.projectId;
                log.title = request.message.title || "";
                log.body = options.isSensitive
                    ? "Sensitive message not logged"
                    : request.message.body || "";
                log.deviceType = request.deviceType;
                // Set device name if available
                if (device === null || device === void 0 ? void 0 : device.name) {
                    log.deviceName = device.name;
                }
                // relations if provided
                if (options.incidentId) {
                    log.incidentId = options.incidentId;
                }
                if (options.alertId) {
                    log.alertId = options.alertId;
                }
                if (options.monitorId) {
                    log.monitorId = options.monitorId;
                }
                if (options.scheduledMaintenanceId) {
                    log.scheduledMaintenanceId = options.scheduledMaintenanceId;
                }
                if (options.statusPageId) {
                    log.statusPageId = options.statusPageId;
                }
                if (options.statusPageAnnouncementId) {
                    log.statusPageAnnouncementId = options.statusPageAnnouncementId;
                }
                if (options.userId) {
                    log.userId = options.userId;
                }
                if (options.teamId) {
                    log.teamId = options.teamId;
                }
                // Set OnCall-related fields
                if (options.onCallPolicyId) {
                    log.onCallDutyPolicyId = options.onCallPolicyId;
                }
                if (options.onCallPolicyEscalationRuleId) {
                    log.onCallDutyPolicyEscalationRuleId =
                        options.onCallPolicyEscalationRuleId;
                }
                if (options.onCallScheduleId) {
                    log.onCallDutyPolicyScheduleId = options.onCallScheduleId;
                }
                if (result.status === "fulfilled") {
                    log.status = PushStatus.Success;
                    log.statusMessage = "Push notification sent";
                }
                else {
                    log.status = PushStatus.Error;
                    const reason = (result &&
                        (((_b = result.reason) === null || _b === void 0 ? void 0 : _b.message) || ((_d = (_c = result.reason) === null || _c === void 0 ? void 0 : _c.toString) === null || _d === void 0 ? void 0 : _d.call(_c)))) ||
                        `Failed to send push notification`;
                    log.statusMessage = reason;
                }
                await PushNotificationLogService.create({
                    data: log,
                    props: { isRoot: true },
                });
            }
        }
        // Update user on call log timeline status if provided
        if (options.userOnCallLogTimelineId) {
            const status = successCount > 0
                ? UserNotificationStatus.Sent
                : UserNotificationStatus.Error;
            const statusMessage = successCount > 0
                ? "Push notification sent successfully"
                : `Failed to send push notification: ${errorCount} errors`;
            await UserOnCallLogTimelineService.updateOneById({
                id: options.userOnCallLogTimelineId,
                data: {
                    status,
                    statusMessage,
                },
                props: {
                    isRoot: true,
                },
            });
        }
        if (errorCount > 0 && successCount === 0) {
            throw new Error(`Failed to send push notification to all ${errorCount} devices`);
        }
    }
    static async sendWebPushNotification(deviceToken, message, _options) {
        if (!this.isWebPushInitialized) {
            this.initializeWebPush();
        }
        if (!this.isWebPushInitialized) {
            throw new Error("Web push notifications not configured");
        }
        try {
            const payload = JSON.stringify({
                title: message.title,
                body: message.body,
                icon: message.icon || PushNotificationUtil.DEFAULT_ICON,
                badge: message.badge || PushNotificationUtil.DEFAULT_BADGE,
                data: message.data || {},
                tag: message.tag || "oneuptime-notification",
                requireInteraction: message.requireInteraction || false,
                actions: message.actions || [],
                url: message.url || message.clickAction,
            });
            logger.debug(`Sending push notification with payload: ${payload}`);
            logger.debug(`Device token: ${deviceToken}`);
            let subscriptionObject;
            try {
                subscriptionObject = JSON.parse(deviceToken);
                logger.debug(`Parsed subscription object: ${JSON.stringify(subscriptionObject)}`);
            }
            catch (parseError) {
                logger.error(`Failed to parse device token: ${parseError}`);
                throw new Error(`Invalid device token format: ${parseError}`);
            }
            const result = await webpush.sendNotification(subscriptionObject, payload, {
                TTL: 24 * 60 * 60, // 24 hours
            });
            logger.debug(`Web push notification sent successfully:`);
            logger.debug(`Result: ${JSON.stringify(result, null, 2)}`);
            logger.debug(`Payload: ${JSON.stringify(payload, null, 2)}`);
            logger.debug(`Subscription object: ${JSON.stringify(subscriptionObject, null, 2)}`);
            logger.info(`Web push notification sent successfully`);
        }
        catch (error) {
            logger.error(`Failed to send web push notification: ${error.message}`);
            logger.error(error);
            // If the subscription is no longer valid, remove it
            if (error.statusCode === 410 || error.statusCode === 404) {
                logger.info("Removing invalid web push subscription");
                // You would implement removal logic here
            }
            throw error;
        }
    }
    static async sendExpoPushNotification(expoPushToken, message, deviceType, _options) {
        var _a;
        if (!Expo.isExpoPushToken(expoPushToken)) {
            throw new Error(`Invalid Expo push token for ${deviceType} device: ${expoPushToken}`);
        }
        const dataPayload = {};
        if (message.data) {
            for (const key of Object.keys(message.data)) {
                dataPayload[key] = String(message.data[key]);
            }
        }
        if (message.url || message.clickAction) {
            dataPayload["url"] = message.url || message.clickAction || "";
        }
        const channelId = deviceType === PushDeviceType.Android ? "oncall_high" : "default";
        // If EXPO_ACCESS_TOKEN is not set, relay through the push notification gateway
        if (!ExpoAccessToken) {
            await this.sendViaRelay(expoPushToken, message, dataPayload, channelId, deviceType);
            return;
        }
        // Send directly via Expo SDK
        try {
            const expoPushMessage = {
                to: expoPushToken,
                title: message.title,
                body: message.body,
                data: dataPayload,
                sound: "default",
                priority: "high",
                channelId: channelId,
            };
            const tickets = await this.expoClient.sendPushNotificationsAsync([expoPushMessage]);
            const ticket = tickets[0];
            if (ticket && ticket.status === "error") {
                const errorTicket = ticket;
                logger.error(`Expo push notification error for ${deviceType} device: ${errorTicket.message}`);
                if (((_a = errorTicket.details) === null || _a === void 0 ? void 0 : _a.error) === "DeviceNotRegistered") {
                    logger.info("Expo push token is no longer valid (DeviceNotRegistered)");
                }
                throw new Error(`Expo push notification failed: ${errorTicket.message}`);
            }
            logger.info(`Expo push notification sent successfully to ${deviceType} device`);
        }
        catch (error) {
            logger.error(`Failed to send Expo push notification to ${deviceType} device: ${error.message}`);
            throw error;
        }
    }
    static async sendViaRelay(expoPushToken, message, dataPayload, channelId, deviceType) {
        logger.info(`Sending ${deviceType} push notification via relay: ${PushNotificationRelayUrl}`);
        try {
            const response = await API.post({
                url: URL.fromString(PushNotificationRelayUrl),
                data: {
                    to: expoPushToken,
                    title: message.title || "",
                    body: message.body || "",
                    data: dataPayload,
                    sound: "default",
                    priority: "high",
                    channelId: channelId,
                },
            });
            if (response instanceof HTTPErrorResponse) {
                throw new Error(`Push relay error: ${JSON.stringify(response.jsonData)}`);
            }
            logger.info(`Push notification sent via relay successfully to ${deviceType} device`);
        }
        catch (error) {
            logger.error(`Failed to send push notification via relay to ${deviceType} device: ${error.message}`);
            throw error;
        }
    }
    static isValidExpoPushToken(token) {
        return Expo.isExpoPushToken(token);
    }
    static hasExpoAccessToken() {
        return Boolean(ExpoAccessToken);
    }
    static async sendRelayPushNotification(data) {
        if (!ExpoAccessToken) {
            throw new Error("Push relay is not configured. EXPO_ACCESS_TOKEN is not set on this server.");
        }
        const expoPushMessage = {
            to: data.to,
            title: data.title || "",
            body: data.body || "",
            data: data.data || {},
            sound: data.sound || "default",
            priority: data.priority || "high",
            channelId: data.channelId || "default",
        };
        const tickets = await this.expoClient.sendPushNotificationsAsync([expoPushMessage]);
        const ticket = tickets[0];
        if (ticket && ticket.status === "error") {
            const errorTicket = ticket;
            logger.error(`Push relay: Expo push notification error: ${errorTicket.message}`);
            throw new Error(`Failed to send push notification: ${errorTicket.message}`);
        }
        logger.info(`Push relay: notification sent successfully to ${data.to}`);
    }
    static async sendPushNotificationToUser(userId, projectId, message, options = {}) {
        // Get all verified push devices for the user
        const userPushDevices = await UserPushService.findBy({
            query: {
                userId: userId,
                projectId: projectId,
                isVerified: true,
            },
            select: {
                deviceToken: true,
                deviceType: true,
                deviceName: true,
                _id: true,
            },
            limit: LIMIT_PER_PROJECT,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        if (userPushDevices.length === 0) {
            logger.info(`No verified push devices found for user ${userId.toString()}`);
            return;
        }
        // Group devices by type
        const devicesByType = new Map();
        for (const device of userPushDevices) {
            const type = device.deviceType || PushDeviceType.Web;
            if (!devicesByType.has(type)) {
                devicesByType.set(type, []);
            }
            devicesByType.get(type).push({
                token: device.deviceToken,
                name: device.deviceName || "Unknown Device",
            });
        }
        // Send notifications to each device type group
        const sendPromises = [];
        for (const [deviceType, devices] of devicesByType.entries()) {
            if (devices.length > 0) {
                sendPromises.push(this.sendPushNotification({
                    devices: devices,
                    message: message,
                    deviceType: deviceType,
                }, options));
            }
        }
        await Promise.allSettled(sendPromises);
    }
}
PushNotificationService.isWebPushInitialized = false;
PushNotificationService.expoClient = new Expo(ExpoAccessToken ? { accessToken: ExpoAccessToken } : undefined);
export default PushNotificationService;
//# sourceMappingURL=PushNotificationService.js.map