import { and, eq } from "drizzle-orm";
import {
  DEFAULT_NOTIFY_PREFS,
  describeNotification,
  type NotificationDescription,
  type NotifyPrefs,
} from "@slackwsh/core";
import type { Message } from "@slackwsh/contracts";
import { schema, withTenant } from "@slackwsh/data";
import type { NotificationJob, SearchIndexJob } from "./jobs";

export interface WebPushDelivery {
  userId: number;
  endpoint: string;
  p256dh: string;
  auth: string;
  payload: Pick<NotificationDescription, "title" | "body" | "route" | "tag">;
}

function toWireMessage(row: typeof schema.messages.$inferSelect): Message {
  return {
    id: row.id,
    workspaceId: row.workspaceId,
    channelId: row.channelId,
    seq: row.seq,
    clientMsgId: row.clientMsgId,
    authorId: row.authorId,
    type: row.type,
    text: row.text,
    blocks: row.blocks,
    revision: row.revision,
    parentId: row.parentId,
    isBroadcast: row.isBroadcast,
    threadReplyCount: row.threadReplyCount,
    threadLastReplyAt: row.threadLastReplyAt?.toISOString() ?? null,
    editedAt: row.editedAt?.toISOString() ?? null,
    deletedAt: row.deletedAt?.toISOString() ?? null,
    createdAt: row.createdAt.toISOString(),
  } as Message;
}

function dndActive(member: { dndEnabled: boolean; dndUntil: Date | null }, now: Date): boolean {
  if (member.dndEnabled) return true;
  return member.dndUntil != null && member.dndUntil.getTime() > now.getTime();
}

/**
 * Turns a committed `notifications` job into Web Push deliveries. Preference,
 * mute, DND, and "don't notify the author" all live here so the worker stays
 * a thin `web-push` sender.
 */
export async function processNotificationJob(job: NotificationJob): Promise<WebPushDelivery[]> {
  const now = new Date();
  const loaded = await withTenant({ workspaceId: job.workspaceId, userId: job.authorId }, async (tx) => {
    const [message] = await tx
      .select()
      .from(schema.messages)
      .where(and(eq(schema.messages.id, job.messageId), eq(schema.messages.workspaceId, job.workspaceId)))
      .limit(1);
    if (!message || message.deletedAt) return null;
    const [channel] = await tx.select().from(schema.channels).where(eq(schema.channels.id, message.channelId)).limit(1);
    if (!channel) return null;
    const members = await tx
      .select({
        userId: schema.channelMembers.userId,
        isMuted: schema.channelMembers.isMuted,
        username: schema.users.username,
        name: schema.users.name,
        dndEnabled: schema.workspaceMembers.dndEnabled,
        dndUntil: schema.workspaceMembers.dndUntil,
        deactivatedAt: schema.workspaceMembers.deactivatedAt,
      })
      .from(schema.channelMembers)
      .innerJoin(schema.users, eq(schema.users.id, schema.channelMembers.userId))
      .innerJoin(
        schema.workspaceMembers,
        and(
          eq(schema.workspaceMembers.workspaceId, job.workspaceId),
          eq(schema.workspaceMembers.userId, schema.channelMembers.userId),
        ),
      )
      .where(eq(schema.channelMembers.channelId, message.channelId));
    return { message, channel, members };
  });
  if (!loaded) return [];

  const deliveries: WebPushDelivery[] = [];
  for (const member of loaded.members) {
    if (member.userId === job.authorId) continue;
    if (member.deactivatedAt) continue;
    if (member.isMuted) continue;
    if (dndActive(member, now)) continue;

    const { prefs, subscriptions } = await withTenant(
      { workspaceId: job.workspaceId, userId: member.userId },
      async (tx) => {
        const [prefRow] = await tx
          .select()
          .from(schema.notificationPreferences)
          .where(
            and(
              eq(schema.notificationPreferences.workspaceId, job.workspaceId),
              eq(schema.notificationPreferences.userId, member.userId),
            ),
          )
          .limit(1);
        const prefs: NotifyPrefs = {
          messages: (prefRow?.messages as NotifyPrefs["messages"]) ?? DEFAULT_NOTIFY_PREFS.messages,
          calls: prefRow?.calls ?? DEFAULT_NOTIFY_PREFS.calls,
          tasks: prefRow?.tasks ?? DEFAULT_NOTIFY_PREFS.tasks,
          calendar: prefRow?.calendar ?? DEFAULT_NOTIFY_PREFS.calendar,
          sound: prefRow?.sound ?? DEFAULT_NOTIFY_PREFS.sound,
          inAppFlash: prefRow?.inAppFlash ?? DEFAULT_NOTIFY_PREFS.inAppFlash,
        };
        const subscriptions = await tx
          .select()
          .from(schema.pushSubscriptions)
          .where(eq(schema.pushSubscriptions.userId, member.userId));
        return { prefs, subscriptions };
      },
    );

    const description = describeNotification(
      {
        type: "message:created",
        message: toWireMessage(loaded.message),
        workspaceId: job.workspaceId,
        channelId: loaded.channel.id,
        channelType: loaded.channel.type,
        channelName: loaded.channel.name,
      },
      {
        myUserId: member.userId,
        myUsername: member.username,
        prefs,
        hidden: true,
        nameFor: (id) => loaded.members.find((m) => m.userId === id)?.name,
      },
    );
    if (!description) continue;
    for (const sub of subscriptions) {
      deliveries.push({
        userId: member.userId,
        endpoint: sub.endpoint,
        p256dh: sub.p256dh,
        auth: sub.auth,
        payload: {
          title: description.title,
          body: description.body,
          route: description.route,
          tag: description.tag,
        },
      });
    }
  }
  return deliveries;
}

/** Postgres FTS is the live search path (ADR-007). GIN indexes update in the
 * same transaction as the message insert, so this job is currently a drain
 * no-op; Typesense indexing is a later swap-in of the same queue. */
export async function processSearchIndexJob(_job: SearchIndexJob): Promise<{ indexed: boolean }> {
  return { indexed: true };
}
