import { BadRequestException, NotFoundException } from "@nestjs/common";
import { and, desc, eq, isNull } from "drizzle-orm";
import type Redis from "ioredis";
import type { NotificationPreferences, UpdateNotificationPreferencesRequest } from "@slackwsh/contracts";
import { DEFAULT_NOTIFY_PREFS, sanitizeMessageBlocks } from "@slackwsh/core";
import { schema, withTenant } from "@slackwsh/data";
import { asEntityId, requireChannelMembership, requireWorkspaceMembership, type EntityIdInput } from "./channels";
import { publishRoomEvent } from "./events-bus";

function draftContextKey(threadRootMessageId: number | null) {
  return threadRootMessageId == null ? "channel" : `thread:${threadRootMessageId}`;
}

function toNotificationPreferences(
  workspaceId: number,
  row: typeof schema.notificationPreferences.$inferSelect | undefined,
): NotificationPreferences {
  return {
    workspaceId,
    messages: (row?.messages ?? DEFAULT_NOTIFY_PREFS.messages) as NotificationPreferences["messages"],
    calls: row?.calls ?? DEFAULT_NOTIFY_PREFS.calls,
    tasks: row?.tasks ?? DEFAULT_NOTIFY_PREFS.tasks,
    calendar: row?.calendar ?? DEFAULT_NOTIFY_PREFS.calendar,
    sound: row?.sound ?? DEFAULT_NOTIFY_PREFS.sound,
    inAppFlash: row?.inAppFlash ?? DEFAULT_NOTIFY_PREFS.inAppFlash,
    updatedAt: row?.updatedAt.toISOString() ?? null,
  };
}

export async function getNotificationPreferences(workspaceId: EntityIdInput, actorUserId: EntityIdInput) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  return withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    const [row] = await tx
      .select()
      .from(schema.notificationPreferences)
      .where(
        and(
          eq(schema.notificationPreferences.workspaceId, wsId),
          eq(schema.notificationPreferences.userId, userId),
        ),
      )
      .limit(1);
    return toNotificationPreferences(wsId, row);
  });
}

export async function updateNotificationPreferences(
  redis: Redis,
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  patch: UpdateNotificationPreferencesRequest,
) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  const preferences = await withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    const [existing] = await tx
      .select()
      .from(schema.notificationPreferences)
      .where(
        and(
          eq(schema.notificationPreferences.workspaceId, wsId),
          eq(schema.notificationPreferences.userId, userId),
        ),
      )
      .limit(1);
    const current = toNotificationPreferences(wsId, existing);
    const [row] = await tx
      .insert(schema.notificationPreferences)
      .values({
        workspaceId: wsId,
        userId,
        messages: patch.messages ?? current.messages,
        calls: patch.calls ?? current.calls,
        tasks: patch.tasks ?? current.tasks,
        calendar: patch.calendar ?? current.calendar,
        sound: patch.sound ?? current.sound,
        inAppFlash: patch.inAppFlash ?? current.inAppFlash,
      })
      .onConflictDoUpdate({
        target: [schema.notificationPreferences.userId, schema.notificationPreferences.workspaceId],
        set: { ...patch, updatedAt: new Date() },
      })
      .returning();
    return toNotificationPreferences(wsId, row);
  });
  await publishRoomEvent(redis, {
    room: `u:${userId}`,
    event: { type: "notification-preferences:updated", payload: { preferences } },
  });
  return preferences;
}

export async function listSavedItems(workspaceId: EntityIdInput, actorUserId: EntityIdInput) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  return withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    const rows = await tx
      .select({
        messageId: schema.messages.id,
        workspaceId: schema.savedItems.workspaceId,
        channelId: schema.messages.channelId,
        channelName: schema.channels.name,
        authorName: schema.users.name,
        text: schema.messages.text,
        createdAt: schema.messages.createdAt,
        savedAt: schema.savedItems.savedAt,
      })
      .from(schema.savedItems)
      .innerJoin(schema.messages, eq(schema.messages.id, schema.savedItems.messageId))
      .innerJoin(schema.channels, eq(schema.channels.id, schema.messages.channelId))
      .innerJoin(schema.users, eq(schema.users.id, schema.messages.authorId))
      .innerJoin(
        schema.channelMembers,
        and(eq(schema.channelMembers.channelId, schema.messages.channelId), eq(schema.channelMembers.userId, userId)),
      )
      .where(
        and(
          eq(schema.savedItems.workspaceId, wsId),
          eq(schema.savedItems.userId, userId),
          isNull(schema.messages.deletedAt),
        ),
      )
      .orderBy(desc(schema.savedItems.savedAt));

    return rows.map((row) => ({
      id: row.messageId,
      workspaceId: row.workspaceId,
      channelId: row.channelId,
      channelName: row.channelName,
      authorName: row.authorName,
      text: row.text,
      createdAt: row.createdAt.toISOString(),
      savedAt: row.savedAt.toISOString(),
    }));
  });
}

export async function saveItem(
  redis: Redis,
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  messageId: EntityIdInput,
) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  const msgId = asEntityId(messageId);
  const saved = await withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    const [message] = await tx
      .select()
      .from(schema.messages)
      .where(and(eq(schema.messages.id, msgId), eq(schema.messages.workspaceId, wsId), isNull(schema.messages.deletedAt)))
      .limit(1);
    if (!message) throw new NotFoundException("message not found");
    await requireChannelMembership(tx, message.channelId, userId);
    const [row] = await tx
      .insert(schema.savedItems)
      .values({ workspaceId: wsId, userId, messageId: msgId })
      .onConflictDoUpdate({
        target: [schema.savedItems.userId, schema.savedItems.messageId],
        set: { savedAt: new Date() },
      })
      .returning();
    return row!;
  });
  await publishRoomEvent(redis, {
    room: `u:${userId}`,
    event: { type: "saved:updated", payload: { workspaceId: wsId, messageId: msgId, saved: true } },
  });
  return saved;
}

export async function unsaveItem(
  redis: Redis,
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  messageId: EntityIdInput,
) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  const msgId = asEntityId(messageId);
  await withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    await tx
      .delete(schema.savedItems)
      .where(
        and(
          eq(schema.savedItems.workspaceId, wsId),
          eq(schema.savedItems.userId, userId),
          eq(schema.savedItems.messageId, msgId),
        ),
      );
  });
  await publishRoomEvent(redis, {
    room: `u:${userId}`,
    event: { type: "saved:updated", payload: { workspaceId: wsId, messageId: msgId, saved: false } },
  });
}

export async function listDrafts(workspaceId: EntityIdInput, actorUserId: EntityIdInput) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  return withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    const rows = await tx
      .select()
      .from(schema.messageDrafts)
      .where(and(eq(schema.messageDrafts.workspaceId, wsId), eq(schema.messageDrafts.userId, userId)))
      .orderBy(desc(schema.messageDrafts.updatedAt));
    return rows.map((row) => ({
      id: row.id,
      workspaceId: row.workspaceId,
      channelId: row.channelId,
      threadRootMessageId: row.threadRootMessageId,
      text: row.text,
      blocks: row.blocks ?? null,
      updatedAt: row.updatedAt.toISOString(),
    }));
  });
}

export async function upsertDraft(
  redis: Redis,
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  input: { channelId: EntityIdInput; threadRootMessageId?: EntityIdInput | null; text: string; blocks?: unknown },
) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  const channelId = asEntityId(input.channelId);
  const threadRootMessageId = input.threadRootMessageId == null ? null : asEntityId(input.threadRootMessageId);
  const blocks = sanitizeMessageBlocks(input.blocks, input.text);
  if (!input.text.trim()) {
    await deleteDraft(redis, wsId, userId, { channelId, threadRootMessageId });
    return null;
  }
  const contextKey = draftContextKey(threadRootMessageId);
  const draft = await withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    await requireChannelMembership(tx, channelId, userId);
    if (threadRootMessageId != null) {
      const [root] = await tx
        .select({ parentId: schema.messages.parentId })
        .from(schema.messages)
        .where(
          and(
            eq(schema.messages.id, threadRootMessageId),
            eq(schema.messages.channelId, channelId),
            eq(schema.messages.workspaceId, wsId),
          ),
        )
        .limit(1);
      if (!root) throw new NotFoundException("thread root not found");
      if (root.parentId != null) throw new BadRequestException("threadRootMessageId must be a top-level message");
    }
    const [row] = await tx
      .insert(schema.messageDrafts)
      .values({ workspaceId: wsId, userId, channelId, threadRootMessageId, contextKey, text: input.text, blocks })
      .onConflictDoUpdate({
        target: [
          schema.messageDrafts.userId,
          schema.messageDrafts.workspaceId,
          schema.messageDrafts.channelId,
          schema.messageDrafts.contextKey,
        ],
        set: { text: input.text, blocks, threadRootMessageId, updatedAt: new Date() },
      })
      .returning();
    return row!;
  });
  await publishRoomEvent(redis, {
    room: `u:${userId}`,
    event: {
      type: "draft:updated",
      payload: { workspaceId: wsId, channelId, threadRootMessageId, text: input.text, blocks, deleted: false },
    },
  });
  return {
    id: draft.id,
    workspaceId: draft.workspaceId,
    channelId: draft.channelId,
    threadRootMessageId: draft.threadRootMessageId,
    text: draft.text,
    blocks: draft.blocks ?? null,
    updatedAt: draft.updatedAt.toISOString(),
  };
}

export async function deleteDraft(
  redis: Redis,
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  input: { channelId: EntityIdInput; threadRootMessageId?: EntityIdInput | null },
) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  const channelId = asEntityId(input.channelId);
  const threadRootMessageId = input.threadRootMessageId == null ? null : asEntityId(input.threadRootMessageId);
  const contextKey = draftContextKey(threadRootMessageId);
  await withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    await tx
      .delete(schema.messageDrafts)
      .where(
        and(
          eq(schema.messageDrafts.workspaceId, wsId),
          eq(schema.messageDrafts.userId, userId),
          eq(schema.messageDrafts.channelId, channelId),
          eq(schema.messageDrafts.contextKey, contextKey),
        ),
      );
  });
  await publishRoomEvent(redis, {
    room: `u:${userId}`,
    event: { type: "draft:updated", payload: { workspaceId: wsId, channelId, threadRootMessageId, text: "", blocks: null, deleted: true } },
  });
}
