import "dotenv/config";

import { prisma } from "./db";
import { sendBarkPush } from "./services/bark";
import { nextDueAt } from "./services/recurrence";

const LOCK_TIMEOUT_MS = 5 * 60 * 1000;

const calcBackoffMs = (attemptNo: number) => {
  const steps = [60_000, 5 * 60_000, 15 * 60_000, 60 * 60_000, 4 * 60 * 60_000];
  return steps[Math.min(attemptNo - 1, steps.length - 1)];
};

const buildOffsets = (offsets: { offsetMinutes: number; channelInapp: boolean; channelBark: boolean }[]) => {
  if (offsets.length === 0) {
    return [{ offsetMinutes: 0, channelInapp: true, channelBark: false }];
  }
  return offsets;
};

const generateNotifications = async (now: Date) => {
  const maxOffset = await prisma.reminderOffset.aggregate({
    _max: { offsetMinutes: true },
  });
  const offsetMinutes = maxOffset._max.offsetMinutes ?? 0;
  const upperBound = new Date(now.getTime() + offsetMinutes * 60 * 1000);

  const todos = await prisma.todo.findMany({
    where: { dueAt: { lte: upperBound } },
    include: { owner: true, recurrenceRule: true },
  });
  const todoOffsets = await prisma.reminderOffset.findMany({
    where: { targetType: "todo", targetId: { in: todos.map((todo) => todo.id) } },
  });
  const todoOffsetsById = todoOffsets.reduce<Record<string, typeof todoOffsets>>((acc, offset) => {
    acc[offset.targetId] = acc[offset.targetId] ?? [];
    acc[offset.targetId].push(offset);
    return acc;
  }, {});

  for (const todo of todos) {
    const offsets = buildOffsets(todoOffsetsById[todo.id] ?? []);
    const data = [];
    for (const offset of offsets) {
      const triggerAt = new Date(todo.dueAt.getTime() - offset.offsetMinutes * 60 * 1000);
      if (triggerAt > now) continue;
      if (offset.channelInapp && todo.owner.inappEnabled) {
        data.push({
          recipientId: todo.ownerId,
          targetType: "todo" as const,
          targetId: todo.id,
          triggerAt,
          channel: "inapp" as const,
        });
      }
      if (offset.channelBark && todo.owner.barkEnabled && todo.owner.barkUrl) {
        data.push({
          recipientId: todo.ownerId,
          targetType: "todo" as const,
          targetId: todo.id,
          triggerAt,
          channel: "bark" as const,
        });
      }
    }

    if (data.length > 0) {
      await prisma.notification.createMany({ data, skipDuplicates: true });
    }

    if (todo.recurrenceRule && todo.dueAt <= now) {
      const next = nextDueAt(todo.dueAt, todo.recurrenceRule);
      await prisma.todo.update({ where: { id: todo.id }, data: { dueAt: next } });
    }
  }

  const tasks = await prisma.reminderTask.findMany({
    where: { dueAt: { lte: upperBound } },
    include: {
      recurrenceRule: true,
      recipients: { include: { user: true } },
    },
  });
  const taskOffsets = await prisma.reminderOffset.findMany({
    where: { targetType: "reminder_task", targetId: { in: tasks.map((task) => task.id) } },
  });
  const taskOffsetsById = taskOffsets.reduce<Record<string, typeof taskOffsets>>((acc, offset) => {
    acc[offset.targetId] = acc[offset.targetId] ?? [];
    acc[offset.targetId].push(offset);
    return acc;
  }, {});

  for (const task of tasks) {
    const offsets = buildOffsets(taskOffsetsById[task.id] ?? []);
    const data = [];
    for (const offset of offsets) {
      const triggerAt = new Date(task.dueAt.getTime() - offset.offsetMinutes * 60 * 1000);
      if (triggerAt > now) continue;
      for (const recipient of task.recipients) {
        const user = recipient.user;
        if (offset.channelInapp && user.inappEnabled) {
          data.push({
            recipientId: user.id,
            targetType: "reminder_task" as const,
            targetId: task.id,
            triggerAt,
            channel: "inapp" as const,
          });
        }
        if (offset.channelBark && user.barkEnabled && user.barkUrl) {
          data.push({
            recipientId: user.id,
            targetType: "reminder_task" as const,
            targetId: task.id,
            triggerAt,
            channel: "bark" as const,
          });
        }
      }
    }

    if (data.length > 0) {
      await prisma.notification.createMany({ data, skipDuplicates: true });
    }

    if (task.recurrenceRule && task.dueAt <= now) {
      const next = nextDueAt(task.dueAt, task.recurrenceRule);
      await prisma.reminderTask.update({ where: { id: task.id }, data: { dueAt: next } });
    }
  }
};

const deliverNotifications = async (now: Date) => {
  const expiredLock = new Date(now.getTime() - LOCK_TIMEOUT_MS);
  const pending = await prisma.notification.findMany({
    where: {
      status: "pending",
      triggerAt: { lte: now },
      OR: [{ lockedAt: null }, { lockedAt: { lt: expiredLock } }],
    },
    include: { recipient: true },
    take: 50,
  });

  for (const notification of pending) {
    const locked = await prisma.notification.updateMany({
      where: { id: notification.id, status: "pending" },
      data: { status: "queued", lockedAt: now },
    });
    if (locked.count === 0) continue;

    try {
      if (notification.channel === "inapp") {
        await prisma.notification.update({
          where: { id: notification.id },
          data: { status: "sent", sentAt: now, lockedAt: null },
        });
        await prisma.deliveryLog.create({
          data: {
            notificationId: notification.id,
            attemptNo: 1,
            channel: "inapp",
            status: "sent",
          },
        });
        continue;
      }

      const recipient = notification.recipient;
      if (!recipient.barkEnabled || !recipient.barkUrl) {
        await prisma.notification.update({
          where: { id: notification.id },
          data: { status: "failed", lockedAt: null },
        });
        await prisma.deliveryLog.create({
          data: {
            notificationId: notification.id,
            attemptNo: 1,
            channel: "bark",
            status: "failed",
            responseMeta: { reason: "bark_disabled" },
          },
        });
        continue;
      }

      const title = "Notify 提醒";
      const body = `触发时间：${notification.triggerAt.toISOString()}`;
      await sendBarkPush(recipient.barkUrl, {
        title,
        body,
        group: "notify",
      });

      await prisma.notification.update({
        where: { id: notification.id },
        data: { status: "sent", sentAt: now, lockedAt: null },
      });
      await prisma.deliveryLog.create({
        data: {
          notificationId: notification.id,
          attemptNo: 1,
          channel: "bark",
          status: "sent",
        },
      });
    } catch (error) {
      const logs = await prisma.deliveryLog.findMany({
        where: { notificationId: notification.id },
        orderBy: { attemptNo: "desc" },
        take: 1,
      });
      const attemptNo = (logs[0]?.attemptNo ?? 0) + 1;
      const shouldRetry = attemptNo < 5;
      const retryAt = new Date(now.getTime() + calcBackoffMs(attemptNo));

      await prisma.notification.update({
        where: { id: notification.id },
        data: {
          status: shouldRetry ? "pending" : "failed",
          lockedAt: null,
          triggerAt: shouldRetry ? retryAt : notification.triggerAt,
        },
      });
      await prisma.deliveryLog.create({
        data: {
          notificationId: notification.id,
          attemptNo,
          channel: notification.channel,
          status: shouldRetry ? "pending" : "failed",
          responseMeta: { message: (error as Error).message },
        },
      });
    }
  }
};

const loop = async () => {
  const now = new Date();
  await generateNotifications(now);
  await deliverNotifications(now);
};

const start = async () => {
  // eslint-disable-next-line no-console
  console.log("Notify worker started");
  await loop();
  setInterval(loop, 30 * 1000);
};

start().catch((error) => {
  // eslint-disable-next-line no-console
  console.error("Worker error", error);
  process.exit(1);
});
