"use server";

import { Types } from "mongoose";
import { revalidatePath } from "next/cache";
import { hasPermission } from "@/lib/adminAuth";
import { getSessionClientId } from "@/lib/auth";
import { ensureAutoTodo, removeAutoTodo, daysBefore } from "@/lib/todoAutomation";
import { connectDB } from "@/lib/db";
import { Convention } from "@/models/Convention";
import { WaitlistEntry } from "@/models/WaitlistEntry";
import { WaitlistMessage } from "@/models/WaitlistMessage";
import { Client } from "@/models/Client";
import { Order } from "@/models/Order";
import { sendMail } from "@/lib/mail";
import { emailShell } from "@/lib/emailTemplate";
import { notifyRecipient, notifyStaffWithPermission } from "@/lib/notifications";
import { saveConventionImage, saveConventionLogo, deletePublicUploadImage } from "@/lib/uploads";

const CONVENTIONS_ADMIN_PATH = "/de/admin/conventions";

type FormState = { error?: string };

function revalidateConventionPaths() {
  revalidatePath(CONVENTIONS_ADMIN_PATH);
  revalidatePath("/de/conventions");
  revalidatePath("/en/conventions");
}

function parseDate(value: FormDataEntryValue | null): Date | null {
  const s = String(value || "").trim();
  if (!s) return null;
  const d = new Date(s);
  return Number.isNaN(d.getTime()) ? null : d;
}

export async function createConventionAction(_prev: FormState, formData: FormData): Promise<FormState> {
  if (!(await hasPermission("conventions_create"))) return { error: "Keine Berechtigung." };
  const name = String(formData.get("name") || "").trim();
  const startDate = parseDate(formData.get("startDate"));
  if (!name) return { error: "Bitte einen Namen eingeben." };
  if (!startDate) return { error: "Bitte ein Startdatum eingeben." };
  const endDate = parseDate(formData.get("endDate")) || startDate;

  const imageFile = formData.get("image");
  let image = "";
  if (imageFile instanceof File && imageFile.size > 0) {
    try {
      image = await saveConventionImage(imageFile);
    } catch (err) {
      return { error: err instanceof Error ? err.message : "Bild-Upload fehlgeschlagen." };
    }
  }

  const logoFile = formData.get("logo");
  let logo = "";
  if (logoFile instanceof File && logoFile.size > 0) {
    try {
      logo = await saveConventionLogo(logoFile);
    } catch (err) {
      return { error: err instanceof Error ? err.message : "Logo-Upload fehlgeschlagen." };
    }
  }

  await connectDB();
  await Convention.create({
    name,
    location: String(formData.get("location") || "").trim(),
    startDate,
    endDate,
    infoUrl: String(formData.get("infoUrl") || "").trim(),
    slotsAvailable: formData.get("slotsAvailable") === "on",
    notes: String(formData.get("notes") || "").trim(),
    notesEn: String(formData.get("notesEn") || "").trim(),
    image,
    logo,
    visible: formData.get("visible") === "on",
  });

  revalidateConventionPaths();
  return {};
}

export async function updateConventionAction(conventionId: string, formData: FormData) {
  if (!(await hasPermission("conventions_edit"))) return;
  const name = String(formData.get("name") || "").trim();
  const startDate = parseDate(formData.get("startDate"));
  if (!name || !startDate) return;
  const endDate = parseDate(formData.get("endDate")) || startDate;

  // "visible" bewusst nicht Teil dieses Formulars — Sichtbarkeit läuft über
  // den separaten Schalter in der Zeilen-Übersicht (setConventionVisibleAction),
  // sonst würde jedes Speichern hier den Status unbeabsichtigt zurücksetzen.
  // "slotsAvailable" ist dagegen ein echtes Feld in diesem Formular.
  await connectDB();

  const update: Record<string, unknown> = {
    name,
    location: String(formData.get("location") || "").trim(),
    startDate,
    endDate,
    infoUrl: String(formData.get("infoUrl") || "").trim(),
    slotsAvailable: formData.get("slotsAvailable") === "on",
    notes: String(formData.get("notes") || "").trim(),
    notesEn: String(formData.get("notesEn") || "").trim(),
  };

  // Bild/Logo: eine neu gewählte Datei ersetzt die alte in einem Schritt
  // (Speichern genügt). Das Entfernen läuft über die eigene Schaltfläche
  // (removeConventionMediaAction) — kein Häkchen-dann-Speichern mehr.
  const imageFile = formData.get("image");
  const logoFile = formData.get("logo");
  const hasNewImage = imageFile instanceof File && imageFile.size > 0;
  const hasNewLogo = logoFile instanceof File && logoFile.size > 0;
  if (hasNewImage || hasNewLogo) {
    const current = await Convention.findById(conventionId).select("image logo").lean();
    if (hasNewImage) {
      try {
        update.image = await saveConventionImage(imageFile as File);
      } catch {
        return; // Upload fehlgeschlagen — Rest nicht speichern, Zeile bleibt wie sie war
      }
      if (current?.image) await deletePublicUploadImage(current.image);
    }
    if (hasNewLogo) {
      try {
        update.logo = await saveConventionLogo(logoFile as File);
      } catch {
        return;
      }
      if (current?.logo) await deletePublicUploadImage(current.logo);
    }
  }

  await Convention.findByIdAndUpdate(conventionId, update);

  revalidateConventionPaths();
}

// Eigene Schaltfläche im Admin: Foto ODER Logo einer Convention sofort
// entfernen — ohne Umweg über "Häkchen setzen + Speichern". Fehler landen
// als Rückgabewert (kein Crash), damit die Zeile bedienbar bleibt.
export async function removeConventionMediaAction(
  conventionId: string,
  kind: "image" | "logo"
): Promise<{ error?: string; success?: boolean }> {
  if (!(await hasPermission("conventions_edit"))) return { error: "Keine Berechtigung." };
  if (kind !== "image" && kind !== "logo") return { error: "Ungültiges Feld." };
  if (!Types.ObjectId.isValid(conventionId)) return { error: "Convention nicht gefunden." };

  try {
    await connectDB();
    const current = await Convention.findById(conventionId).select("image logo").lean();
    if (!current) return { error: "Convention nicht gefunden." };
    const currentPath = kind === "image" ? current.image : current.logo;
    await Convention.findByIdAndUpdate(conventionId, { [kind]: "" });
    if (currentPath) await deletePublicUploadImage(currentPath);
    revalidateConventionPaths();
    return { success: true };
  } catch (err) {
    return { error: err instanceof Error ? err.message : "Entfernen fehlgeschlagen." };
  }
}

export async function setConventionVisibleAction(conventionId: string, visible: boolean) {
  if (!(await hasPermission("conventions_edit"))) return;
  await connectDB();
  await Convention.findByIdAndUpdate(conventionId, { visible });
  revalidateConventionPaths();
}

// Selbst-Anmeldung ("ich bin vor Ort") — analog zum Auftrags-Übernehmen,
// braucht nur die View-Berechtigung, da man sich nur selbst einträgt.
// "Team vor Ort": jede zugeteilte Person bekommt automatisch ein persönliches
// Vorbereitungs-ToDo für die Convention-Tage; trägt sie sich wieder aus, wird
// es (sofern noch offen) wieder entfernt.
async function addConventionTodo(conventionId: string, staffId: string, actorId: string) {
  const c = await Convention.findById(conventionId).select("name startDate location").lean();
  if (!c) return;
  await ensureAutoTodo({
    autoKey: `conv:${conventionId}:${staffId}`,
    title: `Convention: ${c.name}`,
    assignedToId: staffId,
    createdById: actorId,
    dueDate: daysBefore(new Date(c.startDate), 3),
    project: "Conventions",
    description: c.location ? `Ort: ${c.location}` : "",
    subtasks: ["Anfahrt / Unterkunft klären", "Equipment packen", "Slots & Warteliste prüfen"],
  });
}

export async function joinConventionAction(conventionId: string) {
  if (!(await hasPermission("conventions_view"))) return;
  const staffId = await getSessionClientId();
  if (!staffId) return;
  await connectDB();
  await Convention.findByIdAndUpdate(conventionId, { $addToSet: { assignedStaffIds: staffId } });
  await addConventionTodo(conventionId, staffId, staffId);
  revalidateConventionPaths();
}

export async function leaveConventionAction(conventionId: string) {
  const staffId = await getSessionClientId();
  if (!staffId) return;
  await connectDB();
  await Convention.findByIdAndUpdate(conventionId, { $pull: { assignedStaffIds: staffId } });
  await removeAutoTodo(`conv:${conventionId}:${staffId}`);
  revalidateConventionPaths();
}

// Fremdzuteilung — z. B. wenn eine Kollegin/ein Kollege sich nicht selbst
// einträgt, aber trotzdem eingeplant werden soll. Braucht die volle
// Bearbeiten-Berechtigung, anders als die Selbst-Anmeldung oben.
export async function assignStaffToConventionAction(conventionId: string, staffId: string) {
  if (!(await hasPermission("conventions_edit"))) return;
  const actorId = (await getSessionClientId()) || staffId;
  await connectDB();
  await Convention.findByIdAndUpdate(conventionId, { $addToSet: { assignedStaffIds: staffId } });
  await addConventionTodo(conventionId, staffId, actorId);
  revalidateConventionPaths();
}

export async function removeStaffFromConventionAction(conventionId: string, staffId: string) {
  if (!(await hasPermission("conventions_edit"))) return;
  await connectDB();
  await Convention.findByIdAndUpdate(conventionId, { $pull: { assignedStaffIds: staffId } });
  await removeAutoTodo(`conv:${conventionId}:${staffId}`);
  revalidateConventionPaths();
}

// Slots wieder öffnen UND die Warteliste in einem Rutsch benachrichtigen —
// das ist der eigentliche Zweck der Warteliste, daher hier zusammengefasst
// statt zwei getrennte Klicks zu verlangen.
export async function reopenSlotsAndNotifyWaitlistAction(conventionId: string) {
  if (!(await hasPermission("conventions_edit"))) return;
  await connectDB();
  const convention = await Convention.findById(conventionId);
  if (!convention) return;
  convention.slotsAvailable = true;
  await convention.save();

  const entries = await WaitlistEntry.find({ conventionId, notified: false });
  const clientIds = entries.map((e) => String(e.clientId));
  const clients = await Client.find({ _id: { $in: clientIds } }).select("email name firstName nickname locale").lean();
  const clientById = new Map(clients.map((c) => [String(c._id), c]));

  await Promise.all(
    entries.map(async (entry) => {
      const client = clientById.get(String(entry.clientId));
      if (!client) return;
      try {
        const name = client.nickname || client.firstName || client.name;
        const isEn = client.locale === "en";
        const html = await emailShell({
          heading: isEn ? `Hi ${name},` : `Hi ${name},`,
          bodyHtml: isEn
            ? `<p>good news — a slot for <strong>${convention.name}</strong> just opened up! Best to request it quickly before it's gone again.</p>`
            : `<p>gute Nachrichten — für <strong>${convention.name}</strong> sind wieder Slots frei! Am besten schnell anfragen, bevor sie wieder weg sind.</p>`,
          buttonLabel: isEn ? "Request a slot" : "Slot anfragen",
          buttonUrl: `${process.env.SITE_URL}/${client.locale || "de"}/conventions`,
          footerNote: "Rakku Photography",
        });
        await sendMail(client.email, isEn ? `Slot open — ${convention.name}` : `Slots frei — ${convention.name}`, html);
        await notifyRecipient(String(entry.clientId), {
          type: "waitlist_slot_open",
          title: isEn ? `Slot open — ${convention.name}` : `Slot frei — ${convention.name}`,
          body: isEn ? "Request it before it's taken again." : "Am besten schnell anfragen, bevor er wieder weg ist.",
          link: `/${client.locale || "de"}/account/conventions`,
        });
        entry.notified = true;
        entry.notifiedAt = new Date();
        await entry.save();
      } catch (err) {
        console.error("Warteliste-Benachrichtigung fehlgeschlagen:", err);
      }
    })
  );

  revalidateConventionPaths();
  revalidatePath(CONVENTIONS_ADMIN_PATH);
}

// Staff genehmigt einen Warteliste-Eintrag und legt daraus direkt einen
// Auftrag mit festem Termin (Datum + exakte Uhrzeit) an. Der Kunde erstellt
// KEINEN eigenen Auftrag mehr — der Termin wird hier fixiert, u. a. um
// Kollisionen mit anderen Slots an denselben Tagen zu vermeiden.
export async function grantWaitlistEntryAction(entryId: string, formData: FormData): Promise<{ error?: string; success?: boolean }> {
  if (!(await hasPermission("conventions_edit"))) return { error: "Keine Berechtigung." };

  const date = String(formData.get("date") || "").trim(); // YYYY-MM-DD
  const startTime = String(formData.get("startTime") || "").trim(); // HH:mm
  const endTime = String(formData.get("endTime") || "").trim();
  const location = String(formData.get("location") || "").trim();
  if (!date || !startTime) return { error: "Bitte Datum und Startzeit angeben." };

  const shootDate = new Date(`${date}T${startTime}`);
  if (Number.isNaN(shootDate.getTime())) return { error: "Ungültiges Datum / ungültige Uhrzeit." };
  const shootEndDate = endTime ? new Date(`${date}T${endTime}`) : null;
  if (shootEndDate && shootEndDate <= shootDate) return { error: "Ende muss nach dem Start liegen." };

  await connectDB();
  const entry = await WaitlistEntry.findById(entryId);
  if (!entry) return { error: "Warteliste-Eintrag nicht gefunden." };
  if (entry.orderId) return { error: "Für diesen Eintrag wurde bereits ein Auftrag erstellt." };

  const convention = await Convention.findById(entry.conventionId).lean();
  if (!convention) return { error: "Convention nicht gefunden." };

  const client = await Client.findById(entry.clientId)
    .select("email name firstName nickname locale")
    .lean();

  // Aus der strukturierten Warteliste-Nachricht ("Charakter/Idee: X …") nur
  // den Charakter als Auftrags-Titel ziehen; der volle Wortlaut (Wunsch-Tag,
  // -Zeit, Freitext) landet in den Auftrags-Notizen.
  const rawMsg = entry.message?.trim() || "";
  const charLine = rawMsg.match(/^(?:Charakter\/Idee|Character\/idea)\s*:\s*(.+)$/im);
  const characterOrConcept = (
    charLine?.[1]?.trim() ||
    rawMsg.split("\n")[0]?.trim() ||
    `${convention.name} — Slot`
  ).slice(0, 300);

  const order = await Order.create({
    clientId: entry.clientId,
    shootingType: "Convention",
    characterOrConcept,
    conventionId: convention._id,
    eventLocation: location || convention.location || "",
    additionalNotes: rawMsg ? `Warteliste-Anfrage:\n${rawMsg}`.slice(0, 2500) : "",
    tag: "shoot_scheduled",
    shootDate,
    shootEndDate,
  });

  entry.orderId = new Types.ObjectId(String(order._id));
  entry.notified = true;
  entry.notifiedAt = new Date();
  await entry.save();

  // Fester Termin → automatisch ein Vorbereitungs-ToDo (wie bei setOrderShootDateAction).
  const actorId = (await getSessionClientId()) || "";
  await ensureAutoTodo({
    autoKey: `shoot-prep:${order._id}`,
    title: `Shooting vorbereiten: ${characterOrConcept}`,
    assignedToId: actorId,
    createdById: actorId,
    dueDate: daysBefore(shootDate, 2),
    orderId: String(order._id),
    project: "Shootings",
    subtasks: ["Referenzbilder sammeln", "Equipment prüfen", "Ablauf mit Kunde klären"],
  });

  if (client) {
    const name = client.nickname || client.firstName || client.name;
    const isEn = client.locale === "en";
    const whenStr = shootDate.toLocaleString(isEn ? "en-GB" : "de-DE", { dateStyle: "long", timeStyle: "short" });
    const place = location || convention.location;
    try {
      const html = await emailShell({
        heading: `Hi ${name},`,
        bodyHtml: isEn
          ? `<p>you've got a slot for <strong>${convention.name}</strong>! Appointment: <strong>${whenStr}</strong>${place ? ` · ${place}` : ""}.</p>`
          : `<p>du hast einen Slot für <strong>${convention.name}</strong> bekommen! Termin: <strong>${whenStr}</strong>${place ? ` · ${place}` : ""}.</p>`,
        buttonLabel: isEn ? "View order" : "Zum Auftrag",
        buttonUrl: `${process.env.SITE_URL}/${client.locale || "de"}/account/orders/${order._id}`,
        footerNote: "Rakku Photography",
      });
      await sendMail(
        client.email,
        isEn ? `Slot confirmed — ${convention.name}` : `Slot bestätigt — ${convention.name}`,
        html
      );
    } catch (err) {
      console.error("Slot-Bestätigungs-Mail fehlgeschlagen:", err);
    }
    await notifyRecipient(String(entry.clientId), {
      type: "waitlist_slot_open",
      title: isEn ? `Slot confirmed — ${convention.name}` : `Slot bestätigt — ${convention.name}`,
      body: isEn ? `Appointment: ${whenStr}` : `Termin: ${whenStr}`,
      link: `/${client.locale || "de"}/account/orders/${order._id}`,
    });
  }

  revalidateConventionPaths();
  revalidatePath(CONVENTIONS_ADMIN_PATH);
  revalidatePath("/[locale]/account/conventions", "page");
  return { success: true };
}

export async function deleteConventionAction(conventionId: string) {
  if (!(await hasPermission("conventions_delete"))) return;
  await connectDB();
  const entries = await WaitlistEntry.find({ conventionId }).select("_id").lean();
  const removed = await Convention.findByIdAndDelete(conventionId);
  if (removed?.image) await deletePublicUploadImage(removed.image);
  if (removed?.logo) await deletePublicUploadImage(removed.logo);
  await WaitlistEntry.deleteMany({ conventionId });
  await WaitlistMessage.deleteMany({ waitlistEntryId: { $in: entries.map((e) => e._id) } });
  revalidateConventionPaths();
}

// Warteliste-Eintrag (/conventions) — braucht jetzt ein eingeloggtes Konto
// (vorher freie Name/E-Mail-Eingabe), damit der Kunde seinen Eintrag im
// eigenen Profil sieht und dort direkt mit uns chatten kann (siehe
// /account/conventions), statt nur eine E-Mail abzuwarten.
type WaitlistFormState = { error?: string; success?: boolean };

export async function joinWaitlistAction(_prev: WaitlistFormState, formData: FormData): Promise<WaitlistFormState> {
  const locale = String(formData.get("locale") || "de");
  const isEn = locale === "en";
  const clientId = await getSessionClientId();
  if (!clientId) return { error: isEn ? "Please log in to join the waitlist." : "Bitte logge dich ein, um dich auf die Warteliste einzutragen." };

  const conventionId = String(formData.get("conventionId") || "");
  // Ungültige ID (leer oder z. B. die Preview-Beispiel-Convention "sample-…")
  // gar nicht erst an findById geben — sonst wirft Mongoose einen CastError
  // und der Kunde landet auf der Fehlerseite statt einer Meldung im Modal.
  if (!conventionId || !Types.ObjectId.isValid(conventionId)) {
    return { error: isEn ? "Convention not found." : "Convention nicht gefunden." };
  }

  // Strukturierte Wunsch-Angaben aus dem Modal zu einem message-String
  // zusammenfassen (das WaitlistEntry hat nur ein Freitext-Feld) — Staff
  // sieht damit Charakter, Wunsch-Tag und -Zeit für die Termin-Festlegung.
  const character = String(formData.get("character") || "").trim();
  const preferredDay = String(formData.get("preferredDay") || "").trim();
  const preferredTime = String(formData.get("preferredTime") || "").trim();
  const note = String(formData.get("message") || "").trim();

  // Charakter, Wunsch-Tag und Wunsch-Uhrzeit sind Pflicht (required greift nur
  // im Browser — hier die serverseitige Absicherung).
  if (!character || !preferredDay || !preferredTime) {
    return {
      error: isEn
        ? "Please fill in character/idea, preferred day and preferred time."
        : "Bitte fülle Charakter/Idee, Wunsch-Tag und Wunsch-Uhrzeit aus.",
    };
  }

  const message = [
    character && `${isEn ? "Character/idea" : "Charakter/Idee"}: ${character}`,
    preferredDay && `${isEn ? "Preferred day" : "Wunsch-Tag"}: ${preferredDay}`,
    preferredTime && `${isEn ? "Preferred time" : "Wunsch-Zeit"}: ${preferredTime}`,
    note,
  ]
    .filter(Boolean)
    .join("\n")
    .slice(0, 500);

  await connectDB();
  const convention = await Convention.findById(conventionId).lean();
  if (!convention) return { error: isEn ? "Convention not found." : "Convention nicht gefunden." };

  const alreadyOnListError = isEn
    ? "You're already on the waitlist for this convention."
    : "Du stehst für diese Convention schon auf der Warteliste.";

  const existing = await WaitlistEntry.findOne({ conventionId, clientId });
  if (existing) return { error: alreadyOnListError };

  try {
    await WaitlistEntry.create({ conventionId, clientId, message });
  } catch (err: unknown) {
    // Race: zwei nahezu gleichzeitige Einträge (z. B. zwei offene Tabs)
    // können beide die obige Prüfung passieren — der zweite create() schlägt
    // dann am unique-Index {conventionId, clientId} fehl (E11000). Statt
    // eines rohen Fehlers dieselbe freundliche Meldung wie beim Normalfall.
    if (err && typeof err === "object" && "code" in err && err.code === 11000) {
      return { error: alreadyOnListError };
    }
    throw err;
  }
  await notifyStaffWithPermission("conventions_view", {
    type: "new_message_for_staff",
    title: `Neu auf Warteliste — ${convention.name}`,
    body: message.slice(0, 200),
    link: "/de/admin/conventions",
  });
  revalidatePath("/[locale]/account/conventions", "page");
  return { success: true };
}

// Chat zu einem einzelnen Warteliste-Eintrag — analog zum Auftrags-Chat
// (OrderChat/OrderMessage), aber pro Warteliste-Platz statt pro Auftrag.
export async function clientSendWaitlistMessageAction(waitlistEntryId: string, text: string) {
  const clientId = await getSessionClientId();
  if (!clientId) return;
  const trimmed = text.trim();
  if (!trimmed) return;

  await connectDB();
  const entry = await WaitlistEntry.findOne({ _id: waitlistEntryId, clientId }).populate("conventionId", "name").lean();
  if (!entry) return;

  await WaitlistMessage.create({ waitlistEntryId, senderId: clientId, senderType: "client", text: trimmed });

  const conventionName = (entry.conventionId as unknown as { name: string } | null)?.name || "Convention";
  await notifyStaffWithPermission("conventions_view", {
    type: "new_message_for_staff",
    title: `Neue Nachricht (Warteliste) — ${conventionName}`,
    body: trimmed.slice(0, 200),
    link: "/de/admin/conventions",
  });

  revalidatePath("/[locale]/account/conventions", "page");
}
