import { b as private_env } from './shared-server-DaWdgxVh.js';
import { execFileSync, execFile } from 'child_process';
import jwt from 'jsonwebtoken';
import { promisify } from 'util';
import { t as toErrorMessage } from './error-DDXB3duW.js';

const UNCONDITIONAL_STALE_REASONS = /* @__PURE__ */ new Set([
  "DeviceTokenNotForTopic",
  // token belongs to a different bundle id
  "Unregistered"
  // 410 — guarded by the re-registration timestamp check below
]);
function classifyApnsReason(httpStatus, reason, storedAppEnv, serverAppEnv) {
  if (httpStatus === 200) {
    return "sent";
  }
  if (reason === "TopicDisallowed") {
    return "transient_error";
  }
  if (reason === "BadDeviceToken") {
    return storedAppEnv === serverAppEnv ? "stale_token" : "transient_error";
  }
  if (reason && UNCONDITIONAL_STALE_REASONS.has(reason)) {
    return "stale_token";
  }
  return "transient_error";
}
function summarizeApnsFanOut(outcomes, serverAppEnv) {
  const results = [];
  const staleTokens = [];
  let totalSent = 0;
  let totalFailed = 0;
  for (const o of outcomes) {
    let disposition = classifyApnsReason(o.httpStatus, o.reason, o.appEnv, serverAppEnv);
    if (disposition === "stale_token" && o.reason === "Unregistered" && o.timestampMs > 0) {
      const registeredMs = Date.parse(o.registeredAt);
      if (!Number.isNaN(registeredMs) && registeredMs > o.timestampMs) {
        disposition = "transient_error";
      }
    }
    const success = o.httpStatus === 200;
    if (success) {
      totalSent += 1;
    } else {
      totalFailed += 1;
    }
    if (disposition === "stale_token") {
      staleTokens.push(o.token);
    }
    results.push({
      disposition,
      httpStatus: o.httpStatus,
      reason: o.reason,
      success,
      timestampMs: o.timestampMs,
      token: o.token
    });
  }
  return { results, staleTokens, totalFailed, totalSent };
}
const APNS_MAX_BYTES = 3900;
const ELLIPSIS = "…";
function fitApnsPayload(body, maxBytes = APNS_MAX_BYTES) {
  if (payloadBytes(body) <= maxBytes) {
    return body;
  }
  const aps = body.aps;
  const alert = aps?.alert;
  if (!alert) {
    return body;
  }
  for (const field of ["body", "subtitle"]) {
    if (payloadBytes(body) <= maxBytes) {
      break;
    }
    const value = alert[field];
    if (typeof value !== "string" || value.length === 0) {
      continue;
    }
    let text = value;
    while (payloadBytes(body) > maxBytes && text.length > 0) {
      const cut = Math.max(1, Math.ceil(text.length * 0.12));
      text = text.slice(0, text.length - cut);
      alert[field] = text.length > 0 ? text + ELLIPSIS : void 0;
    }
  }
  return body;
}
function payloadBytes(body) {
  return Buffer.byteLength(JSON.stringify(body), "utf8");
}
const execFileAsync = promisify(execFile);
const APNS_HOST_PROD = "api.push.apple.com";
const APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
const JWT_REFRESH_INTERVAL_MS = 30 * 60 * 1e3;
const REQUEST_TIMEOUT_SECONDS = 15;
const MAX_APNS_CONCURRENCY = 20;
class LibraryAPNsService {
  bundleId;
  cachedJwt = null;
  cachedJwtAt = 0;
  configured = false;
  host = "";
  keyId;
  privateKey;
  teamId;
  constructor() {
    this.keyId = private_env.APNS_KEY_ID;
    this.teamId = private_env.APNS_TEAM_ID;
    this.bundleId = private_env.APNS_BUNDLE_ID;
    this.privateKey = private_env.APNS_KEY;
    if (!this.keyId || !this.teamId || !this.bundleId || !this.privateKey) {
      console.error(
        "[apns] Missing required configuration (APNS_KEY_ID, APNS_TEAM_ID, APNS_BUNDLE_ID, or APNS_KEY)"
      );
      this.configured = false;
      return;
    }
    const production = private_env.APNS_PRODUCTION === "true";
    this.host = production ? APNS_HOST_PROD : APNS_HOST_SANDBOX;
    try {
      execFileSync("curl", ["--version"], { stdio: "ignore" });
    } catch (error) {
      console.error(
        "[apns] curl binary not found — install curl to enable APNs delivery:",
        toErrorMessage(error)
      );
      this.configured = false;
      return;
    }
    try {
      this.getJwt();
      this.configured = true;
      console.log(
        `[apns] Provider initialized (${production ? "production" : "sandbox"} mode, curl transport)`
      );
    } catch (error) {
      console.error("[apns] Failed to initialize:", toErrorMessage(error));
      this.configured = false;
    }
  }
  isConfigured() {
    return this.configured;
  }
  async sendNotification(deviceToken, payload) {
    if (!this.configured) {
      throw new Error("APNs service not configured properly");
    }
    if (!deviceToken || !payload) {
      throw new Error("Device token and payload are required");
    }
    return this.deliver(deviceToken, this.buildAlertBody(payload), "alert", "10");
  }
  /**
   * Send a SILENT (content-available) background push to wake a backgrounded app without
   * showing an alert — used to wake the phone-resident agent loop so it can run a burst.
   * Uses apns-push-type:background + apns-priority:5 (required by APNs for silent pushes).
   * iOS throttles these (a few per hour); delivery is best-effort.
   */
  async sendSilentNotification(deviceToken, data) {
    if (!this.configured) {
      throw new Error("APNs service not configured properly");
    }
    if (!deviceToken) {
      throw new Error("Device token is required");
    }
    const body = { aps: { "content-available": 1 } };
    if (data) {
      const { aps: _ignoredAps, ...customData } = data;
      Object.assign(body, customData);
    }
    return this.deliver(deviceToken, body, "background", "5");
  }
  /**
   * Fan out one alert push to many devices concurrently. Fits + serializes the
   * payload once and captures the JWT once (so no call uses a mid-fan-out
   * expired token), then classifies/aggregates results and returns the set of
   * stale tokens for the caller to prune.
   */
  async sendToMany(devices, payload, collapseId) {
    if (!this.configured) {
      throw new Error("APNs service not configured properly");
    }
    if (devices.length === 0) {
      return { results: [], staleTokens: [], totalFailed: 0, totalSent: 0 };
    }
    const serverAppEnv = this.host === APNS_HOST_PROD ? "production" : "sandbox";
    const bodyJson = JSON.stringify(fitApnsPayload(this.buildAlertBody(payload)));
    const jwtToken = this.getJwt();
    const outcomes = await mapLimit(
      devices,
      MAX_APNS_CONCURRENCY,
      async (device) => {
        const res = await this.deliverPreSerialized(
          device.token,
          bodyJson,
          "alert",
          "10",
          jwtToken,
          collapseId
        );
        return {
          appEnv: device.appEnv,
          httpStatus: res.httpStatus ?? 0,
          reason: res.error ?? null,
          registeredAt: device.registeredAt,
          timestampMs: res.timestampMs ?? 0,
          token: device.token
        };
      }
    );
    return summarizeApnsFanOut(outcomes, serverAppEnv);
  }
  shutdown() {
  }
  /** Build the APNs `{ aps, ...customData }` body for an alert push. */
  buildAlertBody(payload) {
    const aps = {
      alert: {
        body: payload.body ?? payload.message ?? "",
        title: payload.title,
        ...payload.subtitle ? { subtitle: payload.subtitle } : {}
      },
      badge: payload.badge ?? 1,
      sound: payload.sound ?? "default"
    };
    if (payload.category) {
      aps.category = payload.category;
    }
    const body = { aps };
    if (payload.data) {
      const { aps: _ignoredAps, ...customData } = payload.data;
      Object.assign(body, customData);
    }
    return body;
  }
  /**
   * Shared curl/HTTP-2 delivery. Fits + serializes the body and signs a JWT,
   * then delegates to deliverPreSerialized. `pushType` is 'alert' | 'background'.
   */
  async deliver(deviceToken, body, pushType, priority) {
    const bodyJson = JSON.stringify(fitApnsPayload(body));
    return this.deliverPreSerialized(deviceToken, bodyJson, pushType, priority, this.getJwt());
  }
  /**
   * Curl/HTTP-2 delivery from a pre-serialized body + pre-captured JWT — the hot
   * path for sendToMany (no per-token fit or JWT sign). Returns httpStatus and,
   * on a 410, the parsed Unregistered timestamp (ms) for the prune guard.
   */
  async deliverPreSerialized(deviceToken, bodyJson, pushType, priority, jwtToken, collapseId) {
    const url = `https://${this.host}/3/device/${deviceToken}`;
    const safeCollapseId = collapseId ? collapseId.replace(/[^\x20-\x7E]/g, "").slice(0, 64) || void 0 : void 0;
    const args = [
      "-sS",
      "--http2",
      "--max-time",
      String(REQUEST_TIMEOUT_SECONDS),
      "-X",
      "POST",
      "-H",
      "content-type: application/json",
      "-H",
      `apns-topic: ${this.bundleId}`,
      "-H",
      `authorization: bearer ${jwtToken}`,
      "-H",
      `apns-push-type: ${pushType}`,
      "-H",
      `apns-priority: ${priority}`,
      ...safeCollapseId ? ["-H", `apns-collapse-id: ${safeCollapseId}`] : [],
      "-d",
      bodyJson,
      "-w",
      "\n__SHOOTER_HTTP_STATUS__:%{http_code}\n",
      url
    ];
    try {
      const { stdout } = await execFileAsync("curl", args, {
        maxBuffer: 1024 * 1024,
        timeout: (REQUEST_TIMEOUT_SECONDS + 5) * 1e3
      });
      const statusMatch = /__SHOOTER_HTTP_STATUS__:(\d+)/.exec(stdout);
      const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
      const bodyText = stdout.replace(/\n?__SHOOTER_HTTP_STATUS__:\d+\n?$/, "").trim();
      if (status === 200) {
        return { failed: 0, httpStatus: 200, sent: 1, success: true };
      }
      let reason = bodyText;
      let timestampMs;
      try {
        const parsed = JSON.parse(bodyText);
        if (parsed && typeof parsed === "object") {
          const obj = parsed;
          if (typeof obj.reason === "string") {
            reason = obj.reason;
          }
          if (typeof obj.timestamp === "number") {
            timestampMs = obj.timestamp < 1e12 ? obj.timestamp * 1e3 : obj.timestamp;
          }
        }
      } catch {
      }
      console.error(`[apns] Delivery failed (status=${status}): ${reason}`);
      return { error: reason, failed: 1, httpStatus: status, sent: 0, success: false, timestampMs };
    } catch (err) {
      const msg = toErrorMessage(err).replace(/bearer\s+[A-Za-z0-9._-]+/gi, "bearer [REDACTED]").replace(/device\/[A-Fa-f0-9]+/g, "device/[REDACTED]");
      console.error(`[apns] curl transport error: ${msg}`);
      return { error: msg, failed: 1, httpStatus: 0, sent: 0, success: false };
    }
  }
  getJwt() {
    const now = Date.now();
    if (this.cachedJwt && now - this.cachedJwtAt < JWT_REFRESH_INTERVAL_MS) {
      return this.cachedJwt;
    }
    if (!this.privateKey || !this.keyId || !this.teamId) {
      throw new Error("APNs credentials missing");
    }
    const token = jwt.sign({ iat: Math.floor(now / 1e3), iss: this.teamId }, this.privateKey, {
      algorithm: "ES256",
      header: { alg: "ES256", kid: this.keyId }
    });
    this.cachedJwt = token;
    this.cachedJwtAt = now;
    return token;
  }
}
async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  const worker = async () => {
    while (next < items.length) {
      const idx = next;
      next += 1;
      results[idx] = await fn(items[idx]);
    }
  };
  const workerCount = Math.min(limit, items.length);
  await Promise.all(Array.from({ length: workerCount }, () => worker()));
  return results;
}

export { LibraryAPNsService as L };
//# sourceMappingURL=library-apns-DMlL1BAg.js.map
