import"../chunk-v0bahtg2.js";

// src/push/apns.ts
import * as http2 from "node:http2";
import { createSign } from "node:crypto";
var APNS_PRODUCTION_HOST = "api.push.apple.com";
var APNS_SANDBOX_HOST = "api.sandbox.push.apple.com";
var TOKEN_EXPIRY_MS = 45 * 60 * 1000;

class APNsClient {
  config;
  token = null;
  tokenGeneratedAt = 0;
  client = null;
  host;
  constructor(config) {
    this.config = config;
    this.host = config.production ? APNS_PRODUCTION_HOST : APNS_SANDBOX_HOST;
  }
  generateToken() {
    const now = Math.floor(Date.now() / 1000);
    if (this.token && Date.now() - this.tokenGeneratedAt < TOKEN_EXPIRY_MS) {
      return this.token;
    }
    const header = {
      alg: "ES256",
      kid: this.config.keyId
    };
    const payload = {
      iss: this.config.teamId,
      iat: now
    };
    const encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url");
    const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
    const signatureInput = `${encodedHeader}.${encodedPayload}`;
    const sign = createSign("SHA256");
    sign.update(signatureInput);
    const signature = sign.sign(this.config.privateKey);
    const rawSignature = this.derToRaw(signature);
    const encodedSignature = rawSignature.toString("base64url");
    this.token = `${signatureInput}.${encodedSignature}`;
    this.tokenGeneratedAt = Date.now();
    return this.token;
  }
  derToRaw(derSignature) {
    let offset = 2;
    if (derSignature[offset] !== 2) {
      throw new Error("Invalid DER signature: expected 0x02 for R");
    }
    offset++;
    const rLength = derSignature[offset];
    offset++;
    let r = derSignature.subarray(offset, offset + rLength);
    offset += rLength;
    if (derSignature[offset] !== 2) {
      throw new Error("Invalid DER signature: expected 0x02 for S");
    }
    offset++;
    const sLength = derSignature[offset];
    offset++;
    let s = derSignature.subarray(offset, offset + sLength);
    if (r[0] === 0 && r.length === 33) {
      r = r.subarray(1);
    }
    if (s[0] === 0 && s.length === 33) {
      s = s.subarray(1);
    }
    const result = Buffer.alloc(64);
    r.copy(result, 32 - r.length);
    s.copy(result, 64 - s.length);
    return result;
  }
  async getClient() {
    if (this.client && !this.client.destroyed) {
      return this.client;
    }
    return new Promise((resolve, reject) => {
      this.client = http2.connect(`https://${this.host}`);
      this.client.on("error", (err) => {
        reject(err);
      });
      this.client.on("connect", () => {
        resolve(this.client);
      });
      this.client.on("close", () => {
        this.client = null;
      });
    });
  }
  buildPayload(notification) {
    const aps = {};
    if (notification.title || notification.body || notification.subtitle) {
      aps.alert = {};
      if (notification.title)
        aps.alert.title = notification.title;
      if (notification.subtitle)
        aps.alert.subtitle = notification.subtitle;
      if (notification.body)
        aps.alert.body = notification.body;
    }
    if (notification.badge !== undefined) {
      aps.badge = notification.badge;
    }
    if (notification.sound !== undefined) {
      aps.sound = notification.sound;
    }
    if (notification.category) {
      aps.category = notification.category;
    }
    if (notification.threadId) {
      aps["thread-id"] = notification.threadId;
    }
    if (notification.contentAvailable) {
      aps["content-available"] = 1;
    }
    if (notification.mutableContent) {
      aps["mutable-content"] = 1;
    }
    if (notification.targetContentId) {
      aps["target-content-id"] = notification.targetContentId;
    }
    const payload = { aps };
    if (notification.data) {
      Object.assign(payload, notification.data);
    }
    return payload;
  }
  async send(notification) {
    try {
      const client = await this.getClient();
      const token = this.generateToken();
      const payload = JSON.stringify(this.buildPayload(notification));
      const headers = {
        ":method": "POST",
        ":path": `/3/device/${notification.deviceToken}`,
        authorization: `bearer ${token}`,
        "apns-topic": this.config.bundleId,
        "apns-push-type": notification.pushType || "alert",
        "apns-priority": String(notification.priority || 10)
      };
      if (notification.expiration !== undefined) {
        headers["apns-expiration"] = String(notification.expiration);
      }
      if (notification.collapseId) {
        headers["apns-collapse-id"] = notification.collapseId;
      }
      return new Promise((resolve) => {
        const req = client.request(headers);
        let responseData = "";
        let statusCode = 0;
        let apnsId;
        req.on("response", (headers2) => {
          statusCode = headers2[":status"];
          apnsId = headers2["apns-id"];
        });
        req.on("data", (chunk) => {
          responseData += chunk.toString();
        });
        req.on("end", () => {
          if (statusCode === 200) {
            resolve({
              success: true,
              deviceToken: notification.deviceToken,
              apnsId,
              statusCode
            });
          } else {
            let error = "Unknown error";
            let reason;
            let timestamp;
            if (responseData) {
              try {
                const parsed = JSON.parse(responseData);
                reason = parsed.reason;
                timestamp = parsed.timestamp;
                error = reason || error;
              } catch {
                error = responseData;
              }
            }
            resolve({
              success: false,
              deviceToken: notification.deviceToken,
              apnsId,
              statusCode,
              error,
              reason,
              timestamp
            });
          }
        });
        req.on("error", (err) => {
          resolve({
            success: false,
            deviceToken: notification.deviceToken,
            error: err.message
          });
        });
        req.write(payload);
        req.end();
      });
    } catch (error) {
      return {
        success: false,
        deviceToken: notification.deviceToken,
        error: error.message
      };
    }
  }
  async sendBatch(notifications, options) {
    const concurrency = options?.concurrency || 10;
    const results = [];
    for (let i = 0;i < notifications.length; i += concurrency) {
      const batch = notifications.slice(i, i + concurrency);
      const batchResults = await Promise.all(batch.map((n) => this.send(n)));
      results.push(...batchResults);
    }
    return {
      sent: results.filter((r) => r.success).length,
      failed: results.filter((r) => !r.success).length,
      results
    };
  }
  async sendSimple(deviceToken, title, body, data) {
    return this.send({
      deviceToken,
      title,
      body,
      data
    });
  }
  async sendSilent(deviceToken, data) {
    return this.send({
      deviceToken,
      contentAvailable: true,
      pushType: "background",
      priority: 5,
      data
    });
  }
  close() {
    if (this.client) {
      this.client.close();
      this.client = null;
    }
    this.token = null;
    this.tokenGeneratedAt = 0;
  }
}
// src/push/fcm.ts
import { createSign as createSign2 } from "node:crypto";
var FCM_API_URL = "https://fcm.googleapis.com/v1/projects";
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
var TOKEN_EXPIRY_MS2 = 55 * 60 * 1000;

class FCMClient {
  config;
  accessToken = null;
  tokenExpiresAt = 0;
  constructor(config) {
    this.config = config;
  }
  static fromServiceAccount(serviceAccount) {
    return new FCMClient({
      projectId: serviceAccount.project_id,
      clientEmail: serviceAccount.client_email,
      privateKey: serviceAccount.private_key
    });
  }
  generateJWT() {
    const now = Math.floor(Date.now() / 1000);
    const exp = now + 3600;
    const header = {
      alg: "RS256",
      typ: "JWT"
    };
    const payload = {
      iss: this.config.clientEmail,
      sub: this.config.clientEmail,
      aud: GOOGLE_TOKEN_URL,
      iat: now,
      exp,
      scope: "https://www.googleapis.com/auth/firebase.messaging"
    };
    const encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url");
    const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
    const signatureInput = `${encodedHeader}.${encodedPayload}`;
    const sign = createSign2("SHA256");
    sign.update(signatureInput);
    const signature = sign.sign(this.config.privateKey, "base64url");
    return `${signatureInput}.${signature}`;
  }
  async getAccessToken() {
    if (this.accessToken && Date.now() < this.tokenExpiresAt) {
      return this.accessToken;
    }
    const jwt = this.generateJWT();
    const response = await fetch(GOOGLE_TOKEN_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body: new URLSearchParams({
        grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
        assertion: jwt
      }).toString()
    });
    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Failed to get access token: ${errorText}`);
    }
    const data = await response.json();
    this.accessToken = data.access_token;
    this.tokenExpiresAt = Date.now() + TOKEN_EXPIRY_MS2;
    return this.accessToken;
  }
  buildMessage(notification) {
    const message = {};
    if (notification.token) {
      message.token = notification.token;
    } else if (notification.topic) {
      message.topic = notification.topic;
    } else if (notification.condition) {
      message.condition = notification.condition;
    }
    if (notification.title || notification.body || notification.imageUrl) {
      message.notification = {};
      if (notification.title)
        message.notification.title = notification.title;
      if (notification.body)
        message.notification.body = notification.body;
      if (notification.imageUrl)
        message.notification.image = notification.imageUrl;
    }
    if (notification.data && Object.keys(notification.data).length > 0) {
      message.data = notification.data;
    }
    if (notification.android) {
      message.android = {
        priority: notification.android.priority || "high"
      };
      if (notification.android.ttl) {
        message.android.ttl = `${notification.android.ttl}s`;
      }
      if (notification.android.collapseKey) {
        message.android.collapse_key = notification.android.collapseKey;
      }
      if (notification.android.directBootOk) {
        message.android.direct_boot_ok = notification.android.directBootOk;
      }
      const androidNotification = {};
      if (notification.android.channelId)
        androidNotification.channel_id = notification.android.channelId;
      if (notification.android.icon)
        androidNotification.icon = notification.android.icon;
      if (notification.android.color)
        androidNotification.color = notification.android.color;
      if (notification.android.sound)
        androidNotification.sound = notification.android.sound;
      if (notification.android.clickAction)
        androidNotification.click_action = notification.android.clickAction;
      if (notification.android.tag)
        androidNotification.tag = notification.android.tag;
      if (notification.android.visibility)
        androidNotification.visibility = notification.android.visibility;
      if (notification.android.notificationCount !== undefined) {
        androidNotification.notification_count = notification.android.notificationCount;
      }
      if (Object.keys(androidNotification).length > 0) {
        message.android.notification = androidNotification;
      }
    }
    if (notification.webpush) {
      message.webpush = notification.webpush;
    }
    if (notification.apns) {
      message.apns = notification.apns;
    }
    if (notification.fcmOptions) {
      message.fcm_options = notification.fcmOptions;
    }
    return { message };
  }
  async send(notification) {
    try {
      const accessToken = await this.getAccessToken();
      const payload = this.buildMessage(notification);
      const response = await fetch(`${FCM_API_URL}/${this.config.projectId}/messages:send`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify(payload)
      });
      const data = await response.json();
      if (response.ok) {
        return {
          success: true,
          messageId: data.name
        };
      } else {
        return {
          success: false,
          error: data.error?.message || "Unknown error",
          errorCode: data.error?.status
        };
      }
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }
  async sendBatch(tokens, notification, options) {
    const concurrency = options?.concurrency || 10;
    const results = [];
    for (let i = 0;i < tokens.length; i += concurrency) {
      const batch = tokens.slice(i, i + concurrency);
      const batchPromises = batch.map(async (token) => {
        const result = await this.send({ ...notification, token });
        return { ...result, token };
      });
      const batchResults = await Promise.all(batchPromises);
      results.push(...batchResults);
    }
    return {
      sent: results.filter((r) => r.success).length,
      failed: results.filter((r) => !r.success).length,
      results
    };
  }
  async sendToTopic(topic, notification) {
    return this.send({ ...notification, topic });
  }
  async sendToCondition(condition, notification) {
    return this.send({ ...notification, condition });
  }
  async sendSimple(token, title, body, data) {
    return this.send({
      token,
      title,
      body,
      data
    });
  }
  async sendSilent(token, data) {
    return this.send({
      token,
      data,
      android: {
        priority: "high"
      }
    });
  }
  async subscribeToTopic(tokens, topic) {
    try {
      const accessToken = await this.getAccessToken();
      const response = await fetch(`https://iid.googleapis.com/iid/v1:batchAdd`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          to: `/topics/${topic}`,
          registration_tokens: tokens
        })
      });
      if (response.ok) {
        return { success: true };
      } else {
        const data = await response.json();
        return { success: false, error: data.error?.message || "Failed to subscribe" };
      }
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
  async unsubscribeFromTopic(tokens, topic) {
    try {
      const accessToken = await this.getAccessToken();
      const response = await fetch(`https://iid.googleapis.com/iid/v1:batchRemove`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          to: `/topics/${topic}`,
          registration_tokens: tokens
        })
      });
      if (response.ok) {
        return { success: true };
      } else {
        const data = await response.json();
        return { success: false, error: data.error?.message || "Failed to unsubscribe" };
      }
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}
export {
  FCMClient,
  APNsClient
};
