/**
 * Web Push / VAPID sender.
 *
 * Concrete implementation of PushClient.
 * Uses the `web-push` npm package for VAPID-signed notifications.
 *
 * Per-type notification toggles (FDS §4.14 / §5.10):
 *   User types:   reminder_new_deals | pickup_reminder | similar_deal_alert | team_message
 *   Vendor types: vendor_every_sale | vendor_stock_runs_out | vendor_personal_deal_request | vendor_team_message
 *
 * Silent mode (global mute) is checked first; then per-type toggle.
 * Both are stored in users.preferences_profile JSONB field.
 *
 * Retry: exponential backoff on 5xx / network errors; no retry on 4xx.
 */

import webPush from 'web-push';
import type { PushClient, PushNotification } from './types.js';
import type { DrizzleClient } from '../db/client.js';
import {
  listSubscriptionsForUser,
  listSubscriptionsForVendor,
  deleteSubscriptionByEndpoint,
} from './subscriptions.js';
import { eq } from 'drizzle-orm';
import { users, vendors } from '../db/schema.js';

// ---------------------------------------------------------------------------
// Env type expected by send functions
// ---------------------------------------------------------------------------

export interface PushEnv {
  VAPID_PUBLIC_KEY: string;
  VAPID_PRIVATE_KEY: string;
  VAPID_SUBJECT: string;
  DATABASE_URL: string;
}

// ---------------------------------------------------------------------------
// Notification preference keys
// ---------------------------------------------------------------------------

export type UserNotifType =
  | 'reminder_new_deals'
  | 'pickup_reminder'
  | 'similar_deal_alert'
  | 'team_message'
  | 'redemption_confirmed';

export type VendorNotifType =
  | 'vendor_every_sale'
  | 'vendor_stock_runs_out'
  | 'vendor_personal_deal_request'
  | 'vendor_team_message';

export type NotifType = UserNotifType | VendorNotifType;

interface NotificationPrefs {
  silent_mode?: boolean;
  reminder_new_deals?: boolean;
  pickup_reminder?: boolean;
  similar_deal_alert?: boolean;
  team_message?: boolean;
  redemption_confirmed?: boolean;
  vendor_every_sale?: boolean;
  vendor_stock_runs_out?: boolean;
  vendor_personal_deal_request?: boolean;
  vendor_team_message?: boolean;
}

// ---------------------------------------------------------------------------
// Preferences helpers (reads from preferences_profile JSONB)
// ---------------------------------------------------------------------------

function getPrefs(preferencesProfile: unknown): NotificationPrefs {
  if (!preferencesProfile || typeof preferencesProfile !== 'object') return {};
  const p = preferencesProfile as Record<string, unknown>;
  const notifs = p['notifications'];
  if (!notifs || typeof notifs !== 'object') return {};
  return notifs as NotificationPrefs;
}

function isNotifEnabled(prefs: NotificationPrefs, type: NotifType | undefined): boolean {
  // Global silent mode check
  if (prefs.silent_mode === true) return false;
  // If no specific type requested, check only silent mode
  if (!type) return true;
  // Per-type toggle - default is true (opted-in) unless explicitly false
  const value = prefs[type];
  return value !== false;
}

// ---------------------------------------------------------------------------
// Retry with exponential backoff
// ---------------------------------------------------------------------------

async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3, baseDelayMs = 500): Promise<T> {
  let lastError: unknown;
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err;
      // Check if it's a non-retryable 4xx error
      if (err && typeof err === 'object' && 'statusCode' in err) {
        const status = (err as { statusCode: number }).statusCode;
        if (status >= 400 && status < 500) {
          throw err; // No retry on 4xx
        }
      }
      if (attempt < maxRetries) {
        const delay = baseDelayMs * Math.pow(2, attempt);
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }
  throw lastError;
}

// ---------------------------------------------------------------------------
// Core send to a single subscription
// ---------------------------------------------------------------------------

async function sendToSubscription(
  vapidKeys: { publicKey: string; privateKey: string; subject: string },
  subscription: { endpoint: string; p256dh: string; auth: string },
  notification: PushNotification,
): Promise<{ gone: boolean }> {
  webPush.setVapidDetails(vapidKeys.subject, vapidKeys.publicKey, vapidKeys.privateKey);

  const payload = JSON.stringify({
    title: notification.title,
    body: notification.body,
    url: notification.url ?? '/',
    icon: notification.icon ?? '/icons/icon-192.png',
    badge: '/icons/badge-96.png',
    tag: notification.tag,
    data: notification.data ?? {},
  });

  try {
    await withRetry(() =>
      webPush.sendNotification(
        {
          endpoint: subscription.endpoint,
          keys: {
            p256dh: subscription.p256dh,
            auth: subscription.auth,
          },
        },
        payload,
      ),
    );
    return { gone: false };
  } catch (err) {
    if (err && typeof err === 'object' && 'statusCode' in err) {
      const status = (err as { statusCode: number }).statusCode;
      if (status === 410 || status === 404) {
        return { gone: true };
      }
    }
    throw err;
  }
}

// ---------------------------------------------------------------------------
// PushClient factory
// ---------------------------------------------------------------------------

/**
 * Create a concrete PushClient bound to the given env + db.
 *
 * Accepts an optional `notifType` to enable per-type toggle checks.
 * When notifType is omitted, only silent_mode is checked.
 */
export function createPushClient(
  db: DrizzleClient,
  env: PushEnv,
  notifType?: NotifType,
): PushClient {
  const vapidKeys = {
    publicKey: env.VAPID_PUBLIC_KEY,
    privateKey: env.VAPID_PRIVATE_KEY,
    subject: env.VAPID_SUBJECT,
  };

  return {
    async sendToUser(userId: string, notification: PushNotification): Promise<void> {
      // Load user preferences
      const [userRow] = await db
        .select({ preferencesProfile: users.preferencesProfile })
        .from(users)
        .where(eq(users.id, userId))
        .limit(1);

      if (!userRow) return;

      const prefs = getPrefs(userRow.preferencesProfile);
      if (!isNotifEnabled(prefs, notifType)) return;

      const subscriptions = await listSubscriptionsForUser(db, userId);
      await Promise.allSettled(
        subscriptions.map(async (sub) => {
          const result = await sendToSubscription(vapidKeys, sub, notification);
          if (result.gone) {
            await deleteSubscriptionByEndpoint(db, sub.endpoint);
          }
        }),
      );
    },

    async sendToVendor(vendorId: string, notification: PushNotification): Promise<void> {
      // Load vendor owner user id for prefs check
      const [vendorRow] = await db
        .select({ ownerUserId: vendors.ownerUserId })
        .from(vendors)
        .where(eq(vendors.id, vendorId))
        .limit(1);

      if (!vendorRow) return;

      const [userRow] = await db
        .select({ preferencesProfile: users.preferencesProfile })
        .from(users)
        .where(eq(users.id, vendorRow.ownerUserId))
        .limit(1);

      if (!userRow) return;

      const prefs = getPrefs(userRow.preferencesProfile);
      if (!isNotifEnabled(prefs, notifType)) return;

      const subscriptions = await listSubscriptionsForVendor(db, vendorId);
      await Promise.allSettled(
        subscriptions.map(async (sub) => {
          const result = await sendToSubscription(vapidKeys, sub, notification);
          if (result.gone) {
            await deleteSubscriptionByEndpoint(db, sub.endpoint);
          }
        }),
      );
    },
  };
}

// ---------------------------------------------------------------------------
// Convenience wrappers (used directly in cron handlers)
// ---------------------------------------------------------------------------

/**
 * Send to a specific user with a notification type check.
 */
export async function sendToUser(
  db: DrizzleClient,
  env: PushEnv,
  userId: string,
  notification: PushNotification,
  notifType?: NotifType,
): Promise<void> {
  return createPushClient(db, env, notifType).sendToUser(userId, notification);
}

/**
 * Send to a vendor's owner with a notification type check.
 */
export async function sendToVendor(
  db: DrizzleClient,
  env: PushEnv,
  vendorId: string,
  notification: PushNotification,
  notifType?: NotifType,
): Promise<void> {
  return createPushClient(db, env, notifType).sendToVendor(vendorId, notification);
}
