import webpush, { WebPushError } from "web-push";
import { createHash } from "crypto";

const heartbeat = Number(process.env.HEARTBEAT_INTERVAL ?? 30_000);

import { CouchDBChangesStream } from "@hereconnect/couchdb-changes-stream";
import {
  qrDb,
  messagesDbUrl,
  webPushSubscriptionsDb,
  userChatSettingsDb,
  checkDatabasesPromise,
  serviceSeqTrackerDb,
} from "./dbs";
import { setupVapidDetails } from "./setupVapidDetails";
import type { MessageDocument } from "@hereconnect/types";
import { ServiceSeqTracker } from "@hereconnect/lib-service-seq-tracker";
import { startHealthServer } from "@hereconnect/lib-service-health-checkserver";

const abortController = new AbortController();

function limitStringLengthWithEllipsis(str: string, maxLength: number): string {
  return str.length > maxLength ? str.substring(0, maxLength) + "…" : str;
}

function getTopicSafe(str: string) {
  const hash = createHash("sha256").update(str).digest("base64");
  return Buffer.from(hash, "hex").toString("base64").replace(/=+$/, "");
}

const start = async () => {
  const checkedDatabases = await checkDatabasesPromise;
  await setupVapidDetails();

  const serviceSeqTracker = await ServiceSeqTracker.Init(
    "_local/service:message-delivery-method-web-push",
    serviceSeqTrackerDb,
  );

  const changesStream = new CouchDBChangesStream<MessageDocument>(
    messagesDbUrl,
    {
      abortController,
      since: serviceSeqTracker.lastSeq,
      feed: "continuous",
      include_docs: true,
      heartbeat,
      filter: "_selector",
      selector: {
        type: "message",
      },
    },
  );

  // Gracefully handle SIGINT
  process.on("SIGINT", async () => {
    changesStream?.stop();
    setTimeout(() => process.exit(1), 5_000).unref();
  });

  console.info("Service started from sequence", serviceSeqTracker.lastSeq);
  if (
    parseInt(serviceSeqTracker.lastSeq.toString(), 10) ===
    parseInt(checkedDatabases.messagesDb.update_seq.toString(), 10)
  ) {
    console.log("Since is the last seq");
  } else {
    console.log("Last seq is", checkedDatabases.messagesDb.update_seq);
  }

  await startHealthServer(abortController);

  for await (const change of changesStream) {
    const { seq, doc: msgDoc } = change;

    try {
      if (!msgDoc) {
        console.error("Change missing document:", change);
        continue;
      }

      serviceSeqTracker.checkNextSeq(seq);

      const qrDoc = await qrDb.get(`qr_code:${msgDoc.qr_code_uri}`);
      const timestamp = new Date(msgDoc.created_at).getTime();

      const topicUnsafe = `cht_${msgDoc.qr_code_uri}_${msgDoc.chat}`;
      const topic = getTopicSafe(topicUnsafe);

      const tag = `${topicUnsafe}_${(timestamp - new Date(qrDoc.created_at).getTime()).toString(36)}`;

      if (msgDoc.from === "guest") {
        console.log("FROM GUEST");
        // send message to owner
        if (qrDoc.messageDeliveryMethod !== "webPush") {
          console.log(`Skip !webPush`);
          continue;
        }

        const [{ rows: pushSubscriptionRows }, { rows: userChatSettingsRows }] =
          await Promise.all([
            webPushSubscriptionsDb.allDocs({
              startkey: `web_push_subscription:${qrDoc.user_uuid}:`,
              endkey: `web_push_subscription:${qrDoc.user_uuid}:\uffff`,
              include_docs: true,
            }),

            userChatSettingsDb.allDocs({
              startkey: `user_chat_settings:${msgDoc.qr_code_uri}:${msgDoc.chat}:owner:`,
              endkey: `user_chat_settings:${msgDoc.qr_code_uri}:${msgDoc.chat}:owner:\uffff`,
              include_docs: true,
            }),
          ]);

        if (
          userChatSettingsRows.some(
            (row) => row.doc!.is_push_notification_enabled === false,
          )
        ) {
          console.log("push notifications is disabled");
          continue;
        }

        if (pushSubscriptionRows.length === 0) {
          console.log("NO SUBSCRIPTIONS");
        } else {
          console.log(`sending to ${pushSubscriptionRows.length} devices`);
        }

        for (const row of pushSubscriptionRows) {
          try {
            const subscription = row.doc!.subscription;
            console.debug("subscription", subscription);

            const title = limitStringLengthWithEllipsis(
              `${qrDoc.name}: новое сообщение`,
              53,
            );
            const body = limitStringLengthWithEllipsis(msgDoc.body, 470);

            const options: NotificationOptions & { timestamp: number } = {
              icon: "/images/qr-code-logo-200x200.jpg",
              body,
              tag,
              requireInteraction: true,
              timestamp,
              data: {
                url: `/manage/qr/${msgDoc.qr_code_uri}/chat/${msgDoc.chat}#msg-${msgDoc.created_at}`,
                payload: { tag },
              },
            } as const;

            await webpush.sendNotification(
              subscription,
              JSON.stringify({
                type: "notification",
                notification: {
                  title,
                  options,
                },
              }),
              {
                urgency: "high",
                topic,
              },
            );
          } catch (error) {
            console.error(
              `Error sending notification to owner ${row.id} - ${JSON.stringify((error as Error).message)}`,
              error,
            );
            if (error instanceof WebPushError) {
              if (error.body.includes("BadWebPushTopic")) {
                console.error(`Error topic=${topic}`);
              }
            }
          }
        }
      } else if (msgDoc.from === "owner") {
        console.log("FROM OWNER");
        const { rows: userChatSettingsRows } = await userChatSettingsDb.allDocs(
          {
            startkey: `user_chat_settings:${msgDoc.qr_code_uri}:${msgDoc.chat}:guest:`,
            endkey: `user_chat_settings:${msgDoc.qr_code_uri}:${msgDoc.chat}:guest:\uffff`,
            include_docs: true,
          },
        );
        for (const { doc: userChatSettingDoc } of userChatSettingsRows) {
          if (!userChatSettingDoc) {
            console.log("push notification has not been configured");
            continue;
          }
          if (!userChatSettingDoc.is_push_notification_enabled) {
            console.log("push notification is disabled");
            continue;
          }

          const { rows: subscriptionRows } =
            await webPushSubscriptionsDb.allDocs({
              startkey: `web_push_subscription:${userChatSettingDoc.user_uuid}:`,
              endkey: `web_push_subscription:${userChatSettingDoc.user_uuid}:\uffff`,
              include_docs: true,
            });

          if (subscriptionRows.length === 0) {
            console.log("no subscriptions");
          } else {
            console.log(`sending to ${subscriptionRows.length} devices`);
          }

          for (const row of subscriptionRows) {
            try {
              const subscription = row.doc!.subscription;

              const title = limitStringLengthWithEllipsis(
                `Новое сообщение`,
                53,
              );
              const body = limitStringLengthWithEllipsis(msgDoc.body, 470);

              const options: NotificationOptions & { timestamp: number } = {
                icon: "/images/qr-code-logo-200x200.jpg",
                body,
                tag,
                requireInteraction: true,
                timestamp,
                data: {
                  url: `/read/qr/${msgDoc.qr_code_uri}/chat/${msgDoc.chat}#msg-${msgDoc.created_at}`,
                  payload: { tag },
                },
              } as const;

              await webpush.sendNotification(
                subscription,
                JSON.stringify({
                  type: "notification",
                  notification: {
                    title,
                    options,
                  },
                }),
                {
                  urgency: "high",
                  topic,
                },
              );
            } catch (error) {
              console.error(
                `Error sending notification to guest ${row.id} - ${JSON.stringify((error as Error).message)}`,
                error,
              );
            }
          }
        }
      } else {
        continue;
      }

      console.log(
        `Processed change: ${JSON.stringify({ _id: change.id, ref: msgDoc._rev, seq: change.seq })}`,
      );
    } catch (error) {
      console.error(
        `Failed processing: ${JSON.stringify(error)} change: ${JSON.stringify({ id: msgDoc?._id, rev: msgDoc?._rev, seq: seq })}`,
      );
    } finally {
      await serviceSeqTracker.saveSeq(seq);
    }
  }
};

start().catch((error) => {
  console.error("error", error);
  process.exit(1);
});
