/**
 * usePushSubscription — push notification subscription lifecycle.
 *
 * Returns { subscribe, unsubscribe, permission, isSubscribed, setEnabled }.
 *
 * Critical iOS constraint (D-08 / CLAUDE.md iOS table):
 *   subscribe() MUST be called directly inside an onClick handler — NO await
 *   before pushManager.subscribe(). iOS requires a synchronous user gesture
 *   in the call stack. The pattern is:
 *
 *     onClick={() => {
 *       void subscribe(registration)  // ← no await here; pushManager.subscribe starts sync
 *     }}
 *
 * The VAPID key is pre-fetched (GET /api/push/vapid-public-key) and cached in
 * sessionStorage before the tap. On tap, only subscribe() + POST are awaited
 * inside the async subscribe() body — the pushManager.subscribe itself is the
 * synchronous first call, satisfying the iOS gesture requirement.
 *
 * Health-check on mount (D-10): if OS permission is still 'granted' but there
 * is no active subscription (expired/cleared) AND notifications were not
 * explicitly disabled, silently re-subscribe in the background without user action.
 *
 * localStorage key: notificationsEnabled — '1' after successful subscribe,
 *   '0' after explicit user disable. Absence means never configured.
 *
 * setEnabled(true)  — if permission 'granted': silently subscribe + set '1'.
 *                     if permission 'default': no-op; caller must use tap-gated subscribe().
 *                     if permission 'denied': no-op.
 * setEnabled(false) — unsubscribe + set '0'.
 */

import { useState, useEffect } from 'react';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Convert a URL-safe base64 string to a Uint8Array.
 * Required to pass the VAPID public key to pushManager.subscribe() as
 * applicationServerKey (Web Push spec — key must be a BufferSource).
 */
function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer> {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const rawData = atob(base64);
  const buffer = new ArrayBuffer(rawData.length);
  const outputArray = new Uint8Array(buffer);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

/**
 * Fetch the VAPID public key from the API.
 * Caches the result in sessionStorage so subsequent calls within the session
 * are instant — the tap handler can proceed without an extra round-trip.
 */
async function fetchVapidKey(): Promise<string> {
  const cached = sessionStorage.getItem('vapidPublicKey');
  if (cached) return cached;

  const res = await fetch('/api/push/vapid-public-key', {
    credentials: 'include',
  });
  if (!res.ok) throw new Error(`Failed to fetch VAPID key: ${res.status}`);
  const data = (await res.json()) as { publicKey: string };
  if (data.publicKey) {
    sessionStorage.setItem('vapidPublicKey', data.publicKey);
  }
  return data.publicKey;
}

// ---------------------------------------------------------------------------
// localStorage guards (pattern from InstallPrompt.tsx lines 284–297)
// ---------------------------------------------------------------------------

function readNotificationsEnabled(): boolean {
  try {
    return localStorage.getItem('notificationsEnabled') === '1';
  } catch {
    return false;
  }
}

/**
 * Returns true when the user has explicitly disabled notifications
 * (localStorage.notificationsEnabled === '0').
 */
function readNotificationsDisabled(): boolean {
  try {
    return localStorage.getItem('notificationsEnabled') === '0';
  } catch {
    return false;
  }
}

function persistNotificationsEnabled(value: boolean): void {
  try {
    if (value) {
      localStorage.setItem('notificationsEnabled', '1');
    } else {
      localStorage.setItem('notificationsEnabled', '0');
    }
  } catch {
    // Private mode / storage disabled — ignore
  }
}

// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------

export interface UsePushSubscriptionReturn {
  /**
   * Call this inside an onClick handler (no await before the call).
   *
   * CR-04: `vapidKey` must be pre-fetched before the tap (e.g. via prefetchVapidKey()
   * in a useEffect) and passed in here. This eliminates the network round-trip inside
   * the tap handler, preserving the iOS user-gesture requirement for pushManager.subscribe().
   */
  subscribe: (registration: ServiceWorkerRegistration, vapidKey: string) => Promise<void>;
  unsubscribe: () => Promise<void>;
  /** Current Notification.permission value ('default' | 'granted' | 'denied') */
  permission: NotificationPermission;
  /** True when an active push subscription exists in pushManager. */
  isSubscribed: boolean;
  /**
   * Master on/off toggle (D-09).
   *
   * setEnabled(true):
   *   - permission 'granted': silently subscribes + persists '1'.
   *   - permission 'default': no-op; caller must trigger tap-gated subscribe() for the OS dialog.
   *   - permission 'denied': no-op; caller shows the permission-denied hint.
   *
   * setEnabled(false):
   *   - Calls unsubscribe() (DELETE server + browser) + persists '0'.
   */
  setEnabled: (on: boolean) => Promise<void>;
}

export function usePushSubscription(): UsePushSubscriptionReturn {
  const [permission, setPermission] = useState<NotificationPermission>(
    typeof Notification !== 'undefined' ? Notification.permission : 'default',
  );
  const [isSubscribed, setIsSubscribed] = useState(false);

  // Health-check on mount (D-10): if OS permission is granted but no active
  // subscription exists (expired/cleared) AND user hasn't explicitly disabled,
  // silently re-subscribe in background.
  useEffect(() => {
    if (typeof Notification === 'undefined') return;
    if (Notification.permission !== 'granted') return;
    if (!navigator.serviceWorker) return;

    void (async () => {
      try {
        const registration = await navigator.serviceWorker.ready;
        const existingSub = await registration.pushManager.getSubscription();

        if (existingSub) {
          // Re-confirm server-side record (handles 410-prune recovery — D-10).
          // The upsert on endpoint is idempotent; this is a no-op if the record exists.
          await fetch('/api/push/subscription', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            credentials: 'include',
            body: JSON.stringify(existingSub.toJSON()),
          }).catch(() => {}); // non-blocking — ignore network failures
          setIsSubscribed(true);
          return;
        }

        // No active subscription — only silently re-subscribe if the user
        // hasn't explicitly turned notifications off (D-10).
        if (readNotificationsDisabled()) return;

        const vapidKey = await fetchVapidKey();
        const sub = await registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: urlBase64ToUint8Array(vapidKey),
        });
        await fetch('/api/push/subscription', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          credentials: 'include',
          body: JSON.stringify(sub.toJSON()),
        });
        persistNotificationsEnabled(true);
        setIsSubscribed(true);
      } catch {
        // Ignore health-check errors — non-blocking background task
      }
    })();
  }, []);

  /**
   * subscribe — MUST be called inside an onClick handler.
   *
   * iOS user-gesture requirement: pushManager.subscribe() must be called
   * synchronously within a user gesture (tap) callback. This function starts
   * the subscription synchronously and awaits only the POST afterwards.
   *
   * CR-04: The VAPID key MUST be pre-fetched before the tap (e.g., via
   * prefetchVapidKey() in a useEffect) and passed in as `vapidKey`. Fetching
   * it here (inside the async function) would insert an await-fetch before
   * pushManager.subscribe() and break the iOS gesture gate on cache miss.
   */
  const subscribe = async (
    registration: ServiceWorkerRegistration,
    vapidKey: string,
  ): Promise<void> => {
    // pushManager.subscribe — synchronous initiation; iOS gesture gate is here.
    // No await before this call: vapidKey is already resolved by the caller.
    // iOS allows the async resolution of PushSubscription after the gesture frame,
    // as long as subscribe() is the first async operation in the tap handler.
    const sub = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(vapidKey),
    });

    // Persist the subscription to the server
    const res = await fetch('/api/push/subscription', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify(sub.toJSON()),
    });

    if (!res.ok) {
      throw new Error(`Failed to persist subscription: ${res.status}`);
    }

    persistNotificationsEnabled(true);
    setPermission(Notification.permission);
    setIsSubscribed(true);
  };

  /**
   * unsubscribe — retrieve the active subscription and cancel it.
   * Also removes the server-side row via DELETE /api/push/subscription.
   */
  const unsubscribe = async (): Promise<void> => {
    if (!navigator.serviceWorker) return;

    const registration = await navigator.serviceWorker.ready;
    const sub = await registration.pushManager.getSubscription();
    if (sub) {
      await sub.unsubscribe();
    }

    await fetch('/api/push/subscription', {
      method: 'DELETE',
      credentials: 'include',
    });

    persistNotificationsEnabled(false);
    setPermission(Notification.permission);
    setIsSubscribed(false);
  };

  /**
   * setEnabled — master on/off for the settings toggle (D-09).
   *
   * on=true + permission granted: silently subscribes (no OS dialog needed).
   * on=true + permission default: no-op (caller triggers tap-gated subscribe()).
   * on=true + permission denied: no-op (caller shows permission-denied hint).
   * on=false: unsubscribes (browser + server DELETE).
   */
  const setEnabled = async (on: boolean): Promise<void> => {
    if (!on) {
      await unsubscribe();
      return;
    }

    // on=true path
    const currentPermission =
      typeof Notification !== 'undefined' ? Notification.permission : 'default';
    if (currentPermission !== 'granted') {
      // 'default' → caller must use tap-gated subscribe(); 'denied' → no-op
      return;
    }

    // Permission is granted — silently subscribe without a tap gesture
    // (already have OS permission, no dialog required).
    if (!navigator.serviceWorker) return;
    try {
      const registration = await navigator.serviceWorker.ready;
      const existingSub = await registration.pushManager.getSubscription();
      if (existingSub) {
        // Already subscribed — just flip the stored flag back on.
        persistNotificationsEnabled(true);
        setIsSubscribed(true);
        return;
      }

      const vapidKey = await fetchVapidKey();
      const sub = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(vapidKey),
      });
      const res = await fetch('/api/push/subscription', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify(sub.toJSON()),
      });
      if (res.ok) {
        persistNotificationsEnabled(true);
        setIsSubscribed(true);
      }
    } catch {
      // Ignore — non-fatal; user can retry via toggle
    }
  };

  return { subscribe, unsubscribe, permission, isSubscribed, setEnabled };
}

/**
 * Prefetch the VAPID key and cache in sessionStorage.
 * Call this in a useEffect when the permission prompt mounts — ensures the key
 * is ready before the user taps "Enable Notifications".
 */
export async function prefetchVapidKey(): Promise<void> {
  try {
    await fetchVapidKey();
  } catch {
    // Non-fatal — the subscribe() call will retry if sessionStorage miss
  }
}

/**
 * Read whether the user has previously enabled notifications
 * (localStorage.notificationsEnabled === '1').
 * Exported for use in PermissionDeniedBanner and SettingsSheet initial state.
 */
export { readNotificationsEnabled };
