<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="isSystemReady" class="text-lg mb-4">
            <span v-if="isDailyCheck">
              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">
            {{ waitingMessage }}
            <font-awesome icon="spinner" spin />
          </p>

          <div v-if="canShowNotificationForm">
            <div v-if="isDailyCheck">
              <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="toggleHourAm"
                >
                  <span>{{ amPmLabel }} <font-awesome :icon="amPmIcon" /></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="handleTurnOnNotifications"
            >
              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 } from "../constants/app";
import {
  NOTIFY_PUSH_VAPID_ERROR,
  NOTIFY_PUSH_INIT_ERROR,
  NOTIFY_PUSH_BROWSER_NOT_SUPPORTED,
  NOTIFY_PUSH_PERMISSION_ERROR,
  NOTIFY_PUSH_SETUP_UNDERWAY,
  NOTIFY_PUSH_SUCCESS,
  NOTIFY_PUSH_SETUP_ERROR,
  NOTIFY_PUSH_SUBSCRIPTION_ERROR,
  PUSH_NOTIFICATION_TIMEOUT_SHORT,
  PUSH_NOTIFICATION_TIMEOUT_MEDIUM,
  PUSH_NOTIFICATION_TIMEOUT_LONG,
  PUSH_NOTIFICATION_TIMEOUT_PERSISTENT,
} from "../constants/notifications";
import { urlBase64ToUint8Array } from "../libs/crypto/vc/util";
import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";

// 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({
  mixins: [PlatformServiceMixin],
})
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 {
      const settings = await this.$accountSettings();
      let pushUrl = DEFAULT_PUSH_SERVER;
      if (settings?.webPushServer) {
        pushUrl = settings.webPushServer;
      }

      if (pushUrl.startsWith("http://localhost")) {
        this.$logAndConsole(
          "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 || "";
            this.$logAndConsole("Got vapid key: " + this.vapidKey);
            responseData = JSON.stringify(response.data);
            navigator.serviceWorker?.addEventListener(
              "controllerchange",
              () => {
                this.$logAndConsole(
                  "New service worker is now controlling the page",
                );
              },
            );
          });
        if (!this.vapidKey) {
          this.$notify(
            {
              group: "alert",
              type: "danger",
              title: NOTIFY_PUSH_VAPID_ERROR.title,
              text: NOTIFY_PUSH_VAPID_ERROR.message,
            },
            PUSH_NOTIFICATION_TIMEOUT_MEDIUM,
          );
          this.$logAndConsole(
            "Error Setting Notifications: web push server response didn't have vapidKey: " +
              responseData,
            true,
          );
        }
      }
    } catch (error) {
      if (window.location.host.startsWith("localhost")) {
        this.$logAndConsole(
          "Ignoring the error getting VAPID for local development.",
        );
      } else {
        this.$logAndConsole(
          "Got an error initializing notifications: " + JSON.stringify(error),
          true,
        );
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: NOTIFY_PUSH_INIT_ERROR.title,
            text: NOTIFY_PUSH_INIT_ERROR.message,
          },
          PUSH_NOTIFICATION_TIMEOUT_MEDIUM,
        );
      }
    }
    // 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 = this.notificationMessagePlaceholder;
      // 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.");
    }

    // TODO: secretDB functionality needs to be migrated to PlatformServiceMixin
    // For now, we'll use a temporary approach
    const secret = "temporary-secret"; // Placeholder until secret management is migrated
    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) => {
      this.$logAndConsole(
        "Response from service worker: " + JSON.stringify(response),
      );
    });
  }

  private checkNotificationSupport(): Promise<void> {
    if (!("Notification" in window)) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: NOTIFY_PUSH_BROWSER_NOT_SUPPORTED.title,
          text: NOTIFY_PUSH_BROWSER_NOT_SUPPORTED.message,
        },
        PUSH_NOTIFICATION_TIMEOUT_SHORT,
      );
      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: NOTIFY_PUSH_PERMISSION_ERROR.title,
              text: NOTIFY_PUSH_PERMISSION_ERROR.message,
            },
            PUSH_NOTIFICATION_TIMEOUT_PERSISTENT,
          );
          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) => {
        this.$logAndConsole(
          "Permission granted: " + JSON.stringify(permission),
        );

        // Call the function and handle promises
        return this.subscribeToPush();
      })
      .then(() => {
        this.$logAndConsole("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: NOTIFY_PUSH_SETUP_UNDERWAY.title,
              text: NOTIFY_PUSH_SETUP_UNDERWAY.message,
            },
            PUSH_NOTIFICATION_TIMEOUT_PERSISTENT,
          );
          // 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
          this.$logAndConsole(
            "Subscription data sent to server with endpoint: " +
              subscription.endpoint,
          );
          return subscriptionWithTime;
        } else {
          throw new Error("Subscription object is not available.");
        }
      })
      .then(async (subscription: PushSubscriptionWithTime) => {
        this.$logAndConsole(
          "Subscription data sent to server and all finished successfully.",
        );
        await libsUtil.sendTestThroughPushServer(subscription, true);
        notifyCloser();
        setTimeout(() => {
          this.$notify(
            {
              group: "alert",
              type: "success",
              title: NOTIFY_PUSH_SUCCESS.title,
              text: NOTIFY_PUSH_SUCCESS.message,
            },
            PUSH_NOTIFICATION_TIMEOUT_LONG,
          );
        }, 500);
        const timeText = this.notificationTimeText;
        this.callback(true, timeText, this.messageInput);
      })
      .catch((error) => {
        this.$logAndConsole(
          "Got an error setting notification permissions: " +
            " string " +
            error.toString() +
            " JSON " +
            JSON.stringify(error),
          true,
        );
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: NOTIFY_PUSH_SETUP_ERROR.title,
            text: NOTIFY_PUSH_SETUP_ERROR.message,
          },
          PUSH_NOTIFICATION_TIMEOUT_SHORT,
        );
        // 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) => {
          this.$logAndConsole(
            "Push subscription successful: " + JSON.stringify(subscription),
          );
          resolve();
        })
        .catch((error) => {
          this.$logAndConsole(
            "Push subscription failed: " +
              JSON.stringify(error) +
              " - " +
              JSON.stringify(options),
            true,
          );

          // Inform the user about the issue
          this.$notify(
            {
              group: "alert",
              type: "danger",
              title: NOTIFY_PUSH_SUBSCRIPTION_ERROR.title,
              text: NOTIFY_PUSH_SUBSCRIPTION_ERROR.message,
            },
            PUSH_NOTIFICATION_TIMEOUT_PERSISTENT,
          );

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

  private sendSubscriptionToServer(
    subscription: PushSubscriptionWithTime,
  ): Promise<void> {
    this.$logAndConsole(
      "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");
      }
      this.$logAndConsole("Push subscription sent to server successfully.");
    });
  }

  /**
   * Computed property: isDailyCheck
   * Returns true if the current pushType is DAILY_CHECK_TITLE
   */
  get isDailyCheck(): boolean {
    return this.pushType === this.DAILY_CHECK_TITLE;
  }

  /**
   * Computed property: isSystemReady
   * Returns true if serviceWorkerReady and vapidKey are set
   */
  get isSystemReady(): boolean {
    return this.serviceWorkerReady && !!this.vapidKey;
  }

  /**
   * Computed property: canShowNotificationForm
   * Returns true if serviceWorkerReady and vapidKey are set
   */
  get canShowNotificationForm(): boolean {
    return this.serviceWorkerReady && !!this.vapidKey;
  }

  /**
   * Computed property: notificationMessagePlaceholder
   * Returns the default message for direct push
   */
  get notificationMessagePlaceholder(): string {
    return `Click to share some gratitude with the world -- even if they're ${UNNAMED_ENTITY_NAME.toLowerCase()}.`;
  }

  /**
   * Computed property: notificationTimeText
   * Returns the formatted time string for display
   */
  get notificationTimeText(): string {
    return `${this.hourInput}:${this.minuteInput} ${this.hourAm ? "AM" : "PM"}`;
  }

  /**
   * Toggles the AM/PM state for the hour input
   */
  toggleHourAm() {
    this.hourAm = !this.hourAm;
  }

  /**
   * Computed property: amPmLabel
   * Returns 'AM' or 'PM' based on hourAm
   */
  get amPmLabel(): string {
    return this.hourAm ? "AM" : "PM";
  }

  /**
   * Computed property: amPmIcon
   * Returns the appropriate icon for AM/PM
   */
  get amPmIcon(): string {
    return this.hourAm ? "chevron-down" : "chevron-up";
  }

  /**
   * Handles the main action button click
   */
  handleTurnOnNotifications() {
    this.close();
    this.turnOnNotifications();
  }

  /**
   * Computed property: waitingMessage
   * Returns the waiting message for initialization
   */
  get waitingMessage(): string {
    return "Waiting for system initialization, which may take up to 5 seconds...";
  }
}
</script>

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