import { Router } from "express";
import { z } from "zod";
import { logActivity } from "../lib/activity";
import { runCardMovedAutomations } from "../lib/automations";
import { requireBoardAccess } from "../lib/authz";
import { notifyUser } from "../lib/notifications";
import { positionBetween } from "../lib/position";
import { prisma } from "../lib/prisma";
import { emitToBoard } from "../lib/socketBus";
import { uploadAttachment } from "../lib/upload";
import { HttpError } from "../middleware/errorHandler";
import { requireAuth } from "../middleware/requireAuth";

export const cardsRouter = Router();
cardsRouter.use(requireAuth);

const cardDetailInclude = {
  labels: { include: { label: true } },
  members: { include: { user: true } },
  checklists: { include: { items: { include: { assignees: { include: { user: true } } } } } },
  comments: {
    include: { author: true, reactions: true, mentions: true, seenBy: true },
    orderBy: { createdAt: "asc" as const },
  },
  attachments: { include: { uploader: true } },
  customFieldValues: { include: { customField: true } },
  portalAnswers: true,
  status: true,
};

const createCardSchema = z.object({
  listId: z.string().min(1),
  title: z.string().min(1).max(200),
});

cardsRouter.post("/", async (req, res, next) => {
  try {
    const data = createCardSchema.parse(req.body);
    const list = await prisma.list.findUnique({ where: { id: data.listId } });
    if (!list) throw new HttpError(404, "List not found");
    await requireBoardAccess(req.userId!, list.boardId, "MEMBER");

    const last = await prisma.card.findFirst({
      where: { listId: data.listId, isArchived: false },
      orderBy: { position: "desc" },
    });

    const card = await prisma.card.create({
      data: {
        listId: data.listId,
        boardId: list.boardId,
        title: data.title,
        position: positionBetween(last?.position, undefined),
        // Auto-assign the creator — otherwise a brand-new card with no members
        // instantly vanishes for anyone viewing the board filtered to themself.
        members: { create: { userId: req.userId! } },
      },
      include: cardDetailInclude,
    });

    await logActivity({
      boardId: list.boardId,
      cardId: card.id,
      actorId: req.userId!,
      action: "card.created",
      metadata: { title: card.title },
    });

    emitToBoard(list.boardId, "card:created", { card });
    res.status(201).json({ card });
  } catch (err) {
    next(err);
  }
});

cardsRouter.get("/:id", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "VIEWER");
    res.json({ card });
  } catch (err) {
    next(err);
  }
});

// Marks every comment currently on the card as seen by the caller — fired when
// the card detail modal opens, so other viewers can see who's read the thread.
cardsRouter.post("/:id/comments/seen", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id }, select: { boardId: true } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "VIEWER");

    const comments = await prisma.comment.findMany({
      where: { cardId: req.params.id, authorId: { not: req.userId! } },
      select: { id: true },
    });
    if (comments.length > 0) {
      await prisma.commentSeen.createMany({
        data: comments.map((c) => ({ commentId: c.id, userId: req.userId! })),
        skipDuplicates: true,
      });
    }

    const updated = await prisma.card.findUnique({ where: { id: req.params.id }, include: cardDetailInclude });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.json({ card: updated });
  } catch (err) {
    next(err);
  }
});

const updateCardSchema = z.object({
  title: z.string().min(1).max(200).optional(),
  description: z.string().nullable().optional(),
  listId: z.string().optional(),
  beforePosition: z.number().optional(),
  afterPosition: z.number().optional(),
  startDate: z.string().datetime().nullable().optional(),
  dueDate: z.string().datetime().nullable().optional(),
  isCompleted: z.boolean().optional(),
  isArchived: z.boolean().optional(),
  coverColor: z.string().nullable().optional(),
  coverImageUrl: z.string().nullable().optional(),
  statusOptionId: z.string().nullable().optional(),
});

cardsRouter.patch("/:id", async (req, res, next) => {
  try {
    const existing = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!existing) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, existing.boardId, "MEMBER");

    const data = updateCardSchema.parse(req.body);
    const { beforePosition, afterPosition, listId, ...rest } = data;

    const movedList = listId !== undefined && listId !== existing.listId;
    const statusChanged =
      rest.statusOptionId !== undefined && rest.statusOptionId !== existing.statusOptionId;
    const dueDateChanged =
      rest.dueDate !== undefined && rest.dueDate !== existing.dueDate?.toISOString();

    await prisma.card.update({
      where: { id: req.params.id },
      data: {
        ...rest,
        ...(dueDateChanged ? { dueDateNotifiedAt: null } : {}),
        ...(listId !== undefined ? { listId } : {}),
        ...(beforePosition !== undefined || afterPosition !== undefined
          ? { position: positionBetween(beforePosition, afterPosition) }
          : {}),
      },
    });

    if (movedList) {
      await logActivity({
        boardId: existing.boardId,
        cardId: req.params.id,
        actorId: req.userId!,
        action: "card.moved",
        metadata: { fromListId: existing.listId, toListId: listId },
      });
      await runCardMovedAutomations(req.params.id, existing.boardId, listId!, req.userId!);
    }

    if (statusChanged) {
      await logActivity({
        boardId: existing.boardId,
        cardId: req.params.id,
        actorId: req.userId!,
        action: "card.status_changed",
        metadata: { fromStatusOptionId: existing.statusOptionId, toStatusOptionId: rest.statusOptionId },
      });
    }

    const card = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });

    emitToBoard(existing.boardId, "card:updated", { card });
    res.json({ card });
  } catch (err) {
    next(err);
  }
});

cardsRouter.delete("/:id", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    await prisma.card.delete({ where: { id: req.params.id } });
    emitToBoard(card.boardId, "card:deleted", { cardId: card.id, listId: card.listId });
    res.status(204).send();
  } catch (err) {
    next(err);
  }
});

cardsRouter.post("/:id/duplicate", async (req, res, next) => {
  try {
    const original = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: {
        labels: true,
        members: true,
        checklists: { include: { items: true } },
      },
    });
    if (!original) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, original.boardId, "MEMBER");

    const last = await prisma.card.findFirst({
      where: { listId: original.listId, isArchived: false },
      orderBy: { position: "desc" },
    });

    const copy = await prisma.card.create({
      data: {
        listId: original.listId,
        boardId: original.boardId,
        title: `${original.title} (copy)`,
        description: original.description,
        position: positionBetween(last?.position, undefined),
        coverColor: original.coverColor,
        statusOptionId: original.statusOptionId,
        labels: { create: original.labels.map((l) => ({ labelId: l.labelId })) },
        members: { create: original.members.map((m) => ({ userId: m.userId })) },
        checklists: {
          create: original.checklists.map((cl) => ({
            title: cl.title,
            position: cl.position,
            items: {
              create: cl.items.map((item) => ({
                text: item.text,
                position: item.position,
              })),
            },
          })),
        },
      },
      include: cardDetailInclude,
    });

    emitToBoard(original.boardId, "card:created", { card: copy });
    res.status(201).json({ card: copy });
  } catch (err) {
    next(err);
  }
});

// ---------- Labels on card ----------

cardsRouter.post("/:id/labels/:labelId", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    await prisma.cardLabel.upsert({
      where: { cardId_labelId: { cardId: req.params.id, labelId: req.params.labelId } },
      update: {},
      create: { cardId: req.params.id, labelId: req.params.labelId },
    });

    const updated = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.status(201).json({ card: updated });
  } catch (err) {
    next(err);
  }
});

cardsRouter.delete("/:id/labels/:labelId", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    await prisma.cardLabel.delete({
      where: { cardId_labelId: { cardId: req.params.id, labelId: req.params.labelId } },
    });

    const updated = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.json({ card: updated });
  } catch (err) {
    next(err);
  }
});

// ---------- Members on card ----------

cardsRouter.post("/:id/members/:userId", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    await prisma.cardMember.upsert({
      where: { cardId_userId: { cardId: req.params.id, userId: req.params.userId } },
      update: {},
      create: { cardId: req.params.id, userId: req.params.userId },
    });

    await logActivity({
      boardId: card.boardId,
      cardId: card.id,
      actorId: req.userId!,
      action: "card.member_added",
      metadata: { userId: req.params.userId },
    });

    const updated = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });

    await notifyUser({
      recipientId: req.params.userId,
      actorId: req.userId!,
      type: "ASSIGNMENT",
      message: `You were added to "${card.title}"`,
      cardId: card.id,
      boardId: card.boardId,
    });

    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.status(201).json({ card: updated });
  } catch (err) {
    next(err);
  }
});

cardsRouter.delete("/:id/members/:userId", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    await prisma.cardMember.delete({
      where: { cardId_userId: { cardId: req.params.id, userId: req.params.userId } },
    });

    const updated = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.json({ card: updated });
  } catch (err) {
    next(err);
  }
});

// ---------- Checklists ----------

const createChecklistSchema = z.object({ title: z.string().min(1).max(100) });

cardsRouter.post("/:id/checklists", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    const data = createChecklistSchema.parse(req.body);
    const last = await prisma.checklist.findFirst({
      where: { cardId: req.params.id },
      orderBy: { position: "desc" },
    });

    await prisma.checklist.create({
      data: { cardId: req.params.id, title: data.title, position: (last?.position ?? 0) + 1000 },
    });

    const updated = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.status(201).json({ card: updated });
  } catch (err) {
    next(err);
  }
});

// ---------- Comments ----------

const createCommentSchema = z.object({
  bodyHtml: z.string().min(1),
  mentionUserIds: z.array(z.string()).optional(),
});

cardsRouter.post("/:id/comments", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    const data = createCommentSchema.parse(req.body);

    const comment = await prisma.comment.create({
      data: {
        cardId: req.params.id,
        authorId: req.userId!,
        bodyHtml: data.bodyHtml,
        mentions: data.mentionUserIds
          ? { create: data.mentionUserIds.map((userId) => ({ cardId: req.params.id, userId })) }
          : undefined,
      },
    });

    await logActivity({
      boardId: card.boardId,
      cardId: card.id,
      actorId: req.userId!,
      action: "comment.created",
      metadata: { commentId: comment.id },
    });

    const author = await prisma.user.findUnique({ where: { id: req.userId! } });

    for (const userId of data.mentionUserIds ?? []) {
      await notifyUser({
        recipientId: userId,
        actorId: req.userId!,
        type: "MENTION",
        message: `${author?.name ?? "Someone"} mentioned you in "${card.title}"`,
        cardId: card.id,
        boardId: card.boardId,
      });
    }

    const followerIds = new Set<string>();
    const commenters = await prisma.comment.findMany({
      where: { cardId: req.params.id },
      select: { authorId: true },
      distinct: ["authorId"],
    });
    const members = await prisma.cardMember.findMany({
      where: { cardId: req.params.id },
      select: { userId: true },
    });
    for (const c of commenters) followerIds.add(c.authorId);
    for (const m of members) followerIds.add(m.userId);
    followerIds.delete(req.userId!);
    for (const mentioned of data.mentionUserIds ?? []) followerIds.delete(mentioned);

    for (const userId of followerIds) {
      await notifyUser({
        recipientId: userId,
        actorId: req.userId!,
        type: "COMMENT",
        message: `${author?.name ?? "Someone"} commented on "${card.title}"`,
        cardId: card.id,
        boardId: card.boardId,
      });
    }

    const updated = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.status(201).json({ card: updated });
  } catch (err) {
    next(err);
  }
});

// ---------- Attachments ----------

const createLinkAttachmentSchema = z.object({
  type: z.literal("link"),
  url: z.string().url(),
  filename: z.string().optional(),
});

cardsRouter.post("/:id/attachments", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    const data = createLinkAttachmentSchema.parse(req.body);
    await prisma.attachment.create({
      data: {
        cardId: req.params.id,
        uploaderId: req.userId!,
        type: "link",
        url: data.url,
        filename: data.filename,
      },
    });

    const updated = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.status(201).json({ card: updated });
  } catch (err) {
    next(err);
  }
});

cardsRouter.post(
  "/:id/attachments/file",
  uploadAttachment.single("file"),
  async (req, res, next) => {
    try {
      const card = await prisma.card.findUnique({ where: { id: req.params.id } });
      if (!card) throw new HttpError(404, "Card not found");
      await requireBoardAccess(req.userId!, card.boardId, "MEMBER");
      if (!req.file) throw new HttpError(400, "No file uploaded");

      await prisma.attachment.create({
        data: {
          cardId: req.params.id,
          uploaderId: req.userId!,
          type: "file",
          url: `/uploads/attachments/${req.file.filename}`,
          filename: req.file.originalname,
          mimeType: req.file.mimetype,
          sizeBytes: req.file.size,
        },
      });

      const updated = await prisma.card.findUnique({
        where: { id: req.params.id },
        include: cardDetailInclude,
      });
      emitToBoard(card.boardId, "card:updated", { card: updated });
      res.status(201).json({ card: updated });
    } catch (err) {
      next(err);
    }
  }
);

// ---------- Cover image ----------

cardsRouter.post("/:id/cover", uploadAttachment.single("image"), async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");
    if (!req.file) throw new HttpError(400, "No file uploaded");

    const updated = await prisma.card.update({
      where: { id: req.params.id },
      data: {
        coverImageUrl: `/uploads/attachments/${req.file.filename}`,
        coverColor: null,
      },
      include: cardDetailInclude,
    });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.json({ card: updated });
  } catch (err) {
    next(err);
  }
});

// ---------- Activity ----------

cardsRouter.get("/:id/activity", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "VIEWER");

    const activity = await prisma.activityLog.findMany({
      where: { cardId: req.params.id },
      include: { actor: true },
      orderBy: { createdAt: "desc" },
      take: 50,
    });
    res.json({ activity });
  } catch (err) {
    next(err);
  }
});

// ---------- Custom field values ----------

const setCustomFieldValueSchema = z.object({ value: z.unknown() });

cardsRouter.put("/:id/custom-fields/:fieldId", async (req, res, next) => {
  try {
    const card = await prisma.card.findUnique({ where: { id: req.params.id } });
    if (!card) throw new HttpError(404, "Card not found");
    await requireBoardAccess(req.userId!, card.boardId, "MEMBER");

    const { value } = setCustomFieldValueSchema.parse(req.body);

    await prisma.customFieldValue.upsert({
      where: { cardId_customFieldId: { cardId: req.params.id, customFieldId: req.params.fieldId } },
      update: { value: value as never },
      create: { cardId: req.params.id, customFieldId: req.params.fieldId, value: value as never },
    });

    const updated = await prisma.card.findUnique({
      where: { id: req.params.id },
      include: cardDetailInclude,
    });
    emitToBoard(card.boardId, "card:updated", { card: updated });
    res.json({ card: updated });
  } catch (err) {
    next(err);
  }
});
