import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createNotification } from "../lib/create-notification.js";
import { sendEmail } from "../lib/email.js";
import { emitTaskAssigned } from "../sse/event-bus.js";
import type { TRPCContext } from "../trpc.js";

export type NotificationProcedureContext = Pick<TRPCContext, "db" | "dbUser" | "roleDefaults" | "session">;

export function requireNotificationDbUser(ctx: NotificationProcedureContext) {
  if (!ctx.dbUser) {
    throw new TRPCError({ code: "UNAUTHORIZED", message: "User account not found" });
  }

  return ctx.dbUser;
}

export async function resolveUserId(ctx: {
  db: {
    user: {
      findUnique: (args: {
        where: { email: string };
        select: { id: true };
      }) => Promise<{ id: string } | null>;
    };
  };
  session: { user?: { email?: string | null } | null } | null;
}): Promise<string> {
  const email = ctx.session?.user?.email;
  if (!email) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }

  const user = await ctx.db.user.findUnique({
    where: { email },
    select: { id: true },
  });
  if (!user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }

  return user.id;
}

export function getNotificationErrorCandidates(error: unknown): Array<{
  code?: unknown;
  message?: unknown;
  meta?: { field_name?: unknown; modelName?: unknown };
  cause?: unknown;
}> {
  const queue: unknown[] = [error];
  const seen = new Set<unknown>();
  const candidates: Array<{
    code?: unknown;
    message?: unknown;
    meta?: { field_name?: unknown; modelName?: unknown };
    cause?: unknown;
  }> = [];

  while (queue.length > 0) {
    const current = queue.shift();
    if (!current || seen.has(current) || typeof current !== "object") {
      continue;
    }
    seen.add(current);

    const candidate = current as {
      code?: unknown;
      message?: unknown;
      meta?: { field_name?: unknown; modelName?: unknown };
      cause?: unknown;
      shape?: { message?: unknown; data?: { cause?: unknown } };
    };
    candidates.push(candidate);

    if ("cause" in candidate) {
      queue.push(candidate.cause);
    }
    if (candidate.shape?.data?.cause) {
      queue.push(candidate.shape.data.cause);
    }
  }

  return candidates;
}

export function rethrowNotificationReferenceError(
  error: unknown,
  recipientContext: "notification" | "task" | "broadcast" = "notification",
): never {
  for (const candidate of getNotificationErrorCandidates(error)) {
    const fieldName = typeof candidate.meta?.field_name === "string"
      ? candidate.meta.field_name.toLowerCase()
      : "";
    const modelName = typeof candidate.meta?.modelName === "string"
      ? candidate.meta.modelName.toLowerCase()
      : "";

    if (
      typeof candidate.code === "string"
      && (candidate.code === "P2003" || candidate.code === "P2025")
      && fieldName.includes("assignee")
    ) {
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "Assignee user not found",
        cause: error,
      });
    }

    if (
      typeof candidate.code === "string"
      && (candidate.code === "P2003" || candidate.code === "P2025")
      && fieldName.includes("sender")
    ) {
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "Sender user not found",
        cause: error,
      });
    }

    if (
      typeof candidate.code === "string"
      && (candidate.code === "P2003" || candidate.code === "P2025")
      && fieldName.includes("userid")
    ) {
      const message = recipientContext === "broadcast"
        ? "Broadcast recipient user not found"
        : recipientContext === "task"
          ? "Task recipient user not found"
          : "Notification recipient user not found";
      throw new TRPCError({
        code: "NOT_FOUND",
        message,
        cause: error,
      });
    }

    if (
      typeof candidate.code === "string"
      && (candidate.code === "P2003" || candidate.code === "P2025")
      && (
        modelName.includes("notificationbroadcast")
        || fieldName.includes("broadcast")
        || fieldName.includes("sourceid")
      )
    ) {
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "Notification broadcast not found",
        cause: error,
      });
    }
  }

  throw error;
}

export const categoryEnum = z.enum(["NOTIFICATION", "REMINDER", "TASK", "APPROVAL"]);
export const priorityEnum = z.enum(["LOW", "NORMAL", "HIGH", "URGENT"]);
export const taskStatusEnum = z.enum(["OPEN", "IN_PROGRESS", "DONE", "DISMISSED"]);
export const channelEnum = z.enum(["in_app", "email", "both"]);
export const recurrenceEnum = z.enum(["daily", "weekly", "monthly"]);
export const targetTypeEnum = z.enum(["user", "role", "project", "orgUnit", "all"]);
export const SCHEDULED_TASK_BROADCAST_UNSUPPORTED_MESSAGE =
  "Scheduled broadcasts with task metadata are not supported yet.";

export const NotificationListInputSchema = z.object({
  unreadOnly: z.boolean().optional(),
  category: categoryEnum.optional(),
  taskStatus: taskStatusEnum.optional(),
  priority: priorityEnum.optional(),
  limit: z.number().min(1).max(100).default(50),
});

export const MarkNotificationReadInputSchema = z.object({
  id: z.string().optional(),
});

export const CreateManagedNotificationInputSchema = z.object({
  userId: z.string(),
  type: z.string(),
  title: z.string(),
  body: z.string().optional(),
  entityId: z.string().optional(),
  entityType: z.string().optional(),
  category: categoryEnum.optional(),
  priority: priorityEnum.optional(),
  link: z.string().optional(),
  taskStatus: taskStatusEnum.optional(),
  taskAction: z.string().optional(),
  assigneeId: z.string().optional(),
  dueDate: z.date().optional(),
  channel: channelEnum.optional(),
  senderId: z.string().optional(),
});

export const ListNotificationTasksInputSchema = z.object({
  status: taskStatusEnum.optional(),
  includeAssigned: z.boolean().default(true),
  limit: z.number().min(1).max(100).default(20),
});

export const NotificationIdInputSchema = z.object({
  id: z.string(),
});

export const UpdateNotificationTaskStatusInputSchema = z.object({
  id: z.string(),
  status: taskStatusEnum,
});

export const CreateReminderInputSchema = z.object({
  title: z.string().min(1).max(200),
  body: z.string().max(2000).optional(),
  remindAt: z.date(),
  recurrence: recurrenceEnum.optional(),
  entityId: z.string().optional(),
  entityType: z.string().optional(),
  link: z.string().optional(),
});

export const UpdateReminderInputSchema = z.object({
  id: z.string(),
  title: z.string().min(1).max(200).optional(),
  body: z.string().max(2000).optional(),
  remindAt: z.date().optional(),
  recurrence: recurrenceEnum.nullish(),
});

export const ListRemindersInputSchema = z.object({
  limit: z.number().min(1).max(100).default(20),
});

export const CreateBroadcastInputSchema = z.object({
  title: z.string().min(1).max(200),
  body: z.string().max(2000).optional(),
  link: z.string().optional(),
  category: categoryEnum.default("NOTIFICATION"),
  priority: priorityEnum.default("NORMAL"),
  channel: channelEnum.default("in_app"),
  targetType: targetTypeEnum,
  targetValue: z.string().optional(),
  scheduledAt: z.date().optional(),
  taskAction: z.string().optional(),
  dueDate: z.date().optional(),
});

export const ListBroadcastsInputSchema = z.object({
  limit: z.number().min(1).max(50).default(20),
});

export const CreateTaskInputSchema = z.object({
  userId: z.string(),
  title: z.string().min(1).max(200),
  body: z.string().max(2000).optional(),
  priority: priorityEnum.default("NORMAL"),
  dueDate: z.date().optional(),
  taskAction: z.string().optional(),
  entityId: z.string().optional(),
  entityType: z.string().optional(),
  link: z.string().optional(),
  channel: channelEnum.default("in_app"),
});

export const AssignTaskInputSchema = z.object({
  id: z.string(),
  assigneeId: z.string(),
});

export type BroadcastRecipientNotification = { id: string; userId: string };
export type BroadcastPersistenceDb = NotificationProcedureContext["db"];

export async function sendNotificationEmail(
  db: {
    user: {
      findUnique: (args: {
        where: { id: string };
        select: { email: true; name: true };
      }) => Promise<{ email: string; name: string | null } | null>;
    };
  },
  userId: string,
  title: string,
  body?: string | null,
): Promise<void> {
  try {
    const user = await db.user.findUnique({
      where: { id: userId },
      select: { email: true, name: true },
    });
    if (!user) {
      return;
    }

    void sendEmail({
      to: user.email,
      subject: title,
      text: body ?? title,
      ...(body !== undefined && body !== null ? { html: `<p>${body}</p>` } : {}),
    });
  } catch {
    // non-blocking
  }
}

export async function listNotifications(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof NotificationListInputSchema>,
) {
  const userId = await resolveUserId(ctx);
  return ctx.db.notification.findMany({
    where: {
      userId,
      ...(input.unreadOnly ? { readAt: null } : {}),
      ...(input.category !== undefined ? { category: input.category } : {}),
      ...(input.taskStatus !== undefined ? { taskStatus: input.taskStatus } : {}),
      ...(input.priority !== undefined ? { priority: input.priority } : {}),
    },
    orderBy: { createdAt: "desc" },
    take: input.limit,
  });
}

export async function countUnreadNotifications(ctx: NotificationProcedureContext) {
  const userId = await resolveUserId(ctx);
  return ctx.db.notification.count({
    where: { userId, readAt: null },
  });
}

export async function markNotificationsRead(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof MarkNotificationReadInputSchema>,
) {
  const userId = await resolveUserId(ctx);
  const now = new Date();
  if (input.id) {
    await ctx.db.notification.update({
      where: { id: input.id, userId },
      data: { readAt: now },
    });
    return;
  }

  await ctx.db.notification.updateMany({
    where: { userId, readAt: null },
    data: { readAt: now },
  });
}

export async function createManagedNotification(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof CreateManagedNotificationInputSchema>,
) {
  const currentUserId = requireNotificationDbUser(ctx).id;
  const isTaskLikeCategory = input.category === "TASK" || input.category === "APPROVAL";
  const taskStatus = input.taskStatus ?? (isTaskLikeCategory ? "OPEN" : undefined);

  let notificationId: string;
  try {
    notificationId = await createNotification({
      db: ctx.db,
      userId: input.userId,
      type: input.type,
      title: input.title,
      body: input.body,
      entityId: input.entityId,
      entityType: input.entityType,
      category: input.category,
      priority: input.priority,
      link: input.link,
      taskStatus,
      taskAction: input.taskAction,
      assigneeId: input.assigneeId,
      dueDate: input.dueDate,
      channel: input.channel,
      senderId: input.senderId ?? currentUserId,
    });
  } catch (error) {
    rethrowNotificationReferenceError(error, "notification");
  }

  if (input.category === "TASK" || input.category === "APPROVAL") {
    emitTaskAssigned(input.userId, notificationId);
  }

  const channel = input.channel ?? "in_app";
  if (channel === "email" || channel === "both") {
    void sendNotificationEmail(ctx.db, input.userId, input.title, input.body);
  }

  return ctx.db.notification.findUnique({ where: { id: notificationId } });
}

export async function deleteNotification(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof NotificationIdInputSchema>,
) {
  const userId = await resolveUserId(ctx);
  const existing = await ctx.db.notification.findFirst({
    where: { id: input.id, userId },
  });

  if (!existing) {
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Notification not found",
    });
  }

  if (
    (existing.category === "TASK" || existing.category === "APPROVAL")
    && existing.senderId
    && existing.senderId !== userId
  ) {
    throw new TRPCError({
      code: "FORBIDDEN",
      message: "Cannot delete tasks created by others",
    });
  }

  await ctx.db.notification.delete({ where: { id: input.id } });
}
