<template>
  <router-view />

  <!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
  <NotificationGroup group="alert">
    <div
      class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
    >
      <Notification
        v-slot="{ notifications, close }"
        enter="transform ease-out duration-300 transition"
        enter-from="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4"
        enter-to="translate-y-0 opacity-100 sm:translate-x-0"
        leave="transition ease-in duration-500"
        leave-from="opacity-100"
        leave-to="opacity-0"
        move="transition duration-500"
        move-delay="delay-300"
      >
        <div
          v-for="notification in notifications"
          :key="notification.id"
          class="w-full"
          role="alert"
        >
          <div
            v-if="notification.type === 'toast'"
            class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md"
          >
            <div class="w-full px-4 py-3">
              <span class="font-semibold">{{ notification.title }}</span>
              <p class="text-sm">{{ notification.text }}</p>
            </div>
          </div>

          <div
            v-if="notification.type === 'info'"
            class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-100 rounded-lg shadow-md"
          >
            <div
              class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
            >
              <fa icon="circle-info" class="fa-fw fa-xl"></fa>
            </div>

            <div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
              <span class="font-semibold">{{ notification.title }}</span>
              <p class="text-sm">{{ notification.text }}</p>

              <button
                @click="close(notification.id)"
                class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
              >
                <fa icon="xmark" class="fa-fw"></fa>
              </button>
            </div>
          </div>

          <div
            v-if="notification.type === 'success'"
            class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-emerald-100 rounded-lg shadow-md"
          >
            <div
              class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
            >
              <fa icon="circle-info" class="fa-fw fa-xl"></fa>
            </div>

            <div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
              <span class="font-semibold">{{ notification.title }}</span>
              <p class="text-sm">{{ notification.text }}</p>

              <button
                @click="close(notification.id)"
                class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
              >
                <fa icon="xmark" class="fa-fw"></fa>
              </button>
            </div>
          </div>

          <div
            v-if="notification.type === 'warning'"
            class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-amber-100 rounded-lg shadow-md"
          >
            <div
              class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
            >
              <fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
            </div>

            <div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
              <span class="font-semibold">{{ notification.title }}</span>
              <p class="text-sm">{{ notification.text }}</p>

              <button
                @click="close(notification.id)"
                class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
              >
                <fa icon="xmark" class="fa-fw"></fa>
              </button>
            </div>
          </div>

          <div
            v-if="notification.type === 'danger'"
            class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-rose-100 rounded-lg shadow-md"
          >
            <div
              class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
            >
              <fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
            </div>

            <div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
              <span class="font-semibold">{{ notification.title }}</span>
              <p class="text-sm">{{ notification.text }}</p>

              <button
                @click="close(notification.id)"
                class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
              >
                <fa icon="xmark" class="fa-fw"></fa>
              </button>
            </div>
          </div>
        </div>
      </Notification>
    </div>
  </NotificationGroup>

  <!-- These are general-purpose messages - except there are some for turning app notifications on and off. -->
  <NotificationGroup group="modal">
    <div class="fixed z-[100] top-0 inset-x-0 w-full">
      <Notification
        v-slot="{ notifications, close }"
        enter="transform ease-out duration-300 transition"
        enter-from="translate-y-2 opacity-0 sm:translate-y-4"
        enter-to="translate-y-0 opacity-100 sm:translate-y-0"
        leave="transition ease-in duration-500"
        leave-from="opacity-100"
        leave-to="opacity-0"
        move="transition duration-500"
        move-delay="delay-300"
      >
        <div
          v-for="notification in notifications"
          :key="notification.id"
          class="w-full"
          role="alert"
        >
          <!-- type "confirm" will post a message and, with onYes function, show a "Yes" button to call that function -->
          <div
            v-if="notification.type === 'confirm'"
            class="absolute 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 class="text-lg mb-4">
                  {{ notification.title }}
                </p>

                <button
                  v-if="notification.onYes"
                  @click="
                    notification.onYes();
                    close(notification.id);
                  "
                  class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
                >
                  Yes
                </button>

                <button
                  @click="close(notification.id)"
                  class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
                >
                  {{ notification.onYes ? "Cancel" : "Close" }}
                </button>
              </div>
            </div>
          </div>
          <div
            v-if="notification.type === 'notification-permission'"
            class="absolute 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" class="text-lg mb-4">
                  Would you like to be notified of new activity once a day?
                </p>
                <p v-else class="text-lg mb-4">
                  Waiting for system initialization, which may take up to 10
                  seconds...
                  <fa icon="spinner" spin />
                </p>

                <div v-if="serviceWorkerReady">
                  <span class="flex flex-row justify-center">
                    <span class="mt-2">Yes, tell me at: </span>
                    <input
                      type="number"
                      class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
                      v-model="hourInput"
                    />
                    <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 <fa icon="chevron-down" /> </span>
                      <span v-else> PM <fa icon="chevron-up" /> </span>
                    </span>
                  </span>
                  <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="
                      () => {
                        if (checkHour()) {
                          close(notification.id);
                          turnOnNotifications();
                        }
                      }
                    "
                  >
                    Turn on Daily Message
                  </button>
                </div>

                <button
                  @click="close(notification.id)"
                  class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
                >
                  No, Not Now
                </button>
              </div>
            </div>
          </div>
          <div
            v-if="notification.type === 'notification-mute'"
            class="absolute 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 class="text-lg mb-4">Mute app notifications:</p>

                <button
                  class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
                >
                  For 1 Hour
                </button>
                <button
                  class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
                >
                  For 8 Hours
                </button>
                <button
                  class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
                >
                  For 24 Hours
                </button>
                <button
                  class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
                >
                  Until I turn it back on
                </button>
                <button
                  @click="close(notification.id)"
                  class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
                >
                  Cancel
                </button>
              </div>
            </div>
          </div>
          <div
            v-if="notification.type === 'notification-off'"
            class="absolute 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 class="text-lg mb-4">
                  Would you like to <b>turn off</b> notifications for this app?
                </p>

                <button
                  @click="
                    close(notification.id);
                    turnOffNotifications();
                  "
                  class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
                >
                  Turn Off Notifications
                </button>
                <button
                  @click="close(notification.id)"
                  class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
                >
                  Leave it On
                </button>
              </div>
            </div>
          </div>
        </div>
      </Notification>
    </div>
  </NotificationGroup>
</template>

<style></style>

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

import * as libsUtil from "@/libs/util";

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

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

// Example interface for error
interface ErrorResponse {
  message: string;
  // Other properties as needed
}

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

interface PushSubscriptionWithTime extends PushSubscriptionJSON {
  notifyTime: { utcHour: number };
}

import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { sendTestThroughPushServer } from "@/libs/util";

@Component
export default class App extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;

  b64 = "";
  hourAm = true;
  hourInput = "8";
  serviceWorkerReady = true;

  async mounted() {
    try {
      await db.open();
      const settings = await db.settings.get(MASTER_SETTINGS_KEY);
      let pushUrl = DEFAULT_PUSH_SERVER;
      if (settings?.webPushServer) {
        pushUrl = settings.webPushServer;
      }

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

  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 askPermission(): Promise<NotificationPermission> {
    console.log("Requesting permission for notifications:", navigator);
    if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
      return Promise.reject("Service worker not available.");
    }

    const secret = localStorage.getItem("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) => {
      console.log("Response from service worker:", response);
    });
  }

  private checkNotificationSupport(): Promise<void> {
    if (!("Notification" in window)) {
      alert("This browser does not support notifications.");
      return Promise.reject("This browser does not support notifications.");
    }
    if (Notification.permission === "granted") {
      return Promise.resolve();
    }
    return Promise.resolve();
  }

  private requestNotificationPermission(): Promise<NotificationPermission> {
    return Notification.requestPermission().then((permission) => {
      if (permission !== "granted") {
        alert(
          "Allow this app permission to make notifications for personal reminders." +
            " You can adjust them at any time in your settings.",
        );
        throw new Error("We weren't granted permission.");
      }
      return permission;
    });
  }

  // this allows us to show an error without closing the dialog
  checkHour() {
    if (!libsUtil.isNumeric(this.hourInput)) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Not a Number",
          text: "The time must be an hour number.",
        },
        5000,
      );
      return false;
    }
    const hourNum = libsUtil.numberOrZero(this.hourInput);
    if (!Number.isInteger(hourNum)) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Not a Whole Number",
          text: "The time must be a whole hour number.",
        },
        5000,
      );
      return false;
    }
    if (hourNum < 1 || 12 < hourNum) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Not a Whole Number",
          text: "The time must be an hour between 1 and 12.",
        },
        5000,
      );
      return false;
    }
    return true;
  }

  public async turnOnNotifications() {
    return this.askPermission()
      .then((permission) => {
        console.log("Permission granted:", permission);

        // Call the function and handle promises
        this.subscribeToPush()
          .then(() => {
            console.log("Subscribed successfully.");
            return navigator.serviceWorker?.ready;
          })
          .then((registration) => {
            return registration.pushManager.getSubscription();
          })
          .then(async (subscription) => {
            if (subscription) {
              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 = rawHourNum + (this.hourAm ? 0 : 12);
              const hourNum = adjHourNum % 24;
              const utcHour =
                hourNum + Math.round(new Date().getTimezoneOffset() / 60);
              const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;

              const subscriptionWithTime: PushSubscriptionWithTime = {
                notifyTime: { utcHour: finalUtcHour },
                ...subscription.toJSON(),
              };
              await this.sendSubscriptionToServer(subscriptionWithTime);
              return subscriptionWithTime;
            } else {
              throw new Error("Subscription object is not available.");
            }
          })
          .then(async (subscription: PushSubscriptionWithTime) => {
            console.log(
              "Subscription data sent to server and all finished successfully.",
            );
            await sendTestThroughPushServer(subscription, true);
            this.$notify(
              {
                group: "alert",
                type: "success",
                title: "Notifications Turned On",
                text: "Notifications are on. You should see at least one on your device; if not, check the 'Troubleshoot' page.",
              },
              -1,
            );
          })
          .catch((error) => {
            console.error(
              "Subscription or server communication failed:",
              error,
            );
            alert(
              "Subscription or server communication failed. Try again in a while.",
            );
          });
      })
      .catch((error) => {
        console.error(
          "An error occurred setting notification permissions:",
          error,
        );
        alert("Some error occurred setting notification permissions.");
      });
  }

  private urlBase64ToUint8Array(base64String: string): Uint8Array {
    const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
    const base64 = (base64String + padding)
      .replace(/-/g, "+")
      .replace(/_/g, "/");
    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }

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

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

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

      navigator.serviceWorker.ready
        .then((registration) => {
          return registration.pushManager.subscribe(options);
        })
        .then((subscription) => {
          console.log("Push subscription successful:", subscription);
          resolve();
        })
        .catch((error) => {
          console.error("Push subscription failed:", error, options);

          // Inform the user about the issue
          alert(
            "We encountered an issue setting up push notifications. " +
              "If you wish to revoke notification permissions, please do so in your browser settings.",
          );

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

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

  async turnOffNotifications() {
    let subscription;
    const pushProviderSuccess = await navigator.serviceWorker?.ready
      .then((registration) => {
        return registration.pushManager.getSubscription();
      })
      .then((subscript) => {
        subscription = subscript;
        if (subscription) {
          return subscription.unsubscribe();
        } else {
          console.log("Subscription object is not available.");
          return false;
        }
      })
      .catch((error) => {
        console.error("Push provider server communication failed:", error);
        return false;
      });

    const pushServerSuccess = await fetch("/web-push/unsubscribe", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(subscription),
    })
      .then((response) => {
        return response.ok;
      })
      .catch((error) => {
        console.error("Push server communication failed:", error);
        return false;
      });

    alert(
      "Notifications are off. Push provider unsubscribe " +
        (pushProviderSuccess ? "succeeded" : "failed") +
        (pushProviderSuccess === pushServerSuccess ? " and" : " but") +
        " push server unsubscribe " +
        (pushServerSuccess ? "succeeded" : "failed") +
        ".",
    );
  }
}
</script>