import { Router, type IRouter } from "express";
import { eq, and, desc, asc, sql } from "drizzle-orm";
import { db } from "@workspace/db";
import {
  conversationsTable,
  conversationParticipantsTable,
  messagesTable,
  messageReadsTable,
  notificationsTable,
  usersTable,
} from "@workspace/db";
import { requireAuth } from "../middlewares/auth";
import {
  CreateConversationBody,
  GetConversationParams,
  ListMessagesParams,
  SendMessageParams,
  SendMessageBody,
  MarkConversationReadParams,
  UpdateConversationBody,
  UpdateConversationParams,
  AddConversationParticipantsParams,
  AddConversationParticipantsBody,
  RemoveConversationParticipantParams,
  UpdateConversationStateParams,
  UpdateConversationStateBody,
  LeaveConversationBody,
  LeaveConversationParams,
} from "@workspace/api-zod";

const router: IRouter = Router();

async function isConversationMember(
  conversationId: number,
  userId: number,
): Promise<boolean> {
  const [row] = await db
    .select({ userId: conversationParticipantsTable.userId })
    .from(conversationParticipantsTable)
    .where(
      and(
        eq(conversationParticipantsTable.conversationId, conversationId),
        eq(conversationParticipantsTable.userId, userId),
      ),
    );
  return !!row;
}

async function getParticipantState(
  conversationId: number,
  userId: number,
): Promise<{ isMuted: boolean; isArchived: boolean } | null> {
  const [row] = await db
    .select({
      isMuted: conversationParticipantsTable.isMuted,
      isArchived: conversationParticipantsTable.isArchived,
    })
    .from(conversationParticipantsTable)
    .where(
      and(
        eq(conversationParticipantsTable.conversationId, conversationId),
        eq(conversationParticipantsTable.userId, userId),
      ),
    );
  return row ?? null;
}

async function buildConversationDetails(conversationId: number, currentUserId: number) {
  const [conv] = await db
    .select()
    .from(conversationsTable)
    .where(eq(conversationsTable.id, conversationId));

  if (!conv) return null;

  const participants = await db
    .select({
      id: usersTable.id,
      username: usersTable.username,
      displayNameAr: usersTable.displayNameAr,
      displayNameEn: usersTable.displayNameEn,
      avatarUrl: usersTable.avatarUrl,
      isAdmin: conversationParticipantsTable.isAdmin,
    })
    .from(conversationParticipantsTable)
    .innerJoin(usersTable, eq(conversationParticipantsTable.userId, usersTable.id))
    .where(eq(conversationParticipantsTable.conversationId, conversationId));

  const lastMessages = await db
    .select()
    .from(messagesTable)
    .where(eq(messagesTable.conversationId, conversationId))
    .orderBy(desc(messagesTable.createdAt))
    .limit(1);

  let lastMessage = null;
  if (lastMessages.length > 0) {
    const msg = lastMessages[0];
    const [sender] = await db
      .select({
        id: usersTable.id,
        username: usersTable.username,
        displayNameAr: usersTable.displayNameAr,
        displayNameEn: usersTable.displayNameEn,
        avatarUrl: usersTable.avatarUrl,
        isAdmin: sql<boolean>`false`,
      })
      .from(usersTable)
      .where(eq(usersTable.id, msg.senderId));

    lastMessage = { ...msg, sender };
  }

  const unreadResult = await db
    .select({ count: sql<number>`count(*)` })
    .from(messagesTable)
    .where(
      and(
        eq(messagesTable.conversationId, conversationId),
        eq(messagesTable.kind, "user"),
        sql`${messagesTable.id} NOT IN (
          SELECT message_id FROM message_reads WHERE user_id = ${currentUserId}
        )`,
        sql`${messagesTable.senderId} != ${currentUserId}`,
      ),
    );

  const unreadCount = Number(unreadResult[0]?.count ?? 0);

  const state = await getParticipantState(conversationId, currentUserId);

  return {
    ...conv,
    participants,
    lastMessage,
    unreadCount,
    isMuted: state?.isMuted ?? false,
    isArchived: state?.isArchived ?? false,
  };
}

router.get("/conversations", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;

  const userConvs = await db
    .select({ conversationId: conversationParticipantsTable.conversationId })
    .from(conversationParticipantsTable)
    .where(eq(conversationParticipantsTable.userId, userId));

  const results = await Promise.all(
    userConvs.map((c) => buildConversationDetails(c.conversationId, userId)),
  );

  res.json(results.filter(Boolean));
});

router.post("/conversations", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const parsed = CreateConversationBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const { participantIds, nameAr, nameEn, isGroup, avatarUrl } = parsed.data;

  const allParticipants = [...new Set([userId, ...participantIds])];

  const [conv] = await db
    .insert(conversationsTable)
    .values({
      nameAr: nameAr ?? null,
      nameEn: nameEn ?? null,
      avatarUrl: avatarUrl ?? null,
      isGroup: isGroup ?? false,
      createdBy: userId,
    })
    .returning();

  await db.insert(conversationParticipantsTable).values(
    allParticipants.map((pid) => ({
      conversationId: conv.id,
      userId: pid,
      isAdmin: pid === userId,
    })),
  );

  const details = await buildConversationDetails(conv.id, userId);
  res.status(201).json(details);
});

router.get("/conversations/:id", requireAuth, async (req, res): Promise<void> => {
  const params = GetConversationParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const userId = req.session.userId!;

  if (!(await isConversationMember(params.data.id, userId))) {
    res.status(403).json({ error: "Forbidden" });
    return;
  }

  const details = await buildConversationDetails(params.data.id, userId);

  if (!details) {
    res.status(404).json({ error: "Conversation not found" });
    return;
  }

  res.json(details);
});

router.patch("/conversations/:id", requireAuth, async (req, res): Promise<void> => {
  const params = UpdateConversationParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }
  const parsed = UpdateConversationBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const userId = req.session.userId!;

  const [participant] = await db
    .select({ isAdmin: conversationParticipantsTable.isAdmin })
    .from(conversationParticipantsTable)
    .where(
      and(
        eq(conversationParticipantsTable.conversationId, params.data.id),
        eq(conversationParticipantsTable.userId, userId),
      ),
    );

  if (!participant) {
    res.status(403).json({ error: "Forbidden" });
    return;
  }
  if (!participant.isAdmin) {
    res.status(403).json({ error: "Only group admins can update this conversation" });
    return;
  }

  const [conv] = await db
    .select({
      isGroup: conversationsTable.isGroup,
      nameAr: conversationsTable.nameAr,
      nameEn: conversationsTable.nameEn,
    })
    .from(conversationsTable)
    .where(eq(conversationsTable.id, params.data.id));
  if (!conv) {
    res.status(404).json({ error: "Conversation not found" });
    return;
  }
  if (!conv.isGroup) {
    res.status(400).json({ error: "Direct conversations cannot be updated" });
    return;
  }

  const updates: Partial<typeof conversationsTable.$inferInsert> = {};
  if (parsed.data.avatarUrl !== undefined) {
    updates.avatarUrl = parsed.data.avatarUrl;
  }
  if (parsed.data.nameAr !== undefined) {
    const trimmed = parsed.data.nameAr?.trim();
    updates.nameAr = trimmed ? trimmed : null;
  }
  if (parsed.data.nameEn !== undefined) {
    const trimmed = parsed.data.nameEn?.trim();
    updates.nameEn = trimmed ? trimmed : null;
  }

  const finalNameAr = updates.nameAr !== undefined ? updates.nameAr : conv.nameAr;
  const finalNameEn = updates.nameEn !== undefined ? updates.nameEn : conv.nameEn;
  if (
    (updates.nameAr !== undefined || updates.nameEn !== undefined) &&
    !finalNameAr &&
    !finalNameEn
  ) {
    res.status(400).json({ error: "Group must have an Arabic or English name" });
    return;
  }

  const nameChanged =
    (updates.nameAr !== undefined && updates.nameAr !== conv.nameAr) ||
    (updates.nameEn !== undefined && updates.nameEn !== conv.nameEn);

  if (Object.keys(updates).length > 0) {
    await db
      .update(conversationsTable)
      .set(updates)
      .where(eq(conversationsTable.id, params.data.id));
  }

  if (nameChanged) {
    const actor = await getUserDisplay(userId);
    await insertAndEmitSystemMessage(params.data.id, userId, "group_renamed", {
      actor,
      oldNameAr: conv.nameAr,
      oldNameEn: conv.nameEn,
      newNameAr: finalNameAr,
      newNameEn: finalNameEn,
    });
  }

  const details = await buildConversationDetails(params.data.id, userId);
  if (!details) {
    res.status(404).json({ error: "Conversation not found" });
    return;
  }

  await emitConversationUpdate(params.data.id);

  res.json(details);
});

async function evictUserFromConversationRoom(
  userId: number,
  conversationId: number,
): Promise<void> {
  const { io } = await import("../index.js");
  const sockets = await io.in(`user:${userId}`).fetchSockets();
  for (const s of sockets) {
    s.leave(`conversation:${conversationId}`);
  }
}

type SystemMessageMeta = Record<string, unknown>;

async function getUserDisplay(userId: number): Promise<{
  id: number;
  username: string;
  displayNameAr: string | null;
  displayNameEn: string | null;
} | null> {
  const [u] = await db
    .select({
      id: usersTable.id,
      username: usersTable.username,
      displayNameAr: usersTable.displayNameAr,
      displayNameEn: usersTable.displayNameEn,
    })
    .from(usersTable)
    .where(eq(usersTable.id, userId));
  return u ?? null;
}

async function getUsersDisplay(userIds: number[]): Promise<
  Array<{
    id: number;
    username: string;
    displayNameAr: string | null;
    displayNameEn: string | null;
  }>
> {
  if (userIds.length === 0) return [];
  return db
    .select({
      id: usersTable.id,
      username: usersTable.username,
      displayNameAr: usersTable.displayNameAr,
      displayNameEn: usersTable.displayNameEn,
    })
    .from(usersTable)
    .where(sql`${usersTable.id} IN (${sql.join(userIds.map((id) => sql`${id}`), sql`, `)})`);
}

async function insertAndEmitSystemMessage(
  conversationId: number,
  actorId: number,
  kind: string,
  meta: SystemMessageMeta,
): Promise<void> {
  const [msg] = await db
    .insert(messagesTable)
    .values({
      conversationId,
      senderId: actorId,
      content: "",
      kind,
      meta,
    })
    .returning();

  const [sender] = await db
    .select({
      id: usersTable.id,
      username: usersTable.username,
      displayNameAr: usersTable.displayNameAr,
      displayNameEn: usersTable.displayNameEn,
      avatarUrl: usersTable.avatarUrl,
      isAdmin: sql<boolean>`false`,
    })
    .from(usersTable)
    .where(eq(usersTable.id, actorId));

  const fullMessage = { ...msg, sender };
  const { io } = await import("../index.js");
  io.to(`conversation:${conversationId}`).emit("new_message", fullMessage);
}

async function emitConversationUpdate(conversationId: number): Promise<void> {
  const { io } = await import("../index.js");
  const members = await db
    .select({ userId: conversationParticipantsTable.userId })
    .from(conversationParticipantsTable)
    .where(eq(conversationParticipantsTable.conversationId, conversationId));

  io.to(`conversation:${conversationId}`).emit("conversation_updated", {
    conversationId,
  });
  for (const m of members) {
    io.to(`user:${m.userId}`).emit("conversation_updated", { conversationId });
  }
}

async function requireGroupAdmin(
  conversationId: number,
  userId: number,
): Promise<{ ok: true } | { ok: false; status: number; error: string }> {
  const [participant] = await db
    .select({ isAdmin: conversationParticipantsTable.isAdmin })
    .from(conversationParticipantsTable)
    .where(
      and(
        eq(conversationParticipantsTable.conversationId, conversationId),
        eq(conversationParticipantsTable.userId, userId),
      ),
    );
  if (!participant) return { ok: false, status: 403, error: "Forbidden" };
  if (!participant.isAdmin) {
    return { ok: false, status: 403, error: "Only group admins can manage participants" };
  }
  const [conv] = await db
    .select({ isGroup: conversationsTable.isGroup })
    .from(conversationsTable)
    .where(eq(conversationsTable.id, conversationId));
  if (!conv) return { ok: false, status: 404, error: "Conversation not found" };
  if (!conv.isGroup) {
    return { ok: false, status: 400, error: "Direct conversations cannot be modified" };
  }
  return { ok: true };
}

router.post(
  "/conversations/:id/participants",
  requireAuth,
  async (req, res): Promise<void> => {
    const params = AddConversationParticipantsParams.safeParse(req.params);
    if (!params.success) {
      res.status(400).json({ error: params.error.message });
      return;
    }
    const parsed = AddConversationParticipantsBody.safeParse(req.body);
    if (!parsed.success) {
      res.status(400).json({ error: parsed.error.message });
      return;
    }
    const userId = req.session.userId!;
    const guard = await requireGroupAdmin(params.data.id, userId);
    if (!guard.ok) {
      res.status(guard.status).json({ error: guard.error });
      return;
    }

    const requestedIds = [...new Set(parsed.data.userIds)].filter((id) => Number.isInteger(id));
    if (requestedIds.length === 0) {
      res.status(400).json({ error: "No users to add" });
      return;
    }

    const existing = await db
      .select({ userId: conversationParticipantsTable.userId })
      .from(conversationParticipantsTable)
      .where(eq(conversationParticipantsTable.conversationId, params.data.id));
    const existingIds = new Set(existing.map((e) => e.userId));
    const toAdd = requestedIds.filter((id) => !existingIds.has(id));

    if (toAdd.length > 0) {
      const validUsers = await db
        .select({ id: usersTable.id })
        .from(usersTable)
        .where(sql`${usersTable.id} IN (${sql.join(toAdd.map((id) => sql`${id}`), sql`, `)})`);
      const validIds = validUsers.map((u) => u.id);
      if (validIds.length === 0) {
        res.status(400).json({ error: "No valid users to add" });
        return;
      }
      await db
        .insert(conversationParticipantsTable)
        .values(
          validIds.map((pid) => ({
            conversationId: params.data.id,
            userId: pid,
            isAdmin: false,
          })),
        )
        .onConflictDoNothing();

      const [actor, addedUsers] = await Promise.all([
        getUserDisplay(userId),
        getUsersDisplay(validIds),
      ]);
      await insertAndEmitSystemMessage(params.data.id, userId, "members_added", {
        actor,
        addedUsers,
      });
    }

    const details = await buildConversationDetails(params.data.id, userId);
    if (!details) {
      res.status(404).json({ error: "Conversation not found" });
      return;
    }
    await emitConversationUpdate(params.data.id);
    res.json(details);
  },
);

router.delete(
  "/conversations/:id/participants/:userId",
  requireAuth,
  async (req, res): Promise<void> => {
    const params = RemoveConversationParticipantParams.safeParse(req.params);
    if (!params.success) {
      res.status(400).json({ error: params.error.message });
      return;
    }
    const userId = req.session.userId!;
    const guard = await requireGroupAdmin(params.data.id, userId);
    if (!guard.ok) {
      res.status(guard.status).json({ error: guard.error });
      return;
    }

    if (params.data.userId === userId) {
      res.status(400).json({ error: "Admins cannot remove themselves" });
      return;
    }

    const deleted = await db
      .delete(conversationParticipantsTable)
      .where(
        and(
          eq(conversationParticipantsTable.conversationId, params.data.id),
          eq(conversationParticipantsTable.userId, params.data.userId),
        ),
      )
      .returning({ userId: conversationParticipantsTable.userId });

    await evictUserFromConversationRoom(params.data.userId, params.data.id);

    if (deleted.length > 0) {
      const [actor, removedUser] = await Promise.all([
        getUserDisplay(userId),
        getUserDisplay(params.data.userId),
      ]);
      await insertAndEmitSystemMessage(params.data.id, userId, "member_removed", {
        actor,
        removedUser,
      });
    }

    const details = await buildConversationDetails(params.data.id, userId);
    if (!details) {
      res.status(404).json({ error: "Conversation not found" });
      return;
    }
    await emitConversationUpdate(params.data.id);
    res.json(details);
  },
);

router.patch(
  "/conversations/:id/state",
  requireAuth,
  async (req, res): Promise<void> => {
    const params = UpdateConversationStateParams.safeParse(req.params);
    if (!params.success) {
      res.status(400).json({ error: params.error.message });
      return;
    }
    const parsed = UpdateConversationStateBody.safeParse(req.body);
    if (!parsed.success) {
      res.status(400).json({ error: parsed.error.message });
      return;
    }
    const userId = req.session.userId!;

    if (!(await isConversationMember(params.data.id, userId))) {
      res.status(403).json({ error: "Forbidden" });
      return;
    }

    const updates: { isMuted?: boolean; isArchived?: boolean } = {};
    if (parsed.data.isMuted !== undefined) updates.isMuted = parsed.data.isMuted;
    if (parsed.data.isArchived !== undefined) updates.isArchived = parsed.data.isArchived;

    if (Object.keys(updates).length > 0) {
      await db
        .update(conversationParticipantsTable)
        .set(updates)
        .where(
          and(
            eq(conversationParticipantsTable.conversationId, params.data.id),
            eq(conversationParticipantsTable.userId, userId),
          ),
        );
    }

    const details = await buildConversationDetails(params.data.id, userId);
    if (!details) {
      res.status(404).json({ error: "Conversation not found" });
      return;
    }
    res.json(details);
  },
);

router.post(
  "/conversations/:id/leave",
  requireAuth,
  async (req, res): Promise<void> => {
    const params = LeaveConversationParams.safeParse(req.params);
    if (!params.success) {
      res.status(400).json({ error: params.error.message });
      return;
    }
    const body = LeaveConversationBody.safeParse(req.body ?? {});
    if (!body.success) {
      res.status(400).json({ error: body.error.message });
      return;
    }
    const requestedSuccessorId = body.data.successorId ?? null;
    const userId = req.session.userId!;

    const [conv] = await db
      .select({ isGroup: conversationsTable.isGroup })
      .from(conversationsTable)
      .where(eq(conversationsTable.id, params.data.id));
    if (!conv) {
      res.status(404).json({ error: "Conversation not found" });
      return;
    }
    if (!conv.isGroup) {
      res.status(400).json({ error: "Cannot leave a direct conversation" });
      return;
    }

    const participants = await db
      .select({
        userId: conversationParticipantsTable.userId,
        isAdmin: conversationParticipantsTable.isAdmin,
        joinedAt: conversationParticipantsTable.joinedAt,
      })
      .from(conversationParticipantsTable)
      .where(eq(conversationParticipantsTable.conversationId, params.data.id))
      .orderBy(asc(conversationParticipantsTable.joinedAt));

    const leaver = participants.find((p) => p.userId === userId);
    if (!leaver) {
      res.status(403).json({ error: "Forbidden" });
      return;
    }

    const remaining = participants.filter((p) => p.userId !== userId);

    // If leaver was the only participant, clean up the conversation entirely.
    if (remaining.length === 0) {
      await db
        .delete(conversationsTable)
        .where(eq(conversationsTable.id, params.data.id));
      await evictUserFromConversationRoom(userId, params.data.id);
      res.json({ success: true });
      return;
    }

    // Determine if we need to promote a successor admin.
    const leaverWasOnlyAdmin =
      leaver.isAdmin && !remaining.some((p) => p.isAdmin);
    let successor: (typeof remaining)[number] | null = null;
    if (leaverWasOnlyAdmin) {
      if (requestedSuccessorId !== null) {
        const chosen = remaining.find(
          (p) => p.userId === requestedSuccessorId,
        );
        if (!chosen) {
          res
            .status(400)
            .json({ error: "Chosen successor is not a member of this group" });
          return;
        }
        successor = chosen;
      } else {
        successor = remaining[0];
      }
    }

    if (successor) {
      await db
        .update(conversationParticipantsTable)
        .set({ isAdmin: true })
        .where(
          and(
            eq(conversationParticipantsTable.conversationId, params.data.id),
            eq(conversationParticipantsTable.userId, successor.userId),
          ),
        );
    }

    // Announce the leave (and promotion) before removing the leaver so the
    // system message has a sensible actor and members see it in real time.
    const actor = await getUserDisplay(userId);
    await insertAndEmitSystemMessage(params.data.id, userId, "member_left", {
      actor,
    });

    if (successor) {
      const promoted = await getUserDisplay(successor.userId);
      await insertAndEmitSystemMessage(
        params.data.id,
        successor.userId,
        "admin_promoted",
        { promoted, reason: "previous_admin_left" },
      );
    }

    await db
      .delete(conversationParticipantsTable)
      .where(
        and(
          eq(conversationParticipantsTable.conversationId, params.data.id),
          eq(conversationParticipantsTable.userId, userId),
        ),
      );

    await evictUserFromConversationRoom(userId, params.data.id);
    await emitConversationUpdate(params.data.id);
    res.json({ success: true });
  },
);

router.get("/conversations/:id/messages", requireAuth, async (req, res): Promise<void> => {
  const params = ListMessagesParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const userId = req.session.userId!;

  if (!(await isConversationMember(params.data.id, userId))) {
    res.status(403).json({ error: "Forbidden" });
    return;
  }

  const msgs = await db
    .select()
    .from(messagesTable)
    .where(eq(messagesTable.conversationId, params.data.id))
    .orderBy(desc(messagesTable.createdAt))
    .limit(50);

  const messagesWithSenders = await Promise.all(
    msgs.reverse().map(async (msg) => {
      const [sender] = await db
        .select({
          id: usersTable.id,
          username: usersTable.username,
          displayNameAr: usersTable.displayNameAr,
          displayNameEn: usersTable.displayNameEn,
          avatarUrl: usersTable.avatarUrl,
          isAdmin: sql<boolean>`false`,
        })
        .from(usersTable)
        .where(eq(usersTable.id, msg.senderId));

      return { ...msg, sender };
    }),
  );

  res.json(messagesWithSenders);
});

router.post("/conversations/:id/messages", requireAuth, async (req, res): Promise<void> => {
  const params = SendMessageParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const parsed = SendMessageBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const userId = req.session.userId!;

  if (!(await isConversationMember(params.data.id, userId))) {
    res.status(403).json({ error: "Forbidden" });
    return;
  }

  const [msg] = await db
    .insert(messagesTable)
    .values({
      conversationId: params.data.id,
      senderId: userId,
      content: parsed.data.content,
    })
    .returning();

  // Auto-unarchive for all participants when a new message arrives
  await db
    .update(conversationParticipantsTable)
    .set({ isArchived: false })
    .where(
      and(
        eq(conversationParticipantsTable.conversationId, params.data.id),
        eq(conversationParticipantsTable.isArchived, true),
      ),
    );

  const [sender] = await db
    .select({
      id: usersTable.id,
      username: usersTable.username,
      displayNameAr: usersTable.displayNameAr,
      displayNameEn: usersTable.displayNameEn,
      avatarUrl: usersTable.avatarUrl,
      isAdmin: sql<boolean>`false`,
    })
    .from(usersTable)
    .where(eq(usersTable.id, userId));

  const fullMessage = { ...msg, sender };

  const { io } = await import("../index.js");
  io.to(`conversation:${params.data.id}`).emit("new_message", fullMessage);

  await createMessageNotifications({
    conversationId: params.data.id,
    senderId: userId,
    sender,
    content: parsed.data.content,
  });

  res.status(201).json(fullMessage);
});

async function createMessageNotifications(input: {
  conversationId: number;
  senderId: number;
  sender: {
    displayNameAr: string | null;
    displayNameEn: string | null;
    username: string;
  };
  content: string;
}): Promise<void> {
  const recipients = await db
    .select({ userId: conversationParticipantsTable.userId })
    .from(conversationParticipantsTable)
    .where(
      and(
        eq(conversationParticipantsTable.conversationId, input.conversationId),
        eq(conversationParticipantsTable.isMuted, false),
        sql`${conversationParticipantsTable.userId} != ${input.senderId}`,
      ),
    );

  if (recipients.length === 0) return;

  const [conv] = await db
    .select({
      isGroup: conversationsTable.isGroup,
      nameAr: conversationsTable.nameAr,
      nameEn: conversationsTable.nameEn,
    })
    .from(conversationsTable)
    .where(eq(conversationsTable.id, input.conversationId));
  if (!conv) return;

  const senderNameAr =
    input.sender.displayNameAr || input.sender.displayNameEn || input.sender.username;
  const senderNameEn =
    input.sender.displayNameEn || input.sender.displayNameAr || input.sender.username;

  const preview = input.content.trim().slice(0, 140);

  let titleAr: string;
  let titleEn: string;
  let bodyAr: string | null;
  let bodyEn: string | null;

  if (conv.isGroup) {
    const groupNameAr = conv.nameAr || conv.nameEn || senderNameAr;
    const groupNameEn = conv.nameEn || conv.nameAr || senderNameEn;
    titleAr = groupNameAr;
    titleEn = groupNameEn;
    bodyAr = preview ? `${senderNameAr}: ${preview}` : senderNameAr;
    bodyEn = preview ? `${senderNameEn}: ${preview}` : senderNameEn;
  } else {
    titleAr = senderNameAr;
    titleEn = senderNameEn;
    bodyAr = preview || null;
    bodyEn = preview || null;
  }

  await db.insert(notificationsTable).values(
    recipients.map((r) => ({
      userId: r.userId,
      titleAr,
      titleEn,
      bodyAr,
      bodyEn,
      type: "chat",
      relatedId: input.conversationId,
      relatedType: "conversation",
    })),
  );

  const { io } = await import("../index.js");
  for (const r of recipients) {
    io.to(`user:${r.userId}`).emit("notification_created", {
      type: "chat",
      relatedId: input.conversationId,
      relatedType: "conversation",
    });
  }
}

router.post("/conversations/:id/read", requireAuth, async (req, res): Promise<void> => {
  const params = MarkConversationReadParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const userId = req.session.userId!;

  if (!(await isConversationMember(params.data.id, userId))) {
    res.status(403).json({ error: "Forbidden" });
    return;
  }

  const unreadMessages = await db
    .select({ id: messagesTable.id })
    .from(messagesTable)
    .where(
      and(
        eq(messagesTable.conversationId, params.data.id),
        sql`${messagesTable.id} NOT IN (
          SELECT message_id FROM message_reads WHERE user_id = ${userId}
        )`,
      ),
    );

  if (unreadMessages.length > 0) {
    await db.insert(messageReadsTable).values(
      unreadMessages.map((m) => ({ messageId: m.id, userId })),
    ).onConflictDoNothing();

    const { io } = await import("../index.js");
    io.to(`conversation:${params.data.id}`).emit("messages_read", {
      conversationId: params.data.id,
      userId,
    });
  }

  res.json({ success: true });
});

export default router;
