<template>
  <transition
    enter-active-class="transform ease-out duration-300 transition"
    enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
    enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
    leave-active-class="transition ease-in duration-500"
    leave-from-class="opacity-100"
    leave-to-class="opacity-0"
  >
    <div
      v-if="isVisible"
      class="fixed z-[100] top-0 inset-x-0 w-full inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
    >
      <div
        class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
      >
        <div class="w-full px-6 py-6 text-slate-900 text-center">
          <p v-if="serviceWorkerReady && vapidKey" class="text-lg mb-4">
            <span v-if="pushType === DAILY_CHECK_TITLE">
              Would you like to be notified of new activity, up to once a day?
            </span>
            <span v-else>
              Would you like to get a reminder message once a day?
            </span>
          </p>
          <p v-else class="text-lg mb-4">
            Waiting for system initialization, which may take up to 5 seconds...
            <font-awesome icon="spinner" spin />
          </p>

          <div v-if="serviceWorkerReady && vapidKey">
            <div v-if="pushType === DAILY_CHECK_TITLE">
              <span>Yes, send me a message when there is new data for me</span>
            </div>
            <div v-else>
              <span>Yes, send me this message:</span>
              <!-- eslint-disable -->
              <textarea
                type="text"
                id="push-message"
                v-model="messageInput"
                class="rounded border border-slate-400 mt-2 px-2 py-2 w-full"
                maxlength="100"
              ></textarea
              >
              <!-- eslint-enable -->
              <span class="w-full flex justify-between text-xs text-slate-500">
                <span></span>
                <span>(100 characters max)</span>
              </span>
            </div>

            <div>
              <span class="flex flex-row justify-center">
                <span class="mt-2">... at: </span>
                <input
                  v-model="hourInput"
                  type="number"
                  class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
                  @change="checkHourInput"
                />
                <input
                  v-model="minuteInput"
                  type="number"
                  class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
                  @change="checkMinuteInput"
                />
                <span
                  class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
                  @click="hourAm = !hourAm"
                >
                  <span v-if="hourAm">
                    AM <font-awesome icon="chevron-down" />
                  </span>
                  <span v-else> PM <font-awesome icon="chevron-up" /> </span>
                </span>
              </span>
            </div>
            <button
              class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md"
              @click="
                close();
                turnOnNotifications();
              "
            >
              Turn on Daily Message
            </button>
          </div>

          <button
            class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
            @click="close()"
          >
            No, Not Now
          </button>
        </div>
      </div>
    </div>
  </transition>
</template>

<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";

import {
  DEFAULT_PUSH_SERVER,
  NotificationIface,
  USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import {
  logConsoleAndDb,
  retrieveSettingsForActiveAccount,
  secretDB,
} from "../db/index";
import { MASTER_SECRET_KEY } from "../db/tables/secret";
import { urlBase64ToUint8Array } from "../libs/crypto/vc/util";
import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger";

// Example interface for error
interface ErrorResponse {
  message: string;
}

// PushSubscriptionJSON is defined in the Push API https://www.w3.org/TR/push-api/#dom-pushsubscriptionjson
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
  message?: string;
  notifyTime: { utcHour: number; minute: number };
  notifyType: string;
}

interface ServiceWorkerMessage {
  type: string;
  data: string;
}

interface ServiceWorkerResponse {
  // Define the properties and their types
  success: boolean;
  message?: string;
}

interface VapidResponse {
  data: {
    vapidKey: string;
  };
}

@Component
export default class PushNotificationPermission extends Vue {
  // eslint-disable-next-line
  $notify!: (notification: NotificationIface, timeout?: number) => Promise<() => void>;

  DAILY_CHECK_TITLE = libsUtil.DAILY_CHECK_TITLE;
  DIRECT_PUSH_TITLE = libsUtil.DIRECT_PUSH_TITLE;

  callback: (success: boolean, time: string, message?: string) => void =
    () => {};
  hourAm = true;
  hourInput = "8";
  isVisible = false;
  messageInput = "";
  minuteInput = "00";
  pushType = "";
  serviceWorkerReady = false;
  vapidKey = "";

  async open(
    pushType: string,
    callback?: (success: boolean, time: string, message?: string) => void,
  ) {
    this.callback = callback || this.callback;
    this.isVisible = true;
    this.pushType = pushType;
    try {
      let settings = await databaseUtil.retrieveSettingsForActiveAccount();
      if (USE_DEXIE_DB) {
        settings = await retrieveSettingsForActiveAccount();
      }
      let pushUrl = DEFAULT_PUSH_SERVER;
      if (settings?.webPushServer) {
        pushUrl = settings.webPushServer;
      }

      if (pushUrl.startsWith("http://localhost")) {
        logConsoleAndDb("Not checking for VAPID in this local environment.");
      } else {
        let responseData = "";
        await this.axios
          .get(pushUrl + "/web-push/vapid")
          .then((response: VapidResponse) => {
            this.vapidKey = response.data?.vapidKey || "";
            logConsoleAndDb("Got vapid key: " + this.vapidKey);
            responseData = JSON.stringify(response.data);
            navigator.serviceWorker?.addEventListener(
              "controllerchange",
              () => {
                logConsoleAndDb(
                  "New service worker is now controlling the page",
                );
              },
            );
          });
        if (!this.vapidKey) {
          this.$notify(
            {
              group: "alert",
              type: "danger",
              title: "Error Setting Notifications",
              text: "Could not set notifications.",
            },
            5000,
          );
          logConsoleAndDb(
            "Error Setting Notifications: web push server response didn't have vapidKey: " +
              responseData,
            true,
          );
        }
      }
    } catch (error) {
      if (window.location.host.startsWith("localhost")) {
        logConsoleAndDb(
          "Ignoring the error getting VAPID for local development.",
        );
      } else {
        logConsoleAndDb(
          "Got an error initializing notifications: " + JSON.stringify(error),
          true,
        );
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error Setting Notifications",
            text: "Got an error setting notifications.",
          },
          5000,
        );
      }
    }
    // there may be a long pause here on first initialization
    navigator.serviceWorker?.ready.then(() => {
      this.serviceWorkerReady = true;
    });

    if (this.pushType === this.DIRECT_PUSH_TITLE) {
      this.messageInput =
        "Click to share some gratitude with the world -- even if they're unnamed.";
      // focus on the message input
      setTimeout(function () {
        document.getElementById("push-message")?.focus();
      }, 100);
    } else {
      // not critical but doesn't make sense in a daily check
      this.messageInput = "";
    }
  }

  private close() {
    this.isVisible = false;
  }

  private sendMessageToServiceWorker(
    message: ServiceWorkerMessage,
  ): Promise<unknown> {
    return new Promise((resolve, reject) => {
      if (navigator.serviceWorker?.controller) {
        const messageChannel = new MessageChannel();

        messageChannel.port1.onmessage = (event: MessageEvent) => {
          if (event.data.error) {
            reject(event.data.error as ErrorResponse);
          } else {
            resolve(event.data as ServiceWorkerResponse);
          }
        };

        navigator.serviceWorker?.controller.postMessage(message, [
          messageChannel.port2,
        ]);
      } else {
        reject("Service worker controller not available");
      }
    });
  }

  private async askPermission(): Promise<NotificationPermission> {
    // console.log(
    //   "Requesting permission for notifications: " + JSON.stringify(navigator),
    // );
    if (
      !("serviceWorker" in navigator && navigator.serviceWorker?.controller)
    ) {
      return Promise.reject("Service worker not available.");
    }

    await secretDB.open();
    const secret = (await secretDB.secret.get(MASTER_SECRET_KEY))?.secret;
    if (!secret) {
      return Promise.reject("No secret found.");
    }

    return this.sendSecretToServiceWorker(secret)
      .then(() => this.checkNotificationSupport())
      .then(() => this.requestNotificationPermission())
      .catch((error) => Promise.reject(error));
  }

  private sendSecretToServiceWorker(secret: string): Promise<void> {
    const message: ServiceWorkerMessage = {
      type: "SEND_LOCAL_DATA",
      data: secret,
    };

    return this.sendMessageToServiceWorker(message).then((response) => {
      logConsoleAndDb(
        "Response from service worker: " + JSON.stringify(response),
      );
    });
  }

  private checkNotificationSupport(): Promise<void> {
    if (!("Notification" in window)) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Browser Notifications Are Not Supported",
          text: "This browser does not support notifications.",
        },
        3000,
      );
      return Promise.reject("This browser does not support notifications.");
    }
    if (window.Notification.permission === "granted") {
      return Promise.resolve();
    }
    return Promise.resolve();
  }

  private requestNotificationPermission(): Promise<NotificationPermission> {
    return window.Notification.requestPermission().then(
      (permission: string) => {
        if (permission !== "granted") {
          this.$notify(
            {
              group: "alert",
              type: "danger",
              title: "Error Requesting Notification Permission",
              text:
                "Allow this app permission to make notifications for personal reminders." +
                " You can adjust them at any time in your settings.",
            },
            -1,
          );
          throw new Error("Permission was not granted to this app.");
        }
        return permission;
      },
    );
  }

  private checkHourInput() {
    const hourNum = parseInt(this.hourInput);
    if (isNaN(hourNum)) {
      this.hourInput = "12";
    } else if (hourNum < 1) {
      this.hourInput = "12";
      this.hourAm = !this.hourAm;
    } else if (hourNum > 12) {
      this.hourInput = "1";
      this.hourAm = !this.hourAm;
    } else {
      this.hourInput = hourNum.toString();
    }
  }

  private checkMinuteInput() {
    const minuteNum = parseInt(this.minuteInput);
    if (isNaN(minuteNum)) {
      this.minuteInput = "00";
    } else if (minuteNum < 0) {
      this.minuteInput = "59";
    } else if (minuteNum < 10) {
      this.minuteInput = "0" + minuteNum;
    } else if (minuteNum > 59) {
      this.minuteInput = "00";
    } else {
      this.minuteInput = minuteNum.toString();
    }
  }

  private async turnOnNotifications() {
    let notifyCloser = () => {};
    return this.askPermission()
      .then((permission) => {
        logConsoleAndDb("Permission granted: " + JSON.stringify(permission));

        // Call the function and handle promises
        return this.subscribeToPush();
      })
      .then(() => {
        logConsoleAndDb("Subscribed successfully.");
        return navigator.serviceWorker?.ready;
      })
      .then((registration) => {
        return registration.pushManager.getSubscription();
      })
      .then(async (subscription) => {
        if (subscription) {
          notifyCloser = await this.$notify(
            {
              group: "alert",
              type: "info",
              title: "Notification Setup Underway",
              text: "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page.",
            },
            -1,
          );
          // we already checked that this is a valid hour number
          const rawHourNum = libsUtil.numberOrZero(this.hourInput);
          const adjHourNum = this.hourAm
            ? // If it's AM, then we'll change it to 0 for 12 AM but otherwise use rawHourNum
              rawHourNum === 12
              ? 0
              : rawHourNum
            : // Otherwise it's PM, so keep a 12 but otherwise add 12
              rawHourNum === 12
              ? 12
              : rawHourNum + 12;
          const hourNum = adjHourNum % 24; // probably unnecessary now
          const utcHour =
            hourNum + Math.round(new Date().getTimezoneOffset() / 60);
          const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
          const minuteNum = libsUtil.numberOrZero(this.minuteInput);
          const utcMinute =
            minuteNum + Math.round(new Date().getTimezoneOffset() % 60);
          const finalUtcMinute = (utcMinute + (utcMinute < 0 ? 60 : 0)) % 60;

          const subscriptionWithTime: PushSubscriptionWithTime = {
            notifyTime: { utcHour: finalUtcHour, minute: finalUtcMinute },
            notifyType: this.pushType,
            message: this.messageInput,
            ...subscription.toJSON(),
          };
          await this.sendSubscriptionToServer(subscriptionWithTime);
          // To help investigate potential issues with this: https://firebase.google.com/docs/cloud-messaging/migrate-v1
          logConsoleAndDb(
            "Subscription data sent to server with endpoint: " +
              subscription.endpoint,
          );
          return subscriptionWithTime;
        } else {
          throw new Error("Subscription object is not available.");
        }
      })
      .then(async (subscription: PushSubscriptionWithTime) => {
        logConsoleAndDb(
          "Subscription data sent to server and all finished successfully.",
        );
        await libsUtil.sendTestThroughPushServer(subscription, true);
        notifyCloser();
        setTimeout(() => {
          this.$notify(
            {
              group: "alert",
              type: "success",
              title: "Notification Is On",
              text: "You should see at least one on your device; if not, check the 'Troubleshoot' link.",
            },
            7000,
          );
        }, 500);
        const timeText =
          // eslint-disable-next-line
          this.hourInput + ":" + this.minuteInput + " " + (this.hourAm ? "AM" : "PM");
        this.callback(true, timeText, this.messageInput);
      })
      .catch((error) => {
        logConsoleAndDb(
          "Got an error setting notification permissions: " +
            " string " +
            error.toString() +
            " JSON " +
            JSON.stringify(error),
          true,
        );
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error Setting Notification Permissions",
            text: "Could not set notification permissions.",
          },
          3000,
        );
        // if we want to also unsubscribe, be sure to do that only if no other notification is active
      });
  }

  private subscribeToPush(): Promise<void> {
    return new Promise<void>((resolve, reject) => {
      if (!("serviceWorker" in navigator && "PushManager" in window)) {
        const errorMsg = "Push messaging is not supported";
        logger.warn(errorMsg);
        return reject(new Error(errorMsg));
      }

      if (window.Notification.permission !== "granted") {
        const errorMsg = "Notification permission not granted";
        logger.warn(errorMsg);
        return reject(new Error(errorMsg));
      }

      const applicationServerKey = urlBase64ToUint8Array(this.vapidKey);
      const options: PushSubscriptionOptions = {
        userVisibleOnly: true,
        applicationServerKey: applicationServerKey,
      };

      navigator.serviceWorker?.ready
        .then((registration) => {
          return registration.pushManager.subscribe(options);
        })
        .then((subscription) => {
          logConsoleAndDb(
            "Push subscription successful: " + JSON.stringify(subscription),
          );
          resolve();
        })
        .catch((error) => {
          logConsoleAndDb(
            "Push subscription failed: " +
              JSON.stringify(error) +
              " - " +
              JSON.stringify(options),
            true,
          );

          // Inform the user about the issue
          this.$notify(
            {
              group: "alert",
              type: "danger",
              title: "Error Setting Push Notifications",
              text:
                "We encountered an issue setting up push notifications. " +
                "If you wish to revoke notification permissions, please do so in your browser settings.",
            },
            -1,
          );

          reject(error);
        });
    });
  }

  private sendSubscriptionToServer(
    subscription: PushSubscriptionWithTime,
  ): Promise<void> {
    logConsoleAndDb(
      "About to send subscription... " + JSON.stringify(subscription),
    );
    return fetch("/web-push/subscribe", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(subscription),
    }).then((response) => {
      if (!response.ok) {
        logger.error("Bad response subscribing to web push: ", response);
        throw new Error("Failed to send push subscription to server");
      }
      logConsoleAndDb("Push subscription sent to server successfully.");
    });
  }
}
</script>

<style scoped>
/* Add any specific styles for this component here */
</style>
