// Push gateway methods send APNs/web-push test notifications and manage web
// push subscriptions/VAPID public-key access for UI clients.
import { normalizeStringifiedOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
  ErrorCodes,
  errorShape,
  validatePushTestParams,
  validateWebPushSubscribeParams,
  validateWebPushTestParams,
  validateWebPushUnsubscribeParams,
  validateWebPushVapidPublicKeyParams,
} from "../../../packages/gateway-protocol/src/index.js";
import {
  clearApnsRegistrationIfCurrent,
  loadApnsRegistration,
  normalizeApnsEnvironment,
  resolveApnsAuthConfigFromEnv,
  resolveApnsRelayConfigFromEnv,
  sendApnsAlert,
  shouldClearStoredApnsRegistration,
} from "../../infra/push-apns.js";
import {
  broadcastWebPush,
  clearWebPushSubscriptionByEndpoint,
  registerWebPushSubscription,
  resolveVapidKeys,
} from "../../infra/push-web.js";
import { respondInvalidParams, respondUnavailableOnThrow } from "./nodes.helpers.js";
import { normalizeTrimmedString } from "./record-shared.js";
import type { GatewayRequestHandlers } from "./types.js";

export const pushHandlers: GatewayRequestHandlers = {
  "push.test": async ({ params, respond, context }) => {
    if (!validatePushTestParams(params)) {
      respondInvalidParams({
        respond,
        method: "push.test",
        validator: validatePushTestParams,
      });
      return;
    }

    const nodeId = normalizeStringifiedOptionalString(params.nodeId) ?? "";
    if (!nodeId) {
      respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required"));
      return;
    }

    const title = normalizeTrimmedString(params.title) ?? "OpenClaw";
    const body = normalizeTrimmedString(params.body) ?? `Push test for node ${nodeId}`;

    await respondUnavailableOnThrow(respond, async () => {
      const registration = await loadApnsRegistration(nodeId);
      if (!registration) {
        respond(
          false,
          undefined,
          errorShape(
            ErrorCodes.INVALID_REQUEST,
            `node ${nodeId} has no APNs registration (connect iOS node first)`,
          ),
        );
        return;
      }

      const overrideEnvironment = normalizeApnsEnvironment(params.environment);
      const result =
        registration.transport === "direct"
          ? await (async () => {
              // Direct registrations require local APNs signing material at
              // send time; relay registrations must not touch those secrets.
              const auth = await resolveApnsAuthConfigFromEnv(process.env);
              if (!auth.ok) {
                respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, auth.error));
                return null;
              }
              return await sendApnsAlert({
                registration: {
                  ...registration,
                  environment: overrideEnvironment ?? registration.environment,
                },
                nodeId,
                title,
                body,
                auth: auth.value,
              });
            })()
          : await (async () => {
              // Relay registrations carry a grant from the node, so the gateway
              // only needs relay config plus the origin bound at registration.
              const relay = resolveApnsRelayConfigFromEnv(
                process.env,
                context.getRuntimeConfig().gateway,
                { registrationRelayOrigin: registration.relayOrigin },
              );
              if (!relay.ok) {
                respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, relay.error));
                return null;
              }
              return await sendApnsAlert({
                registration,
                nodeId,
                title,
                body,
                relayConfig: relay.value,
              });
            })();
      if (!result) {
        return;
      }
      if (
        shouldClearStoredApnsRegistration({
          registration,
          result,
          overrideEnvironment,
        })
      ) {
        // Clear only the exact registration we tested; a reconnect may have
        // written a newer token while the push request was in flight.
        await clearApnsRegistrationIfCurrent({
          nodeId,
          registration,
        });
      }
      respond(true, result, undefined);
    });
  },

  "push.web.vapidPublicKey": async ({ params, respond }) => {
    if (!validateWebPushVapidPublicKeyParams(params)) {
      respondInvalidParams({
        respond,
        method: "push.web.vapidPublicKey",
        validator: validateWebPushVapidPublicKeyParams,
      });
      return;
    }

    await respondUnavailableOnThrow(respond, async () => {
      const vapid = await resolveVapidKeys();
      respond(true, { vapidPublicKey: vapid.publicKey }, undefined);
    });
  },

  "push.web.subscribe": async ({ params, respond }) => {
    if (!validateWebPushSubscribeParams(params)) {
      respondInvalidParams({
        respond,
        method: "push.web.subscribe",
        validator: validateWebPushSubscribeParams,
      });
      return;
    }

    await respondUnavailableOnThrow(respond, async () => {
      const subscription = await registerWebPushSubscription({
        endpoint: params.endpoint,
        keys: params.keys,
      });
      respond(true, { subscriptionId: subscription.subscriptionId }, undefined);
    });
  },

  "push.web.unsubscribe": async ({ params, respond }) => {
    if (!validateWebPushUnsubscribeParams(params)) {
      respondInvalidParams({
        respond,
        method: "push.web.unsubscribe",
        validator: validateWebPushUnsubscribeParams,
      });
      return;
    }

    await respondUnavailableOnThrow(respond, async () => {
      const removed = await clearWebPushSubscriptionByEndpoint(params.endpoint);
      respond(true, { removed }, undefined);
    });
  },

  "push.web.test": async ({ params, respond }) => {
    if (!validateWebPushTestParams(params)) {
      respondInvalidParams({
        respond,
        method: "push.web.test",
        validator: validateWebPushTestParams,
      });
      return;
    }

    const title = normalizeTrimmedString(params.title) ?? "OpenClaw";
    const body = normalizeTrimmedString(params.body) ?? "Web push test notification";

    await respondUnavailableOnThrow(respond, async () => {
      const results = await broadcastWebPush({ title, body });
      if (results.length === 0) {
        respond(
          false,
          undefined,
          errorShape(ErrorCodes.INVALID_REQUEST, "no web push subscriptions registered"),
        );
        return;
      }
      respond(true, { results }, undefined);
    });
  },
};
