"use server";

import { revalidatePath } from "next/cache";
import { hasPermission } from "@/lib/adminAuth";
import { getSessionClientId } from "@/lib/auth";
import { connectDB } from "@/lib/db";
import { Todo, TODO_STATUSES, TODO_PRIORITIES, TODO_RECURRENCES, type TodoStatus, type TodoPriority, type TodoRecurrence } from "@/models/Todo";
import { TodoTemplate } from "@/models/TodoTemplate";
import { Client } from "@/models/Client";
import { notifyRecipients } from "@/lib/notifications";
import { saveTodoAttachments, deleteTodoAttachmentFile } from "@/lib/uploads";
import { STATUS_LABELS } from "@/lib/todoConstants";

const isObjId = (s: string) => /^[a-f0-9]{24}$/i.test(s);

// Kommagetrennte oder Mehrfach-Werte eines Formularfelds -> saubere ID-Liste.
function parseIdList(formData: FormData, key: string): string[] {
  const raw = [...formData.getAll(key)].flatMap((v) => String(v).split(","));
  return [...new Set(raw.map((s) => s.trim()).filter(isObjId))];
}

// Alle Personen rund um ein ToDo (Haupt-/Zusatz-Zuständige, Beobachter,
// Ersteller) — für Benachrichtigungen. `except` = Auslöser der Aktion.
function todoAudience(
  todo: { assignedToId: unknown; additionalAssigneeIds?: unknown[]; watcherIds?: unknown[]; createdById: unknown },
  except: string
): string[] {
  const ids = [
    String(todo.assignedToId),
    ...(todo.additionalAssigneeIds || []).map(String),
    ...(todo.watcherIds || []).map(String),
    String(todo.createdById),
  ];
  return [...new Set(ids)].filter((id) => id && id !== except);
}

async function notifyTodo(recipientIds: string[], title: string, body: string, todoId: string) {
  const ids = [...new Set(recipientIds)].filter(Boolean);
  if (!ids.length) return;
  await notifyRecipients(ids, { type: "todo", title, body, link: `/de/admin/todos#${todoId}` });
}

async function logActivity(todoId: string, type: string, actorId: string, text: string) {
  await Todo.findByIdAndUpdate(todoId, {
    $push: { activity: { type, actorId, text: text.slice(0, 500) } },
  });
}

// "@Vorname" im Notiztext -> Client-IDs (Mitarbeiter, längster Namens-Match).
async function resolveMentions(text: string): Promise<string[]> {
  if (!text.includes("@")) return [];
  const staff = await Client.find({ role: "staff" }).select("name").lean();
  const hits = new Set<string>();
  const lower = text.toLowerCase();
  for (const s of staff) {
    const first = (s.name || "").split(/\s+/)[0].toLowerCase();
    if (first && lower.includes(`@${first}`)) hits.add(String(s._id));
    if (s.name && lower.includes(`@${s.name.toLowerCase()}`)) hits.add(String(s._id));
  }
  return [...hits];
}

function nextDueDate(from: Date, recurrence: TodoRecurrence, interval = 1, weekdays: number[] = []): Date {
  const step = Math.min(52, Math.max(1, Math.round(interval || 1)));
  const next = new Date(from);
  if (recurrence === "daily") {
    next.setDate(next.getDate() + step);
  } else if (recurrence === "weekly") {
    const days = [...new Set((weekdays || []).filter((d) => d >= 0 && d <= 6))];
    if (days.length) {
      for (let i = 1; i <= 14; i++) {
        const cand = new Date(from);
        cand.setDate(cand.getDate() + i);
        if (days.includes(cand.getDay())) return cand;
      }
      next.setDate(next.getDate() + 7 * step);
    } else {
      next.setDate(next.getDate() + 7 * step);
    }
  } else if (recurrence === "monthly") {
    next.setMonth(next.getMonth() + step);
  }
  return next;
}

function parseWeekdays(raw: FormData): number[] {
  return [...new Set([...raw.getAll("recurrenceWeekdays")].map((v) => Number(v)).filter((n) => n >= 0 && n <= 6))];
}

const PATH = "/de/admin/todos";

// "Label1, Label2" -> saubere, deduplizierte Liste (max 10, je 40 Zeichen).
function parseLabels(raw: FormDataEntryValue | null): string[] {
  const seen = new Set<string>();
  return String(raw || "")
    .split(",")
    .map((s) => s.trim().slice(0, 40))
    .filter(Boolean)
    .filter((s) => {
      const k = s.toLowerCase();
      if (seen.has(k)) return false;
      seen.add(k);
      return true;
    })
    .slice(0, 10);
}

// Eigene ToDos (zugewiesen ODER selbst angelegt) darf man immer bearbeiten/
// löschen/den Status ändern — fremde nur mit der jeweiligen Berechtigung.
async function canManage(todoId: string, permission: "todos_edit" | "todos_delete") {
  const viewerId = await getSessionClientId();
  if (!viewerId) return null;
  await connectDB();
  const todo = await Todo.findById(todoId)
    .select("assignedToId additionalAssigneeIds watcherIds createdById dueDate")
    .lean();
  if (!todo) return null;
  const isOwn =
    String(todo.assignedToId) === viewerId ||
    String(todo.createdById) === viewerId ||
    (todo.additionalAssigneeIds || []).map(String).includes(viewerId);
  if (!isOwn && !(await hasPermission(permission))) return null;
  return { todo, viewerId };
}

type FormState = { error?: string; success?: boolean };

export async function createTodoAction(_prev: FormState, formData: FormData): Promise<FormState> {
  const viewerId = await getSessionClientId();
  if (!viewerId) return { error: "Bitte logge dich ein." };

  const title = String(formData.get("title") || "").trim();
  if (!title) return { error: "Bitte einen Titel eingeben." };

  const description = String(formData.get("description") || "").trim();
  const assignedToId = String(formData.get("assignedToId") || "").trim() || viewerId;
  const priorityRaw = String(formData.get("priority") || "normal");
  const priority: TodoPriority = TODO_PRIORITIES.includes(priorityRaw as TodoPriority) ? (priorityRaw as TodoPriority) : "normal";
  const dueDateRaw = String(formData.get("dueDate") || "");
  const dueDate = dueDateRaw ? new Date(dueDateRaw) : null;
  const orderId = String(formData.get("orderId") || "").trim() || null;
  const recurrenceRaw = String(formData.get("recurrence") || "none");
  const recurrence: TodoRecurrence = TODO_RECURRENCES.includes(recurrenceRaw as TodoRecurrence)
    ? (recurrenceRaw as TodoRecurrence)
    : "none";

  // Für andere ein ToDo anlegen ist eine gezielte Zuweisung, braucht die
  // Berechtigung — sich selbst welche anzulegen geht immer.
  if (assignedToId !== viewerId && !(await hasPermission("todos_create"))) {
    return { error: "Keine Berechtigung, ToDos für andere anzulegen." };
  }

  const subtasks = String(formData.get("subtasks") || "")
    .split("\n")
    .map((s) => s.trim().slice(0, 300))
    .filter(Boolean)
    .slice(0, 40)
    .map((text) => ({ text, done: false }));

  const additionalAssigneeIds = parseIdList(formData, "additionalAssigneeIds").filter(
    (id) => id !== assignedToId
  );
  const watcherIds = parseIdList(formData, "watcherIds").filter(
    (id) => id !== assignedToId && !additionalAssigneeIds.includes(id)
  );

  const recurrenceInterval = Math.min(52, Math.max(1, Math.round(Number(formData.get("recurrenceInterval")) || 1)));
  const recurrenceWeekdays = recurrence === "weekly" ? parseWeekdays(formData) : [];
  const recEndRaw = String(formData.get("recurrenceEndDate") || "");
  const recEnd = recEndRaw ? new Date(recEndRaw) : null;

  await connectDB();
  const created = await Todo.create({
    title,
    description,
    assignedToId,
    additionalAssigneeIds,
    watcherIds,
    createdById: viewerId,
    status: "open",
    priority,
    dueDate: dueDate && !Number.isNaN(dueDate.getTime()) ? dueDate : null,
    orderId,
    labels: parseLabels(formData.get("labels")),
    project: String(formData.get("project") || "").trim().slice(0, 80),
    subtasks,
    recurrence,
    recurrenceInterval,
    recurrenceWeekdays,
    recurrenceEndDate: recEnd && !Number.isNaN(recEnd.getTime()) ? recEnd : null,
    activity: [{ type: "created", actorId: viewerId, text: "ToDo angelegt" }],
  });

  const notify = [assignedToId, ...additionalAssigneeIds, ...watcherIds].filter((id) => id !== viewerId);
  await notifyTodo(notify, `Neues ToDo: ${title}`, "Du wurdest einem ToDo zugeordnet.", String(created._id));

  revalidatePath(PATH);
  return { success: true };
}

export async function updateTodoStatusAction(todoId: string, status: TodoStatus) {
  if (!TODO_STATUSES.includes(status)) return;
  const ctx = await canManage(todoId, "todos_edit");
  if (!ctx) return;

  const before = await Todo.findById(todoId)
    .select("status title assignedToId additionalAssigneeIds watcherIds createdById")
    .lean();

  await Todo.findByIdAndUpdate(todoId, {
    status,
    completedAt: status === "done" ? new Date() : null,
  });

  if (before && before.status !== status) {
    await logActivity(todoId, "status", ctx.viewerId, `Status → ${STATUS_LABELS[status]}`);
    await notifyTodo(
      todoAudience(before, ctx.viewerId),
      `ToDo: ${before.title}`,
      `Status → ${STATUS_LABELS[status]}`,
      todoId
    );
  }

  // Wiederkehrende Aufgabe erledigt → nächste Instanz sofort erzeugen, statt
  // auf einen separaten Cron mit Dedup-Logik zu warten. Neue Instanz startet
  // wieder "open" mit verschobenem Fälligkeitsdatum (oder ab heute, falls
  // keins gesetzt war).
  if (status === "done") {
    const full = await Todo.findById(todoId)
      .select(
        "title description assignedToId additionalAssigneeIds watcherIds priority dueDate orderId labels project subtasks recurrence recurrenceInterval recurrenceWeekdays recurrenceEndDate"
      )
      .lean();
    if (full && full.recurrence !== "none") {
      const nd = nextDueDate(
        full.dueDate || new Date(),
        full.recurrence,
        full.recurrenceInterval || 1,
        full.recurrenceWeekdays || []
      );
      const end = full.recurrenceEndDate ? new Date(full.recurrenceEndDate).getTime() : null;
      if (end === null || nd.getTime() <= end + 24 * 60 * 60 * 1000) {
        await Todo.create({
          title: full.title,
          description: full.description,
          assignedToId: full.assignedToId,
          additionalAssigneeIds: full.additionalAssigneeIds || [],
          watcherIds: full.watcherIds || [],
          createdById: ctx.viewerId,
          status: "open",
          priority: full.priority,
          dueDate: nd,
          orderId: full.orderId,
          labels: full.labels || [],
          project: full.project || "",
          // Checkliste der nächsten Instanz startet wieder unerledigt.
          subtasks: (full.subtasks || []).map((s) => ({ text: s.text, done: false })),
          recurrence: full.recurrence,
          recurrenceInterval: full.recurrenceInterval || 1,
          recurrenceWeekdays: full.recurrenceWeekdays || [],
          recurrenceEndDate: full.recurrenceEndDate || null,
        });
      }
    }
  }

  revalidatePath(PATH);
}

export async function updateTodoAction(todoId: string, _prev: FormState, formData: FormData): Promise<FormState> {
  const ctx = await canManage(todoId, "todos_edit");
  if (!ctx) return { error: "Keine Berechtigung." };

  const title = String(formData.get("title") || "").trim();
  if (!title) return { error: "Bitte einen Titel eingeben." };

  const assignedToId = String(formData.get("assignedToId") || "").trim() || String(ctx.todo.assignedToId);
  if (assignedToId !== String(ctx.todo.assignedToId) && !(await hasPermission("todos_create"))) {
    return { error: "Keine Berechtigung, ToDos anderen zuzuweisen." };
  }

  const priorityRaw = String(formData.get("priority") || "normal");
  const priority: TodoPriority = TODO_PRIORITIES.includes(priorityRaw as TodoPriority) ? (priorityRaw as TodoPriority) : "normal";
  const dueDateRaw = String(formData.get("dueDate") || "");
  const dueDate = dueDateRaw ? new Date(dueDateRaw) : null;
  const recurrenceRaw = String(formData.get("recurrence") || "none");
  const recurrence: TodoRecurrence = TODO_RECURRENCES.includes(recurrenceRaw as TodoRecurrence)
    ? (recurrenceRaw as TodoRecurrence)
    : "none";

  // Das "Weitere Personen"-Feld wird nur mit der todos_create-Berechtigung
  // gerendert (Hidden-Marker). Fehlt es, bleiben Zusatz-Zuständige/Beobachter
  // unangetastet, statt sie durch ein leeres Formular zu löschen.
  const peopleEdited = formData.get("peopleEdited") === "1";
  const additionalAssigneeIds = peopleEdited
    ? parseIdList(formData, "additionalAssigneeIds").filter((id) => id !== assignedToId)
    : (ctx.todo.additionalAssigneeIds || []).map(String);
  const watcherIds = peopleEdited
    ? parseIdList(formData, "watcherIds").filter(
        (id) => id !== assignedToId && !additionalAssigneeIds.includes(id)
      )
    : (ctx.todo.watcherIds || []).map(String);

  const prevPeople = new Set([
    String(ctx.todo.assignedToId),
    ...(ctx.todo.additionalAssigneeIds || []).map(String),
    ...(ctx.todo.watcherIds || []).map(String),
  ]);
  const newlyAdded = [assignedToId, ...additionalAssigneeIds, ...watcherIds].filter(
    (id) => id !== ctx.viewerId && !prevPeople.has(id)
  );

  const newDue = dueDate && !Number.isNaN(dueDate.getTime()) ? dueDate : null;
  const dueChanged =
    (ctx.todo.dueDate ? new Date(ctx.todo.dueDate).getTime() : 0) !== (newDue ? newDue.getTime() : 0);

  await Todo.findByIdAndUpdate(todoId, {
    title,
    description: String(formData.get("description") || "").trim(),
    assignedToId,
    additionalAssigneeIds,
    watcherIds,
    priority,
    dueDate: newDue,
    ...(dueChanged ? { dueReminderSentAt: null } : {}),
    orderId: String(formData.get("orderId") || "").trim() || null,
    labels: parseLabels(formData.get("labels")),
    project: String(formData.get("project") || "").trim().slice(0, 80),
    recurrence,
    recurrenceInterval: Math.min(52, Math.max(1, Math.round(Number(formData.get("recurrenceInterval")) || 1))),
    recurrenceWeekdays: recurrence === "weekly" ? parseWeekdays(formData) : [],
    recurrenceEndDate: (() => {
      const r = String(formData.get("recurrenceEndDate") || "");
      const d = r ? new Date(r) : null;
      return d && !Number.isNaN(d.getTime()) ? d : null;
    })(),
  });

  await logActivity(todoId, "edited", ctx.viewerId, "ToDo bearbeitet");
  if (newlyAdded.length) {
    await notifyTodo(newlyAdded, `ToDo: ${title}`, "Du wurdest einem ToDo zugeordnet.", todoId);
  }

  revalidatePath(PATH);
  return { success: true };
}

// --- Unteraufgaben ---------------------------------------------------------

export async function addTodoSubtaskAction(todoId: string, text: string) {
  const trimmed = text.trim().slice(0, 300);
  if (!trimmed) return;
  const ctx = await canManage(todoId, "todos_edit");
  if (!ctx) return;
  await Todo.findByIdAndUpdate(todoId, { $push: { subtasks: { text: trimmed, done: false } } });
  revalidatePath(PATH);
}

export async function toggleTodoSubtaskAction(todoId: string, subtaskId: string, done: boolean) {
  const ctx = await canManage(todoId, "todos_edit");
  if (!ctx) return;
  await Todo.updateOne(
    { _id: todoId, "subtasks._id": subtaskId },
    { $set: { "subtasks.$.done": done, "subtasks.$.doneAt": done ? new Date() : null } }
  );
  revalidatePath(PATH);
}

export async function deleteTodoSubtaskAction(todoId: string, subtaskId: string) {
  const ctx = await canManage(todoId, "todos_edit");
  if (!ctx) return;
  await Todo.findByIdAndUpdate(todoId, { $pull: { subtasks: { _id: subtaskId } } });
  revalidatePath(PATH);
}

// --- Anhänge ---------------------------------------------------------------

export async function addTodoAttachmentsAction(
  todoId: string,
  _prev: FormState,
  formData: FormData
): Promise<FormState> {
  const ctx = await canManage(todoId, "todos_edit");
  if (!ctx) return { error: "Keine Berechtigung." };
  const files = [...formData.getAll("files")].filter((f): f is File => f instanceof File && f.size > 0);
  if (files.length === 0) return { error: "Keine Datei gewählt." };
  let saved;
  try {
    saved = await saveTodoAttachments(todoId, files);
  } catch (e) {
    return { error: e instanceof Error ? e.message : "Upload fehlgeschlagen." };
  }
  if (saved.length === 0) return { error: "Keine gültige Datei." };
  await Todo.findByIdAndUpdate(todoId, { $push: { attachments: { $each: saved } } });
  await logActivity(todoId, "attachment", ctx.viewerId, `${saved.length} Datei${saved.length === 1 ? "" : "en"} angehängt`);
  revalidatePath(PATH);
  return { success: true };
}

export async function deleteTodoAttachmentAction(todoId: string, storedName: string) {
  const ctx = await canManage(todoId, "todos_edit");
  if (!ctx) return;
  await Todo.findByIdAndUpdate(todoId, { $pull: { attachments: { storedName } } });
  await deleteTodoAttachmentFile(todoId, storedName);
  revalidatePath(PATH);
}

export async function deleteTodoAction(todoId: string) {
  const ctx = await canManage(todoId, "todos_delete");
  if (!ctx) return;
  await Todo.deleteOne({ _id: todoId });
  revalidatePath(PATH);
}

// Notizen darf hinzufügen, wer das ToDo überhaupt sehen darf — eigene ToDos
// immer, fremde mit todos_view. Bewusst niedrigere Hürde als Bearbeiten: eine
// Notiz ist eher ein Kommentar/Statusupdate als eine echte Änderung.
export async function addTodoNoteAction(todoId: string, text: string) {
  const trimmed = text.trim();
  if (!trimmed) return;
  const viewerId = await getSessionClientId();
  if (!viewerId) return;

  await connectDB();
  const todo = await Todo.findById(todoId)
    .select("assignedToId additionalAssigneeIds watcherIds createdById title")
    .lean();
  if (!todo) return;
  const isOwn =
    String(todo.assignedToId) === viewerId ||
    String(todo.createdById) === viewerId ||
    (todo.additionalAssigneeIds || []).map(String).includes(viewerId);
  if (!isOwn && !(await hasPermission("todos_view"))) return;

  const body = trimmed.slice(0, 2000);
  await Todo.findByIdAndUpdate(todoId, { $push: { notes: { authorId: viewerId, text: body } } });
  await logActivity(todoId, "note", viewerId, "Notiz hinzugefügt");

  const mentioned = await resolveMentions(body);
  const audience = todoAudience(todo, viewerId);
  await notifyTodo(mentioned.filter((id) => id !== viewerId), `Erwähnt in: ${todo.title}`, body.slice(0, 140), todoId);
  await notifyTodo(
    audience.filter((id) => !mentioned.includes(id)),
    `Neue Notiz: ${todo.title}`,
    body.slice(0, 140),
    todoId
  );

  revalidatePath(PATH);
}

// --- Vorlagen -------------------------------------------------------------

// Eine Zeile der Vorlagen-Textarea:
//   "Titel | +3 | #Vorbereitung #Marketing | hoch"
// Reihenfolge der Teile nach dem Titel egal. "+N" (auch "+N Tage" / "+Nd")
// = Fälligkeit relativ zum Startdatum; "#foo" = Label(s); hoch/normal/niedrig
// = Priorität.
function parseTemplateLine(line: string) {
  const parts = line.split("|").map((p) => p.trim());
  const title = (parts.shift() || "").slice(0, 200);
  if (!title) return null;
  let priority: TodoPriority = "normal";
  let dueOffsetDays: number | null = null;
  const labels: string[] = [];
  for (const raw of parts) {
    const p = raw.trim();
    if (!p) continue;
    const off = p.match(/^\+?\s*(\d{1,3})\s*(t|tage|tag|d|days?)?$/i);
    if (off) {
      dueOffsetDays = Number(off[1]);
      continue;
    }
    const low = p.toLowerCase();
    if (["hoch", "high"].includes(low)) { priority = "high"; continue; }
    if (["niedrig", "low"].includes(low)) { priority = "low"; continue; }
    if (["normal", "mittel"].includes(low)) { priority = "normal"; continue; }
    if (p.startsWith("#")) {
      for (const tok of p.split(/\s+/)) {
        const l = tok.replace(/^#/, "").trim().slice(0, 40);
        if (l) labels.push(l);
      }
      continue;
    }
  }
  return { title, description: "", priority, dueOffsetDays, labels, subtasks: [] as string[] };
}

function parseTemplateItems(raw: FormDataEntryValue | null) {
  return String(raw || "")
    .split("\n")
    .map((l) => l.trim())
    .filter(Boolean)
    .map(parseTemplateLine)
    .filter((x): x is NonNullable<typeof x> => Boolean(x))
    .slice(0, 40);
}

export async function createTodoTemplateAction(_prev: FormState, formData: FormData): Promise<FormState> {
  const viewerId = await getSessionClientId();
  if (!viewerId) return { error: "Bitte logge dich ein." };
  if (!(await hasPermission("todos_create"))) return { error: "Keine Berechtigung." };

  const name = String(formData.get("name") || "").trim().slice(0, 120);
  if (!name) return { error: "Bitte einen Namen für die Vorlage eingeben." };
  const items = parseTemplateItems(formData.get("items"));
  if (items.length === 0) return { error: "Bitte mindestens eine ToDo-Zeile eingeben." };

  await connectDB();
  await TodoTemplate.create({
    name,
    defaultProject: String(formData.get("defaultProject") || "").trim().slice(0, 80),
    items,
    createdById: viewerId,
  });
  revalidatePath(PATH);
  return { success: true };
}

export async function updateTodoTemplateAction(templateId: string, _prev: FormState, formData: FormData): Promise<FormState> {
  if (!(await hasPermission("todos_create"))) return { error: "Keine Berechtigung." };
  const name = String(formData.get("name") || "").trim().slice(0, 120);
  if (!name) return { error: "Bitte einen Namen eingeben." };
  const items = parseTemplateItems(formData.get("items"));
  if (items.length === 0) return { error: "Bitte mindestens eine ToDo-Zeile eingeben." };

  await connectDB();
  await TodoTemplate.findByIdAndUpdate(templateId, {
    name,
    defaultProject: String(formData.get("defaultProject") || "").trim().slice(0, 80),
    items,
  });
  revalidatePath(PATH);
  return { success: true };
}

export async function deleteTodoTemplateAction(templateId: string) {
  if (!(await hasPermission("todos_create"))) return;
  await connectDB();
  await TodoTemplate.findByIdAndDelete(templateId);
  revalidatePath(PATH);
}

export async function createTodosFromTemplateAction(
  templateId: string,
  _prev: FormState,
  formData: FormData
): Promise<FormState> {
  const viewerId = await getSessionClientId();
  if (!viewerId) return { error: "Bitte logge dich ein." };
  if (!(await hasPermission("todos_create"))) return { error: "Keine Berechtigung." };

  await connectDB();
  const tpl = await TodoTemplate.findById(templateId).lean();
  if (!tpl) return { error: "Vorlage nicht gefunden." };

  const assigneeId = String(formData.get("assigneeId") || "").trim() || viewerId;
  const project = String(formData.get("project") || "").trim().slice(0, 80) || tpl.defaultProject || "";
  const baseRaw = String(formData.get("baseDate") || "").trim();
  const base = baseRaw ? new Date(baseRaw) : new Date();
  base.setHours(0, 0, 0, 0);

  const docs = tpl.items.map((it) => ({
    title: it.title,
    description: it.description || "",
    assignedToId: assigneeId,
    createdById: viewerId,
    status: "open" as const,
    priority: it.priority,
    dueDate:
      it.dueOffsetDays != null
        ? new Date(base.getTime() + it.dueOffsetDays * 24 * 60 * 60 * 1000)
        : null,
    labels: it.labels || [],
    project,
    subtasks: (it.subtasks || []).map((text) => ({ text, done: false })),
    recurrence: "none" as const,
  }));
  if (docs.length) await Todo.insertMany(docs);

  revalidatePath(PATH);
  return { success: true };
}

// --- Sammel-Aktionen ---------------------------------------------------------

export async function bulkUpdateTodosAction(
  ids: string[],
  patch: { status?: string; assigneeId?: string; dueDate?: string | null }
) {
  const viewerId = await getSessionClientId();
  if (!viewerId || !Array.isArray(ids) || ids.length === 0) return;
  await connectDB();

  const canEditAll = await hasPermission("todos_edit");
  const canAssign = await hasPermission("todos_create");
  const todos = await Todo.find({ _id: { $in: ids } }).select("assignedToId createdById").lean();
  const allowed = todos
    .filter((t) => String(t.assignedToId) === viewerId || String(t.createdById) === viewerId || canEditAll)
    .map((t) => t._id);
  if (allowed.length === 0) return;

  const update: Record<string, unknown> = {};
  if (patch.status && TODO_STATUSES.includes(patch.status as TodoStatus)) {
    update.status = patch.status;
    update.completedAt = patch.status === "done" ? new Date() : null;
  }
  if (patch.assigneeId && canAssign) update.assignedToId = patch.assigneeId;
  if (patch.dueDate !== undefined) {
    const d = patch.dueDate ? new Date(patch.dueDate) : null;
    update.dueDate = d && !Number.isNaN(d.getTime()) ? d : null;
  }
  if (Object.keys(update).length === 0) return;

  await Todo.updateMany({ _id: { $in: allowed } }, { $set: update });
  revalidatePath(PATH);
}

export async function bulkDeleteTodosAction(ids: string[]) {
  const viewerId = await getSessionClientId();
  if (!viewerId || !Array.isArray(ids) || ids.length === 0) return;
  await connectDB();
  const canDeleteAll = await hasPermission("todos_delete");
  const todos = await Todo.find({ _id: { $in: ids } }).select("assignedToId createdById").lean();
  const allowed = todos
    .filter((t) => String(t.assignedToId) === viewerId || String(t.createdById) === viewerId || canDeleteAll)
    .map((t) => t._id);
  if (allowed.length) await Todo.deleteMany({ _id: { $in: allowed } });
  revalidatePath(PATH);
}
