"use server";

import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { Types } from "mongoose";
import { hasPermission } from "@/lib/adminAuth";
import { getSessionClientId } from "@/lib/auth";
import { ensureAutoTodo, daysBefore } from "@/lib/todoAutomation";
import { connectDB } from "@/lib/db";
import { Order, ORDER_TAGS, SHOOTING_TYPES, type OrderTag, type ShootingType } from "@/models/Order";
import { PriceProposal } from "@/models/PriceProposal";
import { getPriceState } from "@/lib/priceProposal";
import { Contract, type TfpContractData } from "@/models/Contract";
import { ClientPhotoFolder } from "@/models/ClientPhotoFolder";
import { PhotoRating } from "@/models/PhotoRating";
import { PhotoMetadata } from "@/models/PhotoMetadata";
import { RejectionReason } from "@/models/RejectionReason";
import { OrderMessage } from "@/models/OrderMessage";
import { WaitlistEntry } from "@/models/WaitlistEntry";
import { WaitlistMessage } from "@/models/WaitlistMessage";
import { Client } from "@/models/Client";
import { Settings } from "@/models/Settings";
import { getSettings } from "@/lib/settings";
import { Invoice } from "@/models/Invoice";
import { createReminderForOrder } from "@/lib/reminderEscalation";
import { getPayableAmount, getTotalOrderDiscount } from "@/lib/discounts";
import { Calendar } from "@/models/Calendar";
import { findScheduleConflict } from "@/lib/scheduleConflicts";
import { pushEventToGoogle, deleteEventFromGoogle } from "@/lib/googleCalendar";
import {
  notifyClientPriceProposed,
  notifyClientPriceResponded,
  notifyClientStatusChanged,
  notifyClientOrderRejected,
  notifyClientFeedbackRequest,
  notifyClientContractReminder,
} from "@/lib/orderEmails";
import { OrderFeedback } from "@/models/OrderFeedback";
import crypto from "crypto";
import {
  ORDER_TAG_LABELS,
  PAYMENT_METHODS,
  PAYMENT_METHOD_LABELS,
  formatEUR,
  formatPaymentReference,
  formatDatePeriod,
  formatDateTimeDE,
  effectiveEventAddress,
  USAGE_RIGHTS_OPTIONS,
  DEFAULT_CHECKLIST_ITEMS,
  type PaymentMethod,
  type UsageRights,
} from "@/lib/orderConstants";
import { logActivity } from "@/lib/orderActivity";
import { logAdminActivity } from "@/lib/teamActivity";
import { checkAndGrantLoyaltyReward } from "@/lib/loyalty";
import { notifyRecipient, notifyStaffWithPermission } from "@/lib/notifications";

type FormState = { error?: string };
type CreateOrderState = { error?: string; orderId?: string };

// Team legt einen Auftrag im Namen eines Kunden an (z. B. bei einer
// spontanen Anfrage vor Ort) — inhaltlich dieselben Pflichtfelder wie
// createOrderAction in actions/orders.ts (Kunden-Selbstanlage), nur mit
// explizit gewählter clientId statt der eigenen Session, keinem Anhang-
// Upload (kann später über den Auftrag nachgereicht werden) und ohne
// Preisstufen-Vorauswahl (die eigentliche Preisverhandlung läuft wie immer
// über PriceProposal). createdByStaffId markiert den Auftrag intern als
// vom Team erstellt — für den Kunden sieht er aus wie jeder andere.
export async function adminCreateOrderForClientAction(_prev: CreateOrderState, formData: FormData): Promise<CreateOrderState> {
  if (!(await hasPermission("orders_create"))) return { error: "Keine Berechtigung." };
  const staffId = await getSessionClientId();
  if (!staffId) return { error: "Nicht eingeloggt." };

  const clientId = String(formData.get("clientId") || "");
  const shootingType = String(formData.get("shootingType") || "");
  const characterOrConcept = String(formData.get("characterOrConcept") || "").trim();
  const eventLocation = String(formData.get("eventLocation") || "").trim();
  const datePeriod = String(formData.get("datePeriod") || "").trim();
  const instagramHandle = String(formData.get("instagramHandle") || "").trim();
  const discordHandle = String(formData.get("discordHandle") || "").trim();
  const ideaMood = String(formData.get("ideaMood") || "").trim();
  const additionalNotes = String(formData.get("additionalNotes") || "").trim();

  if (!SHOOTING_TYPES.includes(shootingType as ShootingType)) return { error: "Bitte eine Art des Shootings wählen." };
  if (!characterOrConcept) return { error: "Bitte eine kurze Beschreibung der Idee angeben." };

  await connectDB();
  // Kein role:"client"-Filter mehr (Bug-Fix, 26.09.2026) — Mitarbeiter und
  // Till selbst waren dadurch als Auftrags-Empfänger komplett ausgeschlossen,
  // obwohl gerade TFP-Shootings mit dem eigenen Team ein echter Bedarf sind.
  const client = await Client.findById(clientId).select("_id").lean();
  if (!client) return { error: "Bitte einen gültigen Kunden auswählen." };

  const order = await Order.create({
    clientId,
    createdByStaffId: staffId,
    shootingType: shootingType as ShootingType,
    characterOrConcept,
    eventLocation,
    datePeriod,
    instagramHandle,
    discordHandle,
    ideaMood,
    additionalNotes,
    checklist: DEFAULT_CHECKLIST_ITEMS.map((text) => ({ text, done: false, doneAt: null })),
  });

  await logActivity(String(order._id), "order_created", "admin", "Auftrag im Namen des Kunden erstellt");
  await notifyStaffWithPermission("orders_view", {
    type: "new_order",
    title: "Neue Auftragsanfrage",
    body: characterOrConcept,
    link: `/de/admin/orders/${order._id}`,
  });

  revalidatePath("/de/admin/orders");
  revalidatePath(`/de/admin/clients/${clientId}`);
  return { orderId: String(order._id) };
}

// Für die vier E-Mail-Benachrichtigungen unten (Preisvorschlag, Preis-
// Antwort, Statusänderung, Ablehnung) — holt Auftrag + Kunde in einem
// Rutsch, damit die Aktionen selbst nicht jedes Mal dieselbe Lookup-Logik
// wiederholen müssen.
async function getNotifyContext(orderId: string) {
  const order = await Order.findById(orderId).select("characterOrConcept clientId").lean();
  if (!order) return null;
  const client = await Client.findById(order.clientId).select("email name firstName nickname locale").lean();
  if (!client) return null;
  return {
    order: { id: orderId, characterOrConcept: order.characterOrConcept },
    client: { email: client.email, name: client.nickname || client.firstName || client.name, locale: client.locale },
  };
}

// Grobe Phase (vor dem ersten "_") aus dem Tag ableiten, z. B.
// "shoot_waiting_for_date" -> "shoot". Dient nur dazu, echte Phasenwechsel
// (Shoot -> Edit -> Done) von kleineren Statuswechseln innerhalb derselben
// Phase zu unterscheiden.
function tagPhase(tag: string): string {
  return tag.split("_")[0];
}

export async function updateOrderTagAction(orderId: string, tag: string) {
  if (!(await hasPermission("orders_edit"))) return;
  if (!ORDER_TAGS.includes(tag as OrderTag)) return;
  await connectDB();
  const previous = await Order.findById(orderId).select("tag").lean();
  // doneAt markiert, wann der Auftrag erstmals in eine Abschluss-Phase kam —
  // das Konto-Dashboard blendet abgeschlossene Aufträge 2 Wochen danach aus.
  const isDone = tag.startsWith("done_");
  const wasDone = (previous?.tag || "").startsWith("done_");
  const patch: Record<string, unknown> = { tag };
  if (isDone && !wasDone) patch.doneAt = new Date();
  else if (!isDone && wasDone) patch.doneAt = null;
  await Order.findByIdAndUpdate(orderId, patch);
  await logActivity(orderId, "tag_changed", "admin", `Status geändert zu „${ORDER_TAG_LABELS[tag as OrderTag]}"`);

  // Nur bei echtem Phasenwechsel (Shoot -> Edit -> Done) eine Mail schicken —
  // kleinere Statuswechsel innerhalb derselben Phase (z. B. "Warten auf
  // Termin" -> "Termin geplant") sind für den Kunden nicht mailenswert und
  // würden nur unnötig Postfächer füllen.
  if (previous && tagPhase(previous.tag) !== tagPhase(tag)) {
    const ctx = await getNotifyContext(orderId);
    if (ctx) await notifyClientStatusChanged(ctx.client, ctx.order, tag as OrderTag);
  }

  // Auch wenn der Auftrag nicht über "Auftrag abschließen"/Archivieren,
  // sondern direkt über dieses generische Dropdown auf einen echten
  // Abschluss-Status gesetzt wird, muss die Feedback-Anfrage trotzdem los —
  // sonst sieht der Kunde "Auftrag abgeschlossen" auf seiner Seite, kann
  // dort aber nirgends bewerten (kein Feedback-Token vorhanden). Dedupliziert
  // wie gehabt über den eindeutigen orderId-Index auf OrderFeedback.
  if (tag === "done_phase1" || tag === "done_posted") {
    await sendFeedbackRequestIfNeeded(orderId);
  }

  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
}

export async function updateOrderTitleAction(orderId: string, title: string) {
  if (!(await hasPermission("orders_edit"))) return;
  const trimmed = title.trim().slice(0, 300);
  if (!trimmed) return;
  await connectDB();
  await Order.findByIdAndUpdate(orderId, { characterOrConcept: trimmed });
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
  revalidatePath("/[locale]/account", "page");
}

export async function bulkArchiveOrdersAction(orderIds: string[]) {
  if (!(await hasPermission("orders_edit"))) return;
  if (!orderIds.length) return;
  await connectDB();
  await Order.updateMany({ _id: { $in: orderIds } }, { $set: { archived: true } });
  revalidatePath("/de/admin/orders");
}

export async function setOrderInternalNotesAction(orderId: string, notes: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  await Order.findByIdAndUpdate(orderId, { internalNotes: notes.slice(0, 4000) });
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
}

export async function setOrderUsageRightsAction(orderId: string, usageRights: string) {
  if (!(await hasPermission("orders_edit"))) return;
  if (!USAGE_RIGHTS_OPTIONS.includes(usageRights as UsageRights)) return;
  await connectDB();
  await Order.findByIdAndUpdate(orderId, { usageRights });
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
}

// "Übernehmen" ist bewusst selbst-zuweisend statt einer Dropdown-Zuweisung an
// andere — wer auf den Knopf klickt, übernimmt den Auftrag für sich selbst
// (auch als Übernahme von einer Kollegin/einem Kollegen, die Oberfläche
// fragt vorher extra nach). "Abgeben" setzt die Zuweisung zurück auf niemand.
export async function claimOrderAction(orderId: string) {
  if (!(await hasPermission("orders_edit"))) return;
  const staffId = await getSessionClientId();
  if (!staffId) return;

  await connectDB();
  const [order, staff] = await Promise.all([
    Order.findByIdAndUpdate(orderId, { assignedStaffId: staffId }, { new: true }),
    Client.findById(staffId).select("name").lean(),
  ]);
  if (order) await logActivity(orderId, "assignee_changed", "admin", `Übernommen von ${staff?.name ?? "Team"}`);

  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath("/de/admin/orders");
}

export async function releaseOrderAction(orderId: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  const order = await Order.findByIdAndUpdate(orderId, { assignedStaffId: null }, { new: true });
  if (order) await logActivity(orderId, "assignee_changed", "admin", "Abgegeben — nicht mehr übernommen");

  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath("/de/admin/orders");
}

type EventAddressInput = { street: string; postCode: string; city: string; countryCode: string; note?: string };

export async function setOrderShootDateAction(
  orderId: string,
  value: string,
  endValue: string = "",
  eventAddress?: EventAddressInput,
  confirmLeadTimeOverride: boolean = false
): Promise<{ conflict?: string; leadTimeWarningWeeks?: number }> {
  if (!(await hasPermission("orders_edit"))) return {};
  await connectDB();

  const order = await Order.findById(orderId);
  if (!order) return {};
  if (eventAddress) {
    order.eventStreet = eventAddress.street.trim();
    order.eventPostCode = eventAddress.postCode.trim();
    order.eventCity = eventAddress.city.trim();
    order.eventCountryCode = (eventAddress.countryCode.trim() || "DE").toUpperCase();
    if (eventAddress.note !== undefined) order.eventAddressNote = eventAddress.note.trim();
  }

  const ordersCalendar = await Calendar.findOne({ kind: "orders" }).lean();
  let conflict: string | null = null;

  if (!value) {
    order.shootDate = null;
    order.shootEndDate = null;
    await order.save();
    if (ordersCalendar && order.googleEventId) {
      await deleteEventFromGoogle(String(ordersCalendar._id), order.googleEventId);
      await Order.findByIdAndUpdate(orderId, { googleEventId: "" });
    }
  } else {
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return {};

    // Mindestvorlauf ist eine Team-Richtlinie, kein Kundenrecht — ein zu
    // naher Termin wird nicht automatisch abgelehnt, sondern erfordert eine
    // bewusste zweite Bestätigung (siehe ShootDatePanel.tsx), damit nie
    // versehentlich unterhalb der Richtlinie gespeichert wird.
    if (!confirmLeadTimeOverride) {
      const { minLeadTimeWeeks } = (await getSettings()).bookingSettings;
      const earliestRecommended = new Date(Date.now() + minLeadTimeWeeks * 7 * 24 * 60 * 60 * 1000);
      if (minLeadTimeWeeks > 0 && date.getTime() < earliestRecommended.getTime()) {
        return { leadTimeWarningWeeks: minLeadTimeWeeks };
      }
    }

    // Ein Ende vor dem Start ergibt keinen Sinn — dann lieber ganz ohne Ende
    // speichern (fällt auf die Standarddauer von einer Stunde zurück), statt
    // einen negativen/verwirrenden Zeitraum in den Kalender zu schreiben.
    const endDate = endValue ? new Date(endValue) : null;
    const validEndDate = endDate && !Number.isNaN(endDate.getTime()) && endDate.getTime() > date.getTime() ? endDate : null;
    order.shootDate = date;
    order.shootEndDate = validEndDate;
    order.preshootReminderSentAt = null;
    order.weatherWarningSentAt = null;
    await order.save();
    conflict = await findScheduleConflict(date, validEndDate, orderId);

    // Termin steht → automatisch ein Vorbereitungs-ToDo (einmalig pro Auftrag).
    const viewerId = (await getSessionClientId()) || "";
    await ensureAutoTodo({
      autoKey: `shoot-prep:${orderId}`,
      title: `Shooting vorbereiten: ${order.characterOrConcept}`,
      assignedToId: order.assignedStaffId ? String(order.assignedStaffId) : viewerId,
      createdById: viewerId,
      dueDate: daysBefore(date, 2),
      orderId,
      project: "Shootings",
      subtasks: ["Referenzbilder sammeln", "Equipment prüfen", "Ablauf mit Kunde klären"],
    });

    if (ordersCalendar) {
      const client = await Client.findById(order.clientId).select("name").lean();
      const googleEventId = await pushEventToGoogle(String(ordersCalendar._id), order.googleEventId, {
        title: `${order.characterOrConcept} · ${client?.name ?? "Kunde"}`,
        location: effectiveEventAddress(order),
        start: date,
        end: validEndDate ?? undefined,
      });
      if (googleEventId) await Order.findByIdAndUpdate(orderId, { googleEventId });
    }
  }

  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath("/de/admin/calendar");
  return conflict ? { conflict } : {};
}

// Admin darf vorschlagen: beim Start (noch kein Vorschlag), nach einer
// Ablehnung, oder als Gegen-Gegenvorschlag, wenn der Kunde zuletzt
// gegenvorschlagen hat. Nicht erlaubt, während der Kunde noch am Zug ist.
export async function adminProposePriceAction(orderId: string, amount: number, note: string) {
  if (!(await hasPermission("pricing"))) return;
  await connectDB();
  const { state } = await getPriceState(orderId);
  if (state.phase === "waiting_on_client" || state.phase === "agreed") return;

  if (state.phase === "waiting_on_admin") {
    await PriceProposal.findByIdAndUpdate(state.latest._id, { status: "countered" });
  }
  await PriceProposal.create({ orderId, proposedBy: "admin", amount, note, status: "pending" });
  await logActivity(orderId, "price_proposed", "admin", `Preisvorschlag: ${formatEUR(amount)}${note ? ` — ${note}` : ""}`);

  const ctx = await getNotifyContext(orderId);
  if (ctx) await notifyClientPriceProposed(ctx.client, ctx.order, amount);

  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
}

// Admin nimmt den Gegenvorschlag des Kunden an oder lehnt ihn ab.
export async function adminRespondPriceAction(orderId: string, decision: "accepted" | "rejected") {
  if (!(await hasPermission("pricing"))) return;
  await connectDB();
  const { state } = await getPriceState(orderId);
  if (state.phase !== "waiting_on_admin") return;

  await PriceProposal.findByIdAndUpdate(state.latest._id, { status: decision });
  await logActivity(
    orderId,
    decision === "accepted" ? "price_accepted" : "price_rejected",
    "admin",
    decision === "accepted" ? "Gegenvorschlag angenommen" : "Gegenvorschlag abgelehnt"
  );

  const ctx = await getNotifyContext(orderId);
  if (ctx) await notifyClientPriceResponded(ctx.client, ctx.order, decision);

  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
}

export async function setCompensationTypeAction(orderId: string, type: "paid" | "tfp") {
  if (!(await hasPermission("pricing"))) return;
  await connectDB();
  // Sobald ein Vertrag existiert, ist die Vergütungsart darüber
  // festgeschrieben — createStandardContractAction/createTfpContractAction
  // setzen jeweils voraus, dass sie zur bestehenden compensationType passt.
  // Serverseitig geprüft (nicht nur in der UI ausgeblendet), da sonst z. B.
  // die Team-App über ihre eigene Route einen bereits unterschriebenen
  // Vertrag zu einem Auftrag gehören lassen könnte, der danach fälschlich
  // wieder als "Kostenpflichtig" geführt wird. Erst den Vertrag löschen
  // (deleteContractAction), dann ist die Art wieder änderbar.
  const hasContract = await Contract.exists({ orderId });
  if (hasContract) return;
  await Order.findByIdAndUpdate(orderId, { compensationType: type });
  await logActivity(orderId, "compensation_type_changed", "admin", `Vergütungsart geändert zu „${type === "tfp" ? "TFP" : "Bezahlt"}"`);
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
}

// Direkter Wechsel der Vergütungsart auch bei bereits bestehendem Vertrag —
// löscht diesen Vertrag automatisch mit statt den Aufruf einfach abzulehnen
// wie setCompensationTypeAction. Die Bestätigung (inkl. Warnung bei bereits
// unterschriebenem Vertrag) läuft in der UI, siehe CompensationTypeToggle.tsx
// — hier nur die eigentliche Änderung, damit ein Fehlgriff bei der
// Vertragsart nicht erst über "Vertrag löschen" + "Art wechseln" als zwei
// getrennte Schritte behoben werden muss.
export async function switchCompensationTypeAction(orderId: string, type: "paid" | "tfp") {
  if (!(await hasPermission("pricing")) || !(await hasPermission("orders_edit"))) return;
  await connectDB();
  const contract = await Contract.findOne({ orderId });
  if (contract) {
    const wasSigned = contract.status === "signed";
    await contract.deleteOne();
    await logActivity(
      orderId,
      "contract_deleted",
      "admin",
      wasSigned ? "Bereits unterschriebenen Vertrag gelöscht (Vergütungsart gewechselt)" : "Vertrag gelöscht (Vergütungsart gewechselt)"
    );
  }
  await Order.findByIdAndUpdate(orderId, { compensationType: type });
  await logActivity(orderId, "compensation_type_changed", "admin", `Vergütungsart geändert zu „${type === "tfp" ? "TFP" : "Bezahlt"}"`);
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
}

// Erlaubt, einen Vertrag zu löschen und neu zu erstellen — auch nachdem der
// Kunde schon unterschrieben hat (z. B. falscher Vertragstyp, Tippfehler in
// vertragsrelevanten Daten). Bewusst ohne Sonderbehandlung nach Status: das
// Löschen selbst ist immer möglich, die UI warnt aber deutlicher, wenn
// bereits unterschrieben wurde (siehe AdminContractPanel.tsx).
export async function deleteContractAction(contractId: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  const contract = await Contract.findById(contractId);
  if (!contract) return;
  const wasSigned = contract.status === "signed";
  const orderId = String(contract.orderId);
  await contract.deleteOne();
  await logActivity(
    orderId,
    "contract_deleted",
    "admin",
    wasSigned ? "Bereits unterschriebenen Vertrag gelöscht (zum Neuerstellen)" : "Vertrag gelöscht (zum Neuerstellen)"
  );
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
}

export async function createStandardContractAction(orderId: string, locale: "de" | "en" = "de") {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();

  const order = await Order.findById(orderId).lean();
  if (!order || order.compensationType !== "paid") return;

  const { state } = await getPriceState(orderId);
  if (state.phase !== "agreed") return;

  const existing = await Contract.findOne({ orderId });
  if (existing) return;

  await Contract.create({
    orderId,
    clientId: order.clientId,
    type: "standard",
    status: "draft",
    locale,
    standardData: {
      amount: state.latest.amount,
      currency: "EUR",
      characterOrConcept: order.characterOrConcept,
      eventLocation: order.eventLocation,
      datePeriod: order.datePeriod,
      shootingType: order.shootingType,
      usageRights: order.usageRights || "",
    },
  });
  await logActivity(orderId, "contract_created", "admin", "Standard-Vertrag erstellt");
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
}

export async function createTfpContractAction(orderId: string, data: TfpContractData, locale: "de" | "en" = "de") {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();

  const order = await Order.findById(orderId).lean();
  if (!order || order.compensationType !== "tfp") return;

  const existing = await Contract.findOne({ orderId });
  if (existing) return;

  await Contract.create({
    orderId,
    clientId: order.clientId,
    type: "tfp",
    status: "draft",
    locale,
    tfpData: data,
  });
  await logActivity(orderId, "contract_created", "admin", "TFP-Vertrag erstellt");
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}/tfp-contract`);
}

// Sendet den Vertrag an den Kunden — die Fotograf-Unterschrift wird dabei
// gesetzt (Admin = Rakku, stimmt mit dem Versenden den eigenen Bedingungen zu).
export async function sendContractAction(contractId: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  const contract = await Contract.findById(contractId);
  if (!contract || contract.status !== "draft") return;

  // Bei einem Einzelunternehmen unterschreibt die reale Person, nicht der
  // Markenname — Nutzer-Wunsch 15.09.2026: "ich muss ja auch als Till
  // Kuhlmann unterschreiben". Bürgerlicher Name aus den Rechnungsdaten,
  // Rückfall auf den Geschäftsnamen, falls noch nicht gepflegt.
  const inv = (await getSettings()).invoiceSettings;
  const photographerName = inv?.legalName || inv?.businessName || "Rakku Photography";

  contract.status = "sent";
  contract.sentAt = new Date();
  contract.signatures.push({
    role: "photographer",
    name: photographerName,
    signedAt: new Date(),
    ip: "admin",
  });
  await contract.save();
  await logActivity(String(contract.orderId), "contract_sent", "admin", "Vertrag an Kunde gesendet");
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
}

// Mindestabstand zwischen zwei Erinnerungen für denselben Vertrag - Wunsch
// des Nutzers ("kein versehentliches Zuspammen"), bewusst als Konstante statt
// Settings-Feld (kein Bedarf für Konfigurierbarkeit geäußert, minimal genug
// für einen späteren Follow-up).
const CONTRACT_REMINDER_COOLDOWN_HOURS = 48;

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

export async function sendContractReminderAction(contractId: string): Promise<ContractReminderResult> {
  if (!(await hasPermission("orders_edit"))) return { error: "Keine Berechtigung." };
  const staffId = await getSessionClientId();
  if (!staffId) return { error: "Bitte einloggen." };

  await connectDB();
  const contract = await Contract.findById(contractId);
  if (!contract) return { error: "Vertrag nicht gefunden." };
  if (contract.status !== "sent") {
    return { error: "Nur für Verträge möglich, die auf die Unterschrift des Kunden warten." };
  }

  const lastSent =
    contract.reminders.length > 0 ? contract.reminders[contract.reminders.length - 1].sentAt : contract.sentAt;
  if (lastSent) {
    const nextPossibleAt = new Date(new Date(lastSent).getTime() + CONTRACT_REMINDER_COOLDOWN_HOURS * 60 * 60 * 1000);
    if (nextPossibleAt.getTime() > Date.now()) {
      return { error: `Nächste Erinnerung erst ab ${formatDateTimeDE(nextPossibleAt)} möglich.` };
    }
  }

  contract.reminders.push({ sentAt: new Date(), sentByClientId: new Types.ObjectId(staffId) });
  await contract.save();

  const ctx = await getNotifyContext(String(contract.orderId));
  if (ctx) {
    await notifyClientContractReminder(ctx.client, ctx.order);
    await notifyRecipient(String(contract.clientId), {
      type: "contract_reminder",
      title: `Vertrag wartet auf deine Unterschrift — ${ctx.order.characterOrConcept}`,
      link: `/${ctx.client.locale}/account/orders/${contract.orderId}`,
    });
  }

  await logActivity(String(contract.orderId), "contract_reminder_sent", "admin", "Erinnerung für nicht unterschriebenen Vertrag gesendet");
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath("/de/admin/contracts");
  return { success: true };
}

// Foto-Ordner sind grundsätzlich pro Kunde (nicht pro Auftrag) — erreichbar
// sowohl von der Auftragsseite als auch direkt von der Kundendetailseite
// aus (z. B. für Bestandskunden, mit denen schon vor einem Auftrag im
// System fotografiert wurde). Beide Einstiegspunkte teilen sich dieselben
// Actions, daher zählt hier sowohl orders- als auch clients-Berechtigung.
async function canManagePhotoFolders() {
  return (await hasPermission("orders_edit")) || (await hasPermission("clients_edit"));
}

function revalidatePhotoFolderPaths(clientId: string) {
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/clients/${clientId}`);
}

export async function assignPhotoFolderAction(
  clientId: string,
  nextcloudPath: string,
  label: string,
  assignedAt?: string,
  orderId?: string
) {
  if (!(await canManagePhotoFolders())) return;
  await connectDB();
  const date = assignedAt ? new Date(assignedAt) : new Date();
  await ClientPhotoFolder.create({
    clientId,
    orderId: orderId || null,
    nextcloudPath,
    label,
    assignedAt: Number.isNaN(date.getTime()) ? new Date() : date,
    sortOrder: Date.now(),
    // Jeder neu zugewiesene Ordner startet unveröffentlicht — unabhängig
    // davon, ob er über einen Auftrag oder direkt über die Kundenseite
    // zugewiesen wurde — und muss bewusst über den Freigeben-Button sichtbar
    // gemacht werden, damit vorher noch Zeit zum Sichten/Sortieren bleibt.
    released: false,
  });
  revalidatePhotoFolderPaths(clientId);
}

export async function setPhotoFolderReleasedAction(folderId: string, released: boolean) {
  if (!(await canManagePhotoFolders())) return;
  await connectDB();
  const folder = await ClientPhotoFolder.findByIdAndUpdate(folderId, { released }).lean();
  if (!folder) return;
  revalidatePhotoFolderPaths(String(folder.clientId));
  revalidatePath("/[locale]/account/photos", "page");
  revalidatePath(`/[locale]/account/photos/${folderId}`, "page");
}

export async function updatePhotoFolderAction(folderId: string, formData: FormData) {
  if (!(await canManagePhotoFolders())) return;
  await connectDB();
  const folder = await ClientPhotoFolder.findById(folderId);
  if (!folder) return;

  const label = String(formData.get("label") || "").trim();
  const assignedAtRaw = String(formData.get("assignedAt") || "");
  const date = assignedAtRaw ? new Date(assignedAtRaw) : null;

  folder.label = label;
  if (date && !Number.isNaN(date.getTime())) folder.assignedAt = date;

  // Nachträgliche Zuordnung/Entkopplung von einem Auftrag — z. B. wenn ein
  // Ordner erst allgemein über die Kundenseite angelegt und später einem
  // bestimmten Auftrag zugeordnet werden soll (die Admin-Auftragsseite zeigt
  // nur noch Ordner mit passender orderId, siehe dortige Filterung). Leerer
  // Wert = keinem Auftrag zugeordnet. Zielauftrag muss zum selben Kunden
  // gehören, sonst wird die Änderung ignoriert.
  const orderIdField = formData.get("orderId");
  if (orderIdField !== null) {
    const orderIdStr = String(orderIdField).trim();
    if (!orderIdStr) {
      folder.orderId = null;
    } else {
      const targetOrder = await Order.findOne({ _id: orderIdStr, clientId: folder.clientId }).select("_id").lean();
      if (targetOrder) folder.orderId = new Types.ObjectId(orderIdStr);
    }
  }

  await folder.save();
  revalidatePhotoFolderPaths(String(folder.clientId));
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
}

export async function movePhotoFolderAction(folderId: string, direction: "up" | "down") {
  if (!(await canManagePhotoFolders())) return;
  await connectDB();
  const folder = await ClientPhotoFolder.findById(folderId).lean();
  if (!folder) return;

  // Nachbar-Suche bewusst auf denselben Kunden begrenzt — anders als bei
  // FAQ/Testimonials gehören Foto-Ordner unterschiedlichen Kunden, eine
  // globale Sortierung über alle Kunden hinweg ergäbe keinen Sinn.
  const neighbor = await ClientPhotoFolder.findOne({
    clientId: folder.clientId,
    sortOrder: direction === "up" ? { $lt: folder.sortOrder } : { $gt: folder.sortOrder },
  })
    .sort({ sortOrder: direction === "up" ? -1 : 1 })
    .lean();
  if (!neighbor) return;

  await Promise.all([
    ClientPhotoFolder.findByIdAndUpdate(folder._id, { sortOrder: neighbor.sortOrder }),
    ClientPhotoFolder.findByIdAndUpdate(neighbor._id, { sortOrder: folder.sortOrder }),
  ]);
  revalidatePhotoFolderPaths(String(folder.clientId));
}

export async function removePhotoFolderAction(folderId: string) {
  if (!(await canManagePhotoFolders())) return;
  await connectDB();
  const folder = await ClientPhotoFolder.findById(folderId).lean();
  if (!folder) return;
  await ClientPhotoFolder.findByIdAndDelete(folderId);
  await PhotoRating.deleteMany({ folderId });
  revalidatePhotoFolderPaths(String(folder.clientId));
}

export async function setPhotoApprovalAction(folderId: string, clientId: string, fileName: string, approved: boolean) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  await PhotoRating.findOneAndUpdate(
    { folderId, fileName },
    { $set: { approvedForDownload: approved }, $setOnInsert: { clientId, folderId, fileName } },
    { upsert: true }
  );
  revalidatePath(`/de/admin/folders/${folderId}`);
  revalidatePath(`/[locale]/account/photos/${folderId}`, "page");
}

export async function approveAllPhotosAction(folderId: string, clientId: string, fileNames: string[]) {
  if (!(await hasPermission("orders_edit"))) return;
  if (fileNames.length === 0) return;
  await connectDB();
  const folderObjectId = new Types.ObjectId(folderId);
  const clientObjectId = new Types.ObjectId(clientId);
  await PhotoRating.bulkWrite(
    fileNames.map((fileName) => ({
      updateOne: {
        filter: { folderId: folderObjectId, fileName },
        update: {
          $set: { approvedForDownload: true },
          $setOnInsert: { clientId: clientObjectId, folderId: folderObjectId, fileName },
        },
        upsert: true,
      },
    }))
  );
  revalidatePath(`/de/admin/folders/${folderId}`);
  revalidatePath(`/[locale]/account/photos/${folderId}`, "page");
}

// Tags werden von Staff in der Admin-Freigabe-Ansicht vergeben (Foto-Tags-
// Feature, 26.09.2026) — bewusst dieselbe Berechtigung wie die Foto-
// Freigabe (orders_edit), kein eigenes Recht nötig für eine so kleine,
// verwandte Aktion.
export async function setPhotoTagsAction(folderId: string, clientId: string, fileName: string, tags: string[]) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  const cleaned = [...new Set(tags.map((t) => t.trim()).filter(Boolean))].slice(0, 10);
  await PhotoMetadata.findOneAndUpdate(
    { folderId, fileName },
    { $set: { tags: cleaned }, $setOnInsert: { clientId, folderId, fileName } },
    { upsert: true }
  );
  revalidatePath(`/de/admin/folders/${folderId}`);
  revalidatePath(`/[locale]/account/photos/${folderId}`, "page");
}

// Angebote/Preisstufen-Aktionen sind nach lib/actions/services.ts ausgelagert.

type RejectionReasonFormState = { error?: string };

export async function createRejectionReasonAction(
  _prev: RejectionReasonFormState,
  formData: FormData
): Promise<RejectionReasonFormState> {
  if (!(await hasPermission("rejection_reasons_create"))) return { error: "Nicht eingeloggt." };
  const label = String(formData.get("label") || "").trim();
  if (!label) return { error: "Bitte einen Grund eingeben." };

  await connectDB();
  await RejectionReason.create({ label, sortOrder: Date.now() });
  revalidatePath("/de/admin/rejection-reasons");
  return {};
}

export async function toggleRejectionReasonActiveAction(id: string, active: boolean) {
  if (!(await hasPermission("rejection_reasons_edit"))) return;
  await connectDB();
  await RejectionReason.findByIdAndUpdate(id, { active });
  revalidatePath("/de/admin/rejection-reasons");
}

export async function deleteRejectionReasonAction(id: string) {
  if (!(await hasPermission("rejection_reasons_delete"))) return;
  await connectDB();
  await RejectionReason.findByIdAndDelete(id);
  revalidatePath("/de/admin/rejection-reasons");
}

export async function rejectOrderAction(orderId: string, reasonId: string, note: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  const reason = await RejectionReason.findById(reasonId).lean();
  if (!reason) return;

  const trimmedNote = note.trim();
  await Order.findByIdAndUpdate(orderId, {
    rejected: true,
    rejectionReason: reason.label,
    rejectionNote: trimmedNote,
    rejectedAt: new Date(),
    rejectedBy: "admin",
    tag: "done_dropped",
    doneAt: new Date(),
  });
  await logActivity(
    orderId,
    "order_rejected",
    "admin",
    `Auftrag abgelehnt: ${reason.label}${trimmedNote ? ` — ${trimmedNote}` : ""}`
  );

  const ctx = await getNotifyContext(orderId);
  if (ctx) await notifyClientOrderRejected(ctx.client, ctx.order, reason.label, trimmedNote);

  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath("/de/admin/statistics");
}

export async function unrejectOrderAction(orderId: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  await Order.findByIdAndUpdate(orderId, {
    rejected: false,
    rejectionReason: "",
    rejectionNote: "",
    rejectedAt: null,
    rejectedBy: null,
  });
  await logActivity(orderId, "order_unrejected", "admin", "Ablehnung aufgehoben");
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath("/de/admin/statistics");
}

export async function adminSendOrderMessageAction(orderId: string, text: string) {
  if (!(await hasPermission("orders_edit"))) return;
  const senderId = await getSessionClientId();
  if (!senderId) return;
  const trimmed = text.trim();
  if (!trimmed) return;

  await connectDB();
  await OrderMessage.create({ orderId, senderId, senderType: "admin", text: trimmed });

  const order = await Order.findById(orderId).select("clientId characterOrConcept").lean();
  if (order) {
    const orderClient = await Client.findById(order.clientId).select("locale").lean();
    await notifyRecipient(String(order.clientId), {
      type: "new_message_for_client",
      title: `Neue Nachricht — ${order.characterOrConcept}`,
      body: trimmed.slice(0, 200),
      link: `/${orderClient?.locale || "de"}/account/orders/${orderId}`,
    });
  }

  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
}

// Admin-Antwort im Chat zu einem einzelnen Warteliste-Eintrag — Gegenstück
// zu clientSendWaitlistMessageAction (conventions.ts).
export async function adminSendWaitlistMessageAction(waitlistEntryId: string, text: string) {
  if (!(await hasPermission("conventions_edit"))) return;
  const senderId = await getSessionClientId();
  if (!senderId) return;
  const trimmed = text.trim();
  if (!trimmed) return;

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

  const entry = await WaitlistEntry.findById(waitlistEntryId).populate("conventionId", "name").lean();
  if (entry) {
    const entryClient = await Client.findById(entry.clientId).select("locale").lean();
    await notifyRecipient(String(entry.clientId), {
      type: "new_message_for_client",
      title: `Neue Nachricht (Warteliste) — ${(entry.conventionId as unknown as { name: string } | null)?.name || "Convention"}`,
      body: trimmed.slice(0, 200),
      link: `/${entryClient?.locale || "de"}/account/conventions`,
    });
  }

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

// Zufriedenheits-Nachfrage genau einmal pro Auftrag verschicken — der
// eindeutige Index auf orderId verhindert einen Doppelversand, egal ob sie
// über "Auftrag abschließen", das Archivieren oder beides ausgelöst wird.
async function sendFeedbackRequestIfNeeded(orderId: string) {
  const existingFeedback = await OrderFeedback.findOne({ orderId }).select("_id").lean();
  if (existingFeedback) return;
  const ctx = await getNotifyContext(orderId);
  const order = await Order.findById(orderId).select("shootingType clientId").lean();
  if (!ctx || !order) return;
  const token = crypto.randomUUID().replace(/-/g, "");
  await OrderFeedback.create({
    orderId,
    clientId: order.clientId,
    clientName: ctx.client.name,
    shootingType: order.shootingType,
    token,
  });
  await notifyClientFeedbackRequest(ctx.client, ctx.order, token);
}

// Macht den Auftragsabschluss für den Kunden sichtbar (Status-Pill auf der
// Kundenseite wechselt auf "abgeschlossen") und stößt die Feedback-Anfrage
// an — von "Auftrag abschließen" UND vom Archivieren aus genutzt, damit
// diese beiden bisher unabhängigen Aktionen nicht mehr auseinanderlaufen
// (vorher konnte ein archivierter Auftrag beim Kunden noch einen alten
// Zwischen-Status wie "Warten auf Feedback" zeigen, obwohl schon die
// Zufriedenheits-Mail raus war).
async function markCompletedIfNeeded(orderId: string) {
  const order = await Order.findById(orderId).select("tag").lean();
  // done_* (auch done_dropped) heißt: schon abschließend geregelt — nicht
  // überschreiben, sonst würde ein abgesagter Auftrag fälschlich als
  // erfolgreich abgeschlossen markiert.
  if (order && !order.tag.startsWith("done_")) {
    await Order.findByIdAndUpdate(orderId, { tag: "done_phase1" });
    await logActivity(orderId, "tag_changed", "admin", `Status geändert zu „${ORDER_TAG_LABELS.done_phase1}"`);
  }
  await sendFeedbackRequestIfNeeded(orderId);
}

export async function markOrderCompletedAction(orderId: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  await markCompletedIfNeeded(orderId);

  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
}

export async function archiveOrderAction(orderId: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  await Order.findByIdAndUpdate(orderId, { archived: true });
  await markCompletedIfNeeded(orderId);

  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
}

export async function unarchiveOrderAction(orderId: string) {
  if (!(await hasPermission("orders_edit"))) return;
  await connectDB();
  await Order.findByIdAndUpdate(orderId, { archived: false });
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
}

export async function markOrderPaidAction(orderId: string, method: string) {
  if (!(await hasPermission("payments"))) return;
  const paymentMethod = (PAYMENT_METHODS.includes(method as PaymentMethod) ? method : "other") as PaymentMethod;
  await connectDB();
  // Atomarer Claim (nur wenn noch unbezahlt UND kein TFP-Auftrag) statt
  // bedingungslosem Update — verhindert sowohl doppelte Stammkunden-Prämien
  // bei einem versehentlichen Doppelklick als auch, dass ein TFP-Auftrag
  // (keine Bezahlung vorgesehen) über diesen Weg als bezahlt markiert wird,
  // selbst wenn die Aktion direkt statt über die UI aufgerufen würde.
  const claimed = await Order.findOneAndUpdate(
    { _id: orderId, paid: false, compensationType: "paid" },
    { $set: { paid: true, paidAt: new Date(), paymentMethod } }
  );
  if (!claimed) return;
  await logActivity(orderId, "order_paid", "admin", `Als bezahlt markiert (${PAYMENT_METHOD_LABELS[paymentMethod]})`);
  await checkAndGrantLoyaltyReward(String(claimed.clientId));
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
  revalidatePath("/de/admin/statistics");
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
}

export async function markOrderUnpaidAction(orderId: string) {
  if (!(await hasPermission("payments"))) return;
  await connectDB();
  await Order.findByIdAndUpdate(orderId, { paid: false, paidAt: null, paymentMethod: "" });
  await logActivity(orderId, "order_unpaid", "admin", "Zahlungsstatus zurückgesetzt auf offen");
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
  revalidatePath("/de/admin/statistics");
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
}

export async function setOrderDepositAction(orderId: string, amount: number) {
  if (!(await hasPermission("payments"))) return;
  if (!Number.isFinite(amount) || amount < 0) return;
  await connectDB();
  const order = await Order.findById(orderId);
  if (!order || order.paid) return;
  order.depositAmount = amount;
  if (amount === 0) {
    order.depositPaid = false;
    order.depositPaidAt = null;
  }
  await order.save();
  await logActivity(
    orderId,
    "order_deposit_set",
    "admin",
    amount > 0 ? `Anzahlung auf ${amount.toFixed(2)} € gesetzt` : "Anzahlung entfernt"
  );
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
}

export async function recordRefundAction(orderId: string, amount: number, note: string) {
  if (!(await hasPermission("payments"))) return;
  if (!Number.isFinite(amount) || amount <= 0) return;
  await connectDB();
  const order = await Order.findById(orderId);
  if (!order) return;
  order.refundedAmount = (order.refundedAmount || 0) + amount;
  order.refundedAt = new Date();
  order.refundNote = note.trim().slice(0, 300);
  await order.save();
  await logActivity(
    orderId,
    "order_refunded",
    "admin",
    `Rückerstattung von ${amount.toFixed(2)} € erfasst${note ? ` — ${note.trim()}` : ""}`
  );
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
  revalidatePath("/de/admin/statistics");
  revalidatePath("/de/admin/tax");
}

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

export async function updateInvoiceSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };
  await connectDB();

  const vatRate = Number(formData.get("vatRate") || 19);
  if (!Number.isFinite(vatRate) || vatRate < 0 || vatRate > 100) {
    return { error: "Bitte einen Umsatzsteuersatz zwischen 0 und 100 % angeben." };
  }
  const nextInvoiceNumber = Number(formData.get("nextInvoiceNumber") || 1);
  if (!Number.isFinite(nextInvoiceNumber) || nextInvoiceNumber < 1) {
    return { error: "Bitte eine gültige nächste Rechnungsnummer (mindestens 1) angeben." };
  }

  const invoiceNumberPrefix = String(formData.get("invoiceNumberPrefix") || "RG-").trim() || "RG-";
  const prospectiveNumber = `${invoiceNumberPrefix}${String(nextInvoiceNumber).padStart(4, "0")}`;
  if (await Invoice.exists({ number: prospectiveNumber })) {
    return { error: `Rechnungsnummer ${prospectiveNumber} existiert bereits — bitte eine höhere nächste Nummer wählen.` };
  }

  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        invoiceSettings: {
          businessName: String(formData.get("businessName") || "").trim(),
          legalName: String(formData.get("legalName") || "").trim(),
          street: String(formData.get("street") || "").trim(),
          postCode: String(formData.get("postCode") || "").trim(),
          city: String(formData.get("city") || "").trim(),
          countryCode: String(formData.get("countryCode") || "DE").trim().toUpperCase() || "DE",
          email: String(formData.get("email") || "").trim(),
          taxId: String(formData.get("taxId") || "").trim(),
          kleinunternehmer: formData.get("kleinunternehmer") === "on",
          vatRate,
          iban: String(formData.get("iban") || "").trim(),
          bic: String(formData.get("bic") || "").trim(),
          bankName: String(formData.get("bankName") || "").trim(),
          accountHolderName: String(formData.get("accountHolderName") || "").trim(),
          invoiceNumberPrefix,
          nextInvoiceNumber,
        },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateBannerSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };
  await connectDB();

  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        bannerSettings: {
          enabled: formData.get("enabled") === "on",
          text: String(formData.get("text") || "").trim(),
          textEn: String(formData.get("textEn") || "").trim(),
        },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  revalidatePath("/[locale]", "layout");
  return { success: true };
}

export async function updateMaintenanceSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };
  await connectDB();

  const enabled = formData.get("enabled") === "on";
  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        maintenanceMode: {
          enabled,
          message: String(formData.get("message") || "").trim(),
          previewPassword: String(formData.get("previewPassword") || "").trim(),
        },
      },
    },
    { upsert: true }
  );
  await logAdminActivity("Wartungsmodus geändert", enabled ? "Aktiviert" : "Deaktiviert");
  revalidatePath("/de/admin/settings");
  revalidatePath("/[locale]", "layout");
  return { success: true };
}

export async function updateSocialLinksAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };
  await connectDB();

  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        socialLinks: {
          instagram: String(formData.get("instagram") || "").trim(),
          twitter: String(formData.get("twitter") || "").trim(),
          tiktok: String(formData.get("tiktok") || "").trim(),
          twitch: String(formData.get("twitch") || "").trim(),
          youtube: String(formData.get("youtube") || "").trim(),
          discord: String(formData.get("discord") || "").trim(),
          email: String(formData.get("email") || "").trim(),
        },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  revalidatePath("/[locale]", "layout");
  return { success: true };
}

export async function updateReferralSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const refereeDiscountType = formData.get("refereeDiscountType") === "fixed" ? "fixed" : "percent";
  const refereeDiscountValue = Number(formData.get("refereeDiscountValue"));
  const referrerRewardType = formData.get("referrerRewardType") === "fixed" ? "fixed" : "percent";
  const referrerRewardValue = Number(formData.get("referrerRewardValue"));
  if (!Number.isFinite(refereeDiscountValue) || refereeDiscountValue < 0) return { error: "Ungültiger Neukunden-Rabatt." };
  if (!Number.isFinite(referrerRewardValue) || referrerRewardValue < 0) return { error: "Ungültige Empfehlungsprämie." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        referralSettings: {
          enabled: formData.get("enabled") === "on",
          refereeDiscountType,
          refereeDiscountValue,
          referrerRewardType,
          referrerRewardValue,
        },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateReengagementSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const delayDays = Number(formData.get("delayDays"));
  if (!Number.isFinite(delayDays) || delayDays < 30) return { error: "Bitte mindestens 30 Tage angeben." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        reengagementSettings: {
          enabled: formData.get("enabled") === "on",
          delayDays,
          customText: String(formData.get("customText") || "").trim(),
          customTextEn: String(formData.get("customTextEn") || "").trim(),
        },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updatePreshootReminderSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const hoursBefore = Number(formData.get("hoursBefore"));
  if (!Number.isFinite(hoursBefore) || hoursBefore < 2) return { error: "Bitte mindestens 2 Stunden angeben." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        preshootReminderSettings: {
          enabled: formData.get("enabled") === "on",
          hoursBefore,
          customText: String(formData.get("customText") || "").trim(),
          customTextEn: String(formData.get("customTextEn") || "").trim(),
        },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateWeatherCheckSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const hoursBefore = Number(formData.get("hoursBefore"));
  if (!Number.isFinite(hoursBefore) || hoursBefore < 2) return { error: "Bitte mindestens 2 Stunden angeben." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        weatherCheckSettings: {
          enabled: formData.get("enabled") === "on",
          hoursBefore,
        },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateReviewRewardSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const type = formData.get("type") === "percent" ? "percent" : "fixed";
  const value = Number(formData.get("value"));
  if (!Number.isFinite(value) || value < 0) return { error: "Bitte einen gültigen Wert angeben." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    { $set: { reviewRewardSettings: { enabled: formData.get("enabled") === "on", type, value } } },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateLoyaltySettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const ordersThreshold = Number(formData.get("ordersThreshold"));
  if (!Number.isFinite(ordersThreshold) || ordersThreshold < 1) return { error: "Bitte mindestens 1 angeben." };
  const rewardType = formData.get("rewardType") === "fixed" ? "fixed" : "percent";
  const rewardValue = Number(formData.get("rewardValue"));
  if (!Number.isFinite(rewardValue) || rewardValue < 0) return { error: "Bitte einen gültigen Wert angeben." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        loyaltySettings: { enabled: formData.get("enabled") === "on", ordersThreshold, rewardType, rewardValue },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateBookingSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const minLeadTimeWeeks = Number(formData.get("minLeadTimeWeeks"));
  if (!Number.isFinite(minLeadTimeWeeks) || minLeadTimeWeeks < 0) return { error: "Bitte einen gültigen Wert angeben." };

  await connectDB();
  await Settings.findOneAndUpdate({ key: "main" }, { $set: { bookingSettings: { minLeadTimeWeeks } } }, { upsert: true });
  await logAdminActivity("Mindestvorlaufzeit geändert", `${minLeadTimeWeeks} Wochen`);
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateGoogleSyncSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const reflectDeletionsFromGoogle = formData.get("reflectDeletionsFromGoogle") === "on";
  await connectDB();
  await Settings.findOneAndUpdate({ key: "main" }, { $set: { googleSyncSettings: { reflectDeletionsFromGoogle } } }, { upsert: true });
  await logAdminActivity(
    "Google-Kalender-Löschsync geändert",
    reflectDeletionsFromGoogle ? "Aktiviert" : "Deaktiviert"
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateFeedbackRequestSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const delayDays = Number(formData.get("delayDays"));
  if (!Number.isFinite(delayDays) || delayDays < 1) return { error: "Bitte mindestens 1 Tag angeben." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    { $set: { feedbackRequestSettings: { enabled: formData.get("enabled") === "on", delayDays } } },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

// Google-Bewertungslink für den CTA nach guter interner Bewertung (siehe
// FeedbackForm.tsx) — bewusst kein enabled-Schalter, der CTA blendet sich
// von selbst aus, solange url leer ist (26.09.2026).
export async function updateGoogleReviewSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const url = String(formData.get("url") || "").trim();
  if (url && !/^https:\/\//.test(url)) return { error: "Bitte eine vollständige https://-URL angeben." };

  await connectDB();
  await Settings.findOneAndUpdate({ key: "main" }, { $set: { googleReviewSettings: { url } } }, { upsert: true });
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateTaxReserveSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const percentage = Number(formData.get("percentage"));
  if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) return { error: "Bitte einen Wert zwischen 0 und 100 angeben." };

  await connectDB();
  await Settings.findOneAndUpdate({ key: "main" }, { $set: { taxReserveSettings: { percentage } } }, { upsert: true });
  revalidatePath("/de/admin/settings");
  revalidatePath("/de/admin/statistics");
  return { success: true };
}

export async function updateMileageSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const ratePerKm = Number(formData.get("ratePerKm"));
  if (!Number.isFinite(ratePerKm) || ratePerKm < 0 || ratePerKm > 5) return { error: "Bitte einen Wert zwischen 0 und 5 € angeben." };

  await connectDB();
  await Settings.findOneAndUpdate({ key: "main" }, { $set: { mileageSettings: { ratePerKm } } }, { upsert: true });
  revalidatePath("/de/admin/settings");
  revalidatePath("/de/admin/mileage");
  return { success: true };
}

export async function updateReminderEscalationSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  const daysUntilFirstReminder = Number(formData.get("daysUntilFirstReminder"));
  const daysBetweenLevels = Number(formData.get("daysBetweenLevels"));
  if (!Number.isFinite(daysUntilFirstReminder) || daysUntilFirstReminder < 1) return { error: "Bitte eine gültige Anzahl Tage angeben." };
  if (!Number.isFinite(daysBetweenLevels) || daysBetweenLevels < 1) return { error: "Bitte eine gültige Anzahl Tage angeben." };

  const levelFees = [
    Number(formData.get("fee1")) || 0,
    Number(formData.get("fee2")) || 0,
    Number(formData.get("fee3")) || 0,
  ];
  if (levelFees.some((f) => f < 0)) return { error: "Gebühren dürfen nicht negativ sein." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        reminderEscalationSettings: {
          autoEnabled: formData.get("autoEnabled") === "on",
          daysUntilFirstReminder,
          daysBetweenLevels,
          levelFees,
        },
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  return { success: true };
}

export async function updateLegalTextSettingsAction(
  _prev: SettingsFormState,
  formData: FormData
): Promise<SettingsFormState> {
  if (!(await hasPermission("settings"))) return { error: "Nicht eingeloggt." };

  await connectDB();
  await Settings.findOneAndUpdate(
    { key: "main" },
    {
      $set: {
        "legal.termsUpdated": String(formData.get("termsUpdated") || "").trim(),
        "legal.termsTextDe": String(formData.get("termsTextDe") || "").trim(),
        "legal.termsTextEn": String(formData.get("termsTextEn") || "").trim(),
        "legal.privacyUpdated": String(formData.get("privacyUpdated") || "").trim(),
        "legal.privacyTextDe": String(formData.get("privacyTextDe") || "").trim(),
        "legal.privacyTextEn": String(formData.get("privacyTextEn") || "").trim(),
        "legal.impressumUrl": String(formData.get("impressumUrl") || "").trim(),
      },
    },
    { upsert: true }
  );
  revalidatePath("/de/admin/settings");
  revalidatePath("/de/agb");
  revalidatePath("/en/agb");
  revalidatePath("/de/datenschutz");
  revalidatePath("/en/datenschutz");
  return { success: true };
}

// Rechnungen sind unveränderlich, sobald erstellt (Schnappschuss der Daten
// zum Ausstellungszeitpunkt) — daher nur erstellbar, wenn Preis final vereinbart
// ist, der Auftrag kostenpflichtig ist und noch keine Rechnung existiert.
export async function createInvoiceAction(orderId: string) {
  if (!(await hasPermission("invoicing"))) return;
  await connectDB();

  const order = await Order.findById(orderId).lean();
  if (!order || order.compensationType !== "paid") return;

  const existing = await Invoice.findOne({ orderId });
  if (existing) return;

  const { state } = await getPriceState(orderId);
  if (state.phase !== "agreed") return;

  // Settings-Dokument sicherstellen, BEVOR der Zähler erhöht wird — sonst
  // liefert "returnDocument: before" bei einer Neuanlage `null` zurück (kein
  // "davor" bei einem Insert), was zweimal in Folge dieselbe Rechnungsnummer
  // erzeugen und an Invoice.number's unique-Index scheitern würde.
  await getSettings();

  const [client, settingsDoc] = await Promise.all([
    Client.findById(order.clientId).lean(),
    Settings.findOneAndUpdate(
      { key: "main" },
      { $inc: { "invoiceSettings.nextInvoiceNumber": 1 } },
      { returnDocument: "before" }
    ).lean(),
  ]);
  if (!client) return;

  const inv = settingsDoc?.invoiceSettings;
  const number = `${inv?.invoiceNumberPrefix || "RG-"}${String(inv?.nextInvoiceNumber || 1).padStart(4, "0")}`;
  const kleinunternehmer = inv?.kleinunternehmer ?? true;
  const vatRate = inv?.vatRate ?? 19;

  // Vereinbarter Preis wird als Bruttopreis behandelt; bei Umsatzsteuerpflicht
  // wird der Nettobetrag daraus zurückgerechnet. Ein eingelöster Rabattcode
  // mindert den Rechnungsbetrag bereits hier, damit Rechnung und tatsächlich
  // zu zahlender Betrag übereinstimmen.
  const totalAmount = getPayableAmount(state.latest.amount, getTotalOrderDiscount(order));
  const netAmount = kleinunternehmer ? totalAmount : Math.round((totalAmount / (1 + vatRate / 100)) * 100) / 100;
  const vatAmount = Math.round((totalAmount - netAmount) * 100) / 100;

  let invoice;
  try {
    invoice = await Invoice.create({
      orderId,
      clientId: order.clientId,
      number,
      issueDate: new Date(),
      serviceDate: order.datePeriod ? formatDatePeriod(order.datePeriod, client.locale === "en" ? "en" : "de") : "",
      description: `${order.characterOrConcept}${order.shootingType ? ` (${order.shootingType})` : ""}`,
      netAmount,
      vatRate,
      vatAmount,
      totalAmount,
      discountCode: order.discountCode || "",
      discountAmount: order.discountAmount || 0,
      manualDiscountAmount: order.manualDiscountAmount || 0,
      manualDiscountNote: order.manualDiscountNote || "",
      kleinunternehmer,
      businessName: inv?.businessName || "Rakku Photography",
      businessLegalName: inv?.legalName || "",
      businessStreet: inv?.street || "",
      businessPostCode: inv?.postCode || "",
      businessCity: inv?.city || "",
      businessCountryCode: inv?.countryCode || "DE",
      businessTaxId: inv?.taxId || "",
      businessIban: inv?.iban || "",
      businessBic: inv?.bic || "",
      businessBankName: inv?.bankName || "",
      accountHolderName: inv?.accountHolderName || inv?.legalName || inv?.businessName || "",
      clientName: client.name,
      clientSalutation: client.salutation || "",
      clientStreet: client.billingStreet || "",
      clientPostCode: client.billingPostCode || "",
      clientCity: client.billingCity || "",
      clientCountryCode: client.billingCountryCode || "DE",
      paymentReference: formatPaymentReference(orderId),
      status: "sent",
    });
  } catch (err) {
    // Doppelklick/doppelte Anfrage kurz hintereinander — der unique-Index
    // auf Invoice.orderId (siehe models/Invoice.ts) verhindert eine zweite
    // Rechnung für denselben Auftrag; hier einfach so behandeln wie die
    // "existing"-Prüfung oben, statt die Seite mit einem Fehler abstürzen
    // zu lassen. Ein Sprung in der Rechnungsnummer dabei ist unkritisch.
    if (err && typeof err === "object" && "code" in err && err.code === 11000) return;
    throw err;
  }

  await logActivity(orderId, "invoice_created", "admin", `Rechnung ${number} erstellt`);
  revalidatePath("/de/admin/orders");
  revalidatePath("/[locale]/admin/orders/[orderId]", "page");
  revalidatePath(`/de/admin/orders/${orderId}`);
  revalidatePath("/[locale]/account/orders/[orderId]", "page");
  return String(invoice._id);
}

// Mahnstufen zählen pro Rechnung hoch (1 = Zahlungserinnerung, 2 = 1. Mahnung, …),
// inkl. gestaffelter Zusatzgebühr je Stufe und Kunden-Benachrichtigung — siehe
// lib/reminderEscalation.ts (gemeinsamer Kern mit der automatischen Eskalation).
// Nur möglich, solange der Auftrag noch nicht als bezahlt markiert ist.
export async function sendReminderAction(orderId: string, note: string) {
  if (!(await hasPermission("invoicing"))) return;
  return createReminderForOrder(orderId, note, false);
}
