"use client";

import { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/admin/ConfirmDialog";
import { toast } from "@/components/admin/Toast";
import {
  FetchError,
  apiDelete,
  apiGet,
  apiPatch,
  apiPost,
  formatDateShort,
} from "@/lib/client-api";
import {
  buildExportFilename,
  exportToCsv,
  exportToPdf,
  exportToXls,
  type ExportColumn,
} from "@/lib/exporters";
import { AdvanceModulesPanel } from "@/components/dashboard/AdvanceModulesPanel";

const TODO_STATUS_OPTIONS: { label: string; value: string; tone: string }[] = [
  { label: "Pending", value: "pending", tone: "bg-amber-100 text-amber-800" },
  { label: "In progress", value: "in_progress", tone: "bg-blue-100 text-blue-800" },
  { label: "Done", value: "done", tone: "bg-emerald-100 text-emerald-800" },
];

function normalizeStatus(v: unknown): string {
  const s = String(v ?? "").toLowerCase();
  if (s === "done" || s === "completed") return "done";
  if (s === "in_progress") return "in_progress";
  return "pending";
}

function statusMeta(s: string): { label: string; tone: string } {
  const found = TODO_STATUS_OPTIONS.find((o) => o.value === s);
  return found ?? { label: "Pending", tone: "bg-amber-100 text-amber-800" };
}

const MONEY_TYPES = new Set([
  "money",
  "cash",
  "bank",
  "upi",
  "card",
  "cheque",
]);

const GIFT_TYPE_META: Record<
  string,
  { label: string; tone: string; icon: string }
> = {
  cash: { label: "Cash", tone: "bg-emerald-100 text-emerald-700", icon: "$" },
  bank: { label: "Bank transfer", tone: "bg-blue-100 text-blue-700", icon: "⌬" },
  upi: { label: "UPI", tone: "bg-indigo-100 text-indigo-700", icon: "₹" },
  cheque: { label: "Cheque", tone: "bg-slate-100 text-slate-700", icon: "✎" },
  gold: { label: "Gold", tone: "bg-yellow-100 text-yellow-800", icon: "✦" },
  silver: { label: "Silver", tone: "bg-zinc-100 text-zinc-700", icon: "✧" },
  bronze: { label: "Bronze", tone: "bg-orange-100 text-orange-700", icon: "✺" },
  platinum: { label: "Platinum", tone: "bg-sky-100 text-sky-700", icon: "✪" },
  precious_other: { label: "Precious metal (other)", tone: "bg-yellow-50 text-yellow-700", icon: "✦" },
  photo: { label: "Photo", tone: "bg-pink-100 text-pink-700", icon: "▣" },
  video: { label: "Video", tone: "bg-rose-100 text-rose-700", icon: "▶" },
  appliance: { label: "Appliance", tone: "bg-cyan-100 text-cyan-700", icon: "▤" },
  flower: { label: "Flower", tone: "bg-fuchsia-100 text-fuchsia-700", icon: "❀" },
  clothing: { label: "Clothing", tone: "bg-orange-100 text-orange-700", icon: "♔" },
  cake: { label: "Cake", tone: "bg-amber-100 text-amber-700", icon: "❉" },
  other: { label: "Other", tone: "bg-surface-muted text-muted", icon: "✱" },
};

function giftMeta(type: string) {
  return GIFT_TYPE_META[type] ?? GIFT_TYPE_META.other;
}

function GiftTypePicker({
  value,
  onChange,
}: {
  value: string;
  onChange: (v: string) => void;
}) {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      if (!target.closest("[data-gift-type-picker]")) setOpen(false);
    };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  const isKnown = Object.prototype.hasOwnProperty.call(GIFT_TYPE_META, value);
  const meta = isKnown ? giftMeta(value) : {
    label: value || "Select…",
    tone: "bg-surface-muted text-foreground",
    icon: "✱",
  };
  const allEntries = Object.entries(GIFT_TYPE_META);
  const q = query.trim().toLowerCase();
  const filtered = q
    ? allEntries.filter(([v, m]) =>
        m.label.toLowerCase().includes(q) || v.toLowerCase().includes(q),
      )
    : allEntries;
  const canAddCustom =
    q.length > 0 && !allEntries.some(([, m]) => m.label.toLowerCase() === q);

  return (
    <div className="relative" data-gift-type-picker>
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="input flex w-full items-center justify-between gap-2 text-left"
      >
        <span className="flex items-center gap-2 truncate">
          <span className={`grid h-5 w-5 place-items-center rounded-md text-[10px] font-bold ${meta.tone}`}>
            {meta.icon}
          </span>
          <span className="truncate">{meta.label}</span>
          {!isKnown && value ? (
            <span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary">
              custom
            </span>
          ) : null}
        </span>
        <span className="text-xs text-muted">{open ? "▲" : "▼"}</span>
      </button>
      {open ? (
        <div className="absolute z-20 mt-1 w-full overflow-hidden rounded-xl border border-border bg-surface shadow-lg">
          <div className="border-b border-border p-2">
            <div className="relative">
              <span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-muted">
                ⌕
              </span>
              <input
                autoFocus
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === "Enter" && canAddCustom) {
                    e.preventDefault();
                    onChange(query.trim());
                    setOpen(false);
                    setQuery("");
                  }
                }}
                placeholder="Search or type a custom category…"
                className="input pl-8"
              />
            </div>
          </div>
          <ul className="max-h-56 overflow-y-auto py-1">
            {filtered.length === 0 ? (
              <li className="px-3 py-3 text-center text-xs text-muted">No match.</li>
            ) : (
              filtered.map(([v, m]) => (
                <li key={v}>
                  <button
                    type="button"
                    onClick={() => {
                      onChange(v);
                      setOpen(false);
                      setQuery("");
                    }}
                    className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-surface-muted ${
                      value === v ? "bg-surface-muted" : ""
                    }`}
                  >
                    <span className={`grid h-5 w-5 place-items-center rounded-md text-[10px] font-bold ${m.tone}`}>
                      {m.icon}
                    </span>
                    <span>{m.label}</span>
                  </button>
                </li>
              ))
            )}
            {canAddCustom ? (
              <li>
                <button
                  type="button"
                  onClick={() => {
                    onChange(query.trim());
                    setOpen(false);
                    setQuery("");
                  }}
                  className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-primary hover:bg-surface-muted"
                >
                  <span className="grid h-5 w-5 place-items-center rounded-md bg-primary/10 text-[10px] font-bold text-primary">
                    +
                  </span>
                  Use custom name &quot;{query.trim()}&quot;
                </button>
              </li>
            ) : null}
          </ul>
        </div>
      ) : null}
    </div>
  );
}

export interface ModuleFieldSchema {
  key: string;
  label: string;
  type: string;
  required?: boolean;
  placeholder?: string;
  options?: { label: string; value: string }[];
}

export interface PrebuiltModuleSchema {
  slug: string;
  name: string;
  description: string;
  icon: string;
  iconImage?: string | null;
  image?: string | null;
  category: string;
  itemFields: ModuleFieldSchema[];
  eventFields: ModuleFieldSchema[];
}

export interface ModuleItemView {
  id: string;
  moduleSlug: string;
  data: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
}

export function ModuleItemsPanel({
  eventId,
  eventTitle,
  schema,
  advanceFolderIds,
}: {
  eventId: string;
  eventTitle: string;
  schema: PrebuiltModuleSchema;
  advanceFolderIds?: string[];
}) {
  if (schema.slug === "todo") {
    return <TodoPanel eventId={eventId} eventTitle={eventTitle} schema={schema} />;
  }
  if (schema.slug === "advance-purchase") {
    return (
      <AdvancePurchasePanel
        eventId={eventId}
        eventTitle={eventTitle}
        schema={schema}
        attachedFolderIds={advanceFolderIds ?? []}
      />
    );
  }
  if (schema.slug === "budget") {
    return <BudgetPanel eventId={eventId} eventTitle={eventTitle} schema={schema} />;
  }
  if (schema.slug === "guest-list") {
    return <GuestListPanel eventId={eventId} eventTitle={eventTitle} schema={schema} />;
  }
  if (schema.slug === "gift") {
    return <GiftPanel eventId={eventId} eventTitle={eventTitle} schema={schema} />;
  }
  return <GenericModulePanel eventId={eventId} eventTitle={eventTitle} schema={schema} />;
}

function GenericModulePanel({
  eventId,
  eventTitle,
  schema,
}: {
  eventId: string;
  eventTitle: string;
  schema: PrebuiltModuleSchema;
}) {
  const [items, setItems] = useState<ModuleItemView[]>([]);
  const [loading, setLoading] = useState(true);
  const [draft, setDraft] = useState<Record<string, string>>({});
  const [submitting, setSubmitting] = useState(false);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editDraft, setEditDraft] = useState<Record<string, string>>({});
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);

  const reload = useCallback(async () => {
    try {
      setLoading(true);
      const res = await apiGet<{ items: ModuleItemView[] }>(
        `/api/events/${eventId}/items?moduleSlug=${encodeURIComponent(schema.slug)}`,
      );
      setItems(res.items);
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to load items.",
        "error",
      );
    } finally {
      setLoading(false);
    }
  }, [eventId, schema.slug]);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    reload();
  }, [reload]);

  const stats = useMemo(() => summarize(schema.slug, items), [schema.slug, items]);

  const addItem = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      setSubmitting(true);
      const data = buildPayload(schema.itemFields, draft);
      await apiPost(`/api/events/${eventId}/items`, {
        moduleSlug: schema.slug,
        data,
      });
      setDraft({});
      toast(`${schema.name} added.`, "success");
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to add item.",
        "error",
      );
    } finally {
      setSubmitting(false);
    }
  };

  const startEdit = (item: ModuleItemView) => {
    setEditingId(item.id);
    const next: Record<string, string> = {};
    for (const f of schema.itemFields) {
      const v = item.data[f.key];
      next[f.key] = v == null ? "" : String(v);
    }
    setEditDraft(next);
  };

  const saveEdit = async (itemId: string) => {
    try {
      const data = buildPayload(schema.itemFields, editDraft);
      await apiPatch(`/api/events/${eventId}/items/${itemId}`, { data });
      toast("Updated.", "success");
      setEditingId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to update.",
        "error",
      );
    }
  };

  const removeItem = async (itemId: string) => {
    try {
      await apiDelete(`/api/events/${eventId}/items/${itemId}`);
      toast("Deleted.", "info");
      setConfirmDeleteId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to delete.",
        "error",
      );
    }
  };

  const fields = schema.itemFields;
  const primaryField = fields[0];
  const secondaryFields = fields.slice(1);

  return (
    <div className="space-y-5">
      <header className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <h3 className="text-lg font-semibold text-foreground">{schema.name}</h3>
          <p className="mt-0.5 text-sm text-muted">{schema.description}</p>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <ExportMenu
            rows={items}
            columns={
              schema.slug === "expense-tracker"
                ? expenseColumns()
                : genericColumns(schema)
            }
            filenameBase={schema.name}
            title={schema.name}
            eventTitle={eventTitle}
          />
          {stats.map((s) => (
            <span
              key={s.label}
              className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ${s.tone}`}
            >
              <span className="h-1.5 w-1.5 rounded-full bg-current opacity-70" />
              {s.label}: {s.value}
            </span>
          ))}
        </div>
      </header>

      <form
        onSubmit={addItem}
        className="rounded-2xl border border-border bg-surface-muted/30 p-4 shadow-sm"
      >
        <div className="mb-3 flex items-center gap-2">
          <span className="grid h-6 w-6 place-items-center rounded-md bg-primary text-xs font-bold text-primary-foreground">
            +
          </span>
          <h4 className="text-sm font-semibold text-foreground">
            Add a new {schema.name.toLowerCase().replace(/s?$/, "")}
          </h4>
        </div>
        <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
          {fields.map((f) => (
            <FieldInput
              key={f.key}
              field={f}
              value={draft[f.key] ?? (f.type === "date" ? todayISO() : "")}
              onChange={(v) => setDraft((d) => ({ ...d, [f.key]: v }))}
              autoFilled={!draft[f.key] && f.type === "date"}
            />
          ))}
        </div>
        <div className="mt-4 flex items-center justify-between gap-2">
          <p className="text-xs text-muted">
            {primaryField
              ? `${primaryField.label}${primaryField.required ? " *" : ""} is the main field for each entry.`
              : "Fill in the fields above to add a new entry."}
          </p>
          <Button type="submit" size="sm" isLoading={submitting}>
            Add entry
          </Button>
        </div>
      </form>

      <div className="rounded-2xl border border-border bg-surface shadow-sm">
        <div className="flex items-center justify-between border-b border-border px-4 py-3">
          <h4 className="text-sm font-semibold text-foreground">
            All {schema.name.toLowerCase()}
          </h4>
          <span className="text-xs text-muted">
            {items.length} {items.length === 1 ? "entry" : "entries"}
          </span>
        </div>

        {loading ? (
          <div className="p-6 text-center text-sm text-muted">Loading…</div>
        ) : items.length === 0 ? (
          <div className="p-8 text-center">
            <p className="text-sm font-medium text-foreground">No entries yet</p>
            <p className="mt-1 text-xs text-muted">
              Add your first {schema.name.toLowerCase().replace(/s?$/, "")} using the form above.
            </p>
          </div>
        ) : (
          <ul className="divide-y divide-border">
            {items.map((item) => {
              const isEditing = editingId === item.id;
              const primary = primaryField ? item.data[primaryField.key] : null;
              return (
                <li key={item.id} className="px-4 py-3">
                  {isEditing ? (
                    <div>
                      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
                        {fields.map((f) => (
                          <FieldInput
                            key={f.key}
                            field={f}
                            value={editDraft[f.key] ?? ""}
                            onChange={(v) =>
                              setEditDraft((d) => ({ ...d, [f.key]: v }))
                            }
                          />
                        ))}
                      </div>
                      <div className="mt-3 flex justify-end gap-2">
                        <Button
                          variant="secondary"
                          size="sm"
                          onClick={() => setEditingId(null)}
                        >
                          Cancel
                        </Button>
                        <Button size="sm" onClick={() => saveEdit(item.id)}>
                          Save changes
                        </Button>
                      </div>
                    </div>
                  ) : (
                    <div className="flex items-start gap-3">
                      <div className="min-w-0 flex-1">
                        <div className="flex flex-wrap items-center gap-2">
                          <p className="truncate text-sm font-medium text-foreground">
                            {primary ? String(primary) : "Untitled"}
                          </p>
                          {secondaryFields.map((f) => {
                            const raw = item.data[f.key];
                            if (raw == null || raw === "") return null;
                            return (
                              <MetaPill
                                key={f.key}
                                field={f}
                                value={raw}
                              />
                            );
                          })}
                        </div>
                        <div className="mt-2 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
                          {secondaryFields.map((f) => {
                            const raw = item.data[f.key];
                            return (
                              <FieldView
                                key={f.key}
                                field={f}
                                value={raw}
                              />
                            );
                          })}
                        </div>
                      </div>
                      <div className="flex shrink-0 items-center gap-1">
                        <button
                          type="button"
                          onClick={() => startEdit(item)}
                          className="rounded-md px-2 py-1 text-xs font-medium text-muted hover:bg-surface-muted hover:text-foreground"
                        >
                          Edit
                        </button>
                        <button
                          type="button"
                          onClick={() => setConfirmDeleteId(item.id)}
                          className="rounded-md px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
                        >
                          Delete
                        </button>
                      </div>
                    </div>
                  )}
                </li>
              );
            })}
          </ul>
        )}
      </div>

      <ConfirmDialog
        open={confirmDeleteId !== null}
        title={`Delete this ${schema.name.toLowerCase().replace(/s?$/, "")}?`}
        description="This entry will be permanently removed."
        confirmLabel="Delete"
        tone="danger"
        onConfirm={() => {
          if (confirmDeleteId) void removeItem(confirmDeleteId);
        }}
        onCancel={() => setConfirmDeleteId(null)}
      />

      <PanelStyles />
    </div>
  );
}

function TodoPanel({
  eventId,
  eventTitle,
  schema,
}: {
  eventId: string;
  eventTitle: string;
  schema: PrebuiltModuleSchema;
}) {
  const [items, setItems] = useState<ModuleItemView[]>([]);
  const [loading, setLoading] = useState(true);
  const [draftTitle, setDraftTitle] = useState("");
  const [draftDueDate, setDraftDueDate] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editTitle, setEditTitle] = useState("");
  const [editStatus, setEditStatus] = useState("pending");
  const [editDueDate, setEditDueDate] = useState("");
  const [editNotes, setEditNotes] = useState("");
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
  const [query, setQuery] = useState("");
  const [statusFilter, setStatusFilter] = useState<"all" | "pending" | "in_progress" | "done">("all");

  const reload = useCallback(async () => {
    try {
      setLoading(true);
      const res = await apiGet<{ items: ModuleItemView[] }>(
        `/api/events/${eventId}/items?moduleSlug=${schema.slug}`,
      );
      setItems(res.items);
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to load tasks.",
        "error",
      );
    } finally {
      setLoading(false);
    }
  }, [eventId, schema.slug]);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    reload();
  }, [reload]);

  const counts = useMemo(() => {
    const c: { all: number; pending: number; in_progress: number; done: number } = {
      all: items.length,
      pending: 0,
      in_progress: 0,
      done: 0,
    };
    for (const i of items) {
      const s = normalizeStatus(i.data.status) as keyof typeof c;
      c[s] += 1;
    }
    return c;
  }, [items]);

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    return items
      .filter((i) =>
        statusFilter === "all" ? true : normalizeStatus(i.data.status) === statusFilter,
      )
      .filter((i) => {
        if (!q) return true;
        const title = String(i.data.title ?? "").toLowerCase();
        const notes = String(i.data.notes ?? "").toLowerCase();
        const assignee = String(i.data.assignee ?? "").toLowerCase();
        return title.includes(q) || notes.includes(q) || assignee.includes(q);
      })
      .sort((a, b) => {
        const order = { pending: 0, in_progress: 1, done: 2 } as const;
        const sa = order[normalizeStatus(a.data.status) as keyof typeof order] ?? 0;
        const sb = order[normalizeStatus(b.data.status) as keyof typeof order] ?? 0;
        if (sa !== sb) return sa - sb;
        return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
      });
  }, [items, query, statusFilter]);

  const addItem = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!draftTitle.trim()) return;
    try {
      setSubmitting(true);
      await apiPost(`/api/events/${eventId}/items`, {
        moduleSlug: schema.slug,
        data: {
          title: draftTitle.trim(),
          assignee: "",
          status: "pending",
          dueDate: draftDueDate || "",
          notes: "",
        },
      });
      setDraftTitle("");
      setDraftDueDate("");
      toast("Task added.", "success");
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to add task.",
        "error",
      );
    } finally {
      setSubmitting(false);
    }
  };

  const toggleStatus = async (item: ModuleItemView) => {
    const next = normalizeStatus(item.data.status) === "done" ? "pending" : "done";
    try {
      await apiPatch(`/api/events/${eventId}/items/${item.id}`, {
        data: { ...item.data, status: next },
      });
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to update task.",
        "error",
      );
    }
  };

  const startEdit = (item: ModuleItemView) => {
    setEditingId(item.id);
    setEditTitle(String(item.data.title ?? ""));
    setEditStatus(normalizeStatus(item.data.status));
    setEditDueDate(String(item.data.dueDate ?? ""));
    setEditNotes(String(item.data.notes ?? ""));
  };

  const saveEdit = async (itemId: string) => {
    try {
      const target = items.find((i) => i.id === itemId);
      if (!target) return;
      await apiPatch(`/api/events/${eventId}/items/${itemId}`, {
        data: {
          ...target.data,
          title: editTitle.trim(),
          status: editStatus,
          dueDate: editDueDate || "",
          notes: editNotes,
        },
      });
      toast("Task updated.", "success");
      setEditingId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to update task.",
        "error",
      );
    }
  };

  const removeItem = async (itemId: string) => {
    try {
      await apiDelete(`/api/events/${eventId}/items/${itemId}`);
      toast("Task deleted.", "info");
      setConfirmDeleteId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to delete task.",
        "error",
      );
    }
  };

  return (
    <div className="space-y-4">
      <header className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <h3 className="text-lg font-semibold text-foreground">{schema.name}</h3>
          <p className="mt-0.5 text-sm text-muted">{schema.description}</p>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <ExportMenu
            rows={filtered}
            columns={todoColumns()}
            filenameBase={schema.name}
            title={schema.name}
            eventTitle={eventTitle}
          />
          <div className="flex items-center gap-2 text-xs">
            <Pill label="Total" value={counts.all} tone="bg-surface-muted text-foreground" />
            <Pill label="Done" value={counts.done} tone="bg-emerald-100 text-emerald-800" />
            <Pill label="In progress" value={counts.in_progress} tone="bg-blue-100 text-blue-800" />
            <Pill label="Pending" value={counts.pending} tone="bg-amber-100 text-amber-800" />
          </div>
        </div>
      </header>

      <form
        onSubmit={addItem}
        className="flex flex-wrap items-end gap-3 rounded-2xl border border-border bg-surface p-4 shadow-sm"
      >
        <div className="min-w-0 flex-1">
          <label className="mb-1 block text-xs font-medium text-muted">
            New task <span className="text-red-500">*</span>
          </label>
          <input
            value={draftTitle}
            onChange={(e) => setDraftTitle(e.target.value)}
            placeholder="e.g. Book photographer"
            className="input"
            required
          />
        </div>
        <div className="w-44">
          <label className="mb-1 block text-xs font-medium text-muted">Due date</label>
          <input
            type="date"
            value={draftDueDate}
            onChange={(e) => setDraftDueDate(e.target.value)}
            className="input"
          />
        </div>
        <Button type="submit" size="sm" isLoading={submitting}>
          Add task
        </Button>
      </form>

      <div className="flex flex-wrap items-center gap-2 rounded-2xl border border-border bg-surface px-4 py-3 shadow-sm">
        <div className="relative min-w-0 flex-1">
          <span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-muted">
            ⌕
          </span>
          <input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Search tasks, assignee, notes…"
            className="input pl-8"
          />
        </div>
        <div className="flex items-center gap-1 rounded-lg border border-border bg-surface-muted/40 p-1">
          {(
            [
              { key: "all", label: "All" },
              { key: "pending", label: "Pending" },
              { key: "in_progress", label: "In progress" },
              { key: "done", label: "Done" },
            ] as const
          ).map((f) => (
            <button
              key={f.key}
              type="button"
              onClick={() => setStatusFilter(f.key)}
              className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
                statusFilter === f.key
                  ? "bg-surface text-foreground shadow-sm"
                  : "text-muted hover:text-foreground"
              }`}
            >
              {f.label}
            </button>
          ))}
        </div>
      </div>

      <div className="rounded-2xl border border-border bg-surface shadow-sm">
        {loading ? (
          <p className="p-6 text-center text-sm text-muted">Loading…</p>
        ) : items.length === 0 ? (
          <div className="p-10 text-center">
            <p className="text-base font-medium text-foreground">No tasks yet</p>
            <p className="mt-1 text-sm text-muted">
              Add your first task above to get started.
            </p>
          </div>
        ) : filtered.length === 0 ? (
          <p className="p-6 text-center text-sm text-muted">
            No tasks match your filter.
          </p>
        ) : (
          <ul className="divide-y divide-border">
            {filtered.map((item) => {
              const status = normalizeStatus(item.data.status);
              const meta = statusMeta(status);
              const isDone = status === "done";
              const title = String(item.data.title ?? "Untitled task");
              const due = String(item.data.dueDate ?? "");
              const assignee = String(item.data.assignee ?? "");
              const notes = String(item.data.notes ?? "");
              const isEditing = editingId === item.id;
              return (
                <li
                  key={item.id}
                  className={`group px-4 py-3 transition-colors hover:bg-surface-muted/40 ${
                    isDone ? "bg-emerald-50/30" : ""
                  }`}
                  onDoubleClick={() => {
                    if (!isEditing) void toggleStatus(item);
                  }}
                  title="Double-click to toggle complete"
                >
                  {isEditing ? (
                    <div className="space-y-3">
                      <div>
                        <label className="mb-1 block text-xs font-medium text-muted">
                          Title
                        </label>
                        <input
                          value={editTitle}
                          onChange={(e) => setEditTitle(e.target.value)}
                          className="input"
                        />
                      </div>
                      <div className="grid gap-3 sm:grid-cols-3">
                        <div>
                          <label className="mb-1 block text-xs font-medium text-muted">
                            Status
                          </label>
                          <select
                            value={editStatus}
                            onChange={(e) => setEditStatus(e.target.value)}
                            className="input"
                          >
                            {TODO_STATUS_OPTIONS.map((o) => (
                              <option key={o.value} value={o.value}>
                                {o.label}
                              </option>
                            ))}
                          </select>
                        </div>
                        <div>
                          <label className="mb-1 block text-xs font-medium text-muted">
                            Due date
                          </label>
                          <input
                            type="date"
                            value={editDueDate}
                            onChange={(e) => setEditDueDate(e.target.value)}
                            className="input"
                          />
                        </div>
                        <div>
                          <label className="mb-1 block text-xs font-medium text-muted">
                            Assignee
                          </label>
                          <input
                            value={assignee}
                            onChange={() => {}}
                            disabled
                            className="input opacity-60"
                            placeholder="Read-only for now"
                          />
                        </div>
                      </div>
                      <div>
                        <label className="mb-1 block text-xs font-medium text-muted">
                          Notes
                        </label>
                        <textarea
                          value={editNotes}
                          onChange={(e) => setEditNotes(e.target.value)}
                          rows={2}
                          className="input"
                        />
                      </div>
                      <div className="flex justify-end gap-2">
                        <Button
                          variant="secondary"
                          size="sm"
                          onClick={() => setEditingId(null)}
                        >
                          Cancel
                        </Button>
                        <Button size="sm" onClick={() => saveEdit(item.id)}>
                          Save
                        </Button>
                      </div>
                    </div>
                  ) : (
                    <div className="flex items-start gap-3">
                      <button
                        type="button"
                        onClick={() => void toggleStatus(item)}
                        aria-label={isDone ? "Mark as not done" : "Mark as done"}
                        className={`mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full border-2 transition-colors ${
                          isDone
                            ? "border-emerald-500 bg-emerald-500 text-white"
                            : "border-border bg-surface hover:border-primary"
                        }`}
                      >
                        {isDone ? "✓" : ""}
                      </button>
                      <div className="min-w-0 flex-1">
                        <div className="flex flex-wrap items-center gap-2">
                          <p
                            className={`truncate text-sm font-medium ${
                              isDone ? "text-muted line-through" : "text-foreground"
                            }`}
                          >
                            {title}
                          </p>
                          <span
                            className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${meta.tone}`}
                          >
                            {meta.label}
                          </span>
                          {due ? (
                            <span className="text-xs text-muted">
                              Due {formatDateShort(due)}
                            </span>
                          ) : null}
                        </div>
                        {notes ? (
                          <p
                            className={`mt-1 line-clamp-1 text-xs ${
                              isDone ? "text-muted line-through" : "text-muted"
                            }`}
                          >
                            {notes}
                          </p>
                        ) : null}
                      </div>
                      <div className="flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
                        <button
                          type="button"
                          onClick={() => startEdit(item)}
                          className="rounded-md px-2 py-1 text-xs font-medium text-muted hover:bg-surface-muted hover:text-foreground"
                        >
                          Edit
                        </button>
                        <button
                          type="button"
                          onClick={() => setConfirmDeleteId(item.id)}
                          className="rounded-md px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
                        >
                          Delete
                        </button>
                      </div>
                    </div>
                  )}
                </li>
              );
            })}
          </ul>
        )}
      </div>

      <ConfirmDialog
        open={confirmDeleteId !== null}
        title="Delete this task?"
        description="This task will be permanently removed."
        confirmLabel="Delete"
        tone="danger"
        onConfirm={() => {
          if (confirmDeleteId) void removeItem(confirmDeleteId);
        }}
        onCancel={() => setConfirmDeleteId(null)}
      />

      <PanelStyles />
    </div>
  );
}

function BudgetPanel({
  eventId,
  eventTitle,
  schema,
}: {
  eventId: string;
  eventTitle: string;
  schema: PrebuiltModuleSchema;
}) {
  const [items, setItems] = useState<ModuleItemView[]>([]);
  const [expenses, setExpenses] = useState<ModuleItemView[]>([]);
  const [loading, setLoading] = useState(true);
  const [draftTitle, setDraftTitle] = useState("");
  const [draftCategory, setDraftCategory] = useState("");
  const [draftEstimated, setDraftEstimated] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editTitle, setEditTitle] = useState("");
  const [editEstimated, setEditEstimated] = useState("");
  const [editActual, setEditActual] = useState("");
  const [editNotes, setEditNotes] = useState("");
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
  const [payTarget, setPayTarget] = useState<ModuleItemView | null>(null);

  const reload = useCallback(async () => {
    try {
      setLoading(true);
      const [bRes, eRes] = await Promise.all([
        apiGet<{ items: ModuleItemView[] }>(
          `/api/events/${eventId}/items?moduleSlug=${encodeURIComponent(schema.slug)}`,
        ),
        apiGet<{ items: ModuleItemView[] }>(
          `/api/events/${eventId}/items?moduleSlug=expense-tracker`,
        ),
      ]);
      setItems(bRes.items);
      setExpenses(eRes.items);
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to load budget.",
        "error",
      );
    } finally {
      setLoading(false);
    }
  }, [eventId, schema.slug]);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    reload();
  }, [reload]);

  const paidByBudget = useMemo(() => {
    const map = new Map<string, number>();
    for (const e of expenses) {
      const bid = String((e.data as Record<string, unknown>).linkedBudgetItemId ?? "");
      if (!bid) continue;
      const amt = Number((e.data as Record<string, unknown>).amount ?? 0);
      map.set(bid, (map.get(bid) ?? 0) + (Number.isFinite(amt) ? amt : 0));
    }
    return map;
  }, [expenses]);

  const paymentsForBudget = useMemo(() => {
    const map = new Map<string, ModuleItemView[]>();
    for (const e of expenses) {
      const bid = String((e.data as Record<string, unknown>).linkedBudgetItemId ?? "");
      if (!bid) continue;
      const list = map.get(bid) ?? [];
      list.push(e);
      map.set(bid, list);
    }
    for (const list of map.values()) {
      list.sort(
        (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
      );
    }
    return map;
  }, [expenses]);

  const summary = useMemo(() => {
    let estimated = 0;
    let actual = 0;
    let overCount = 0;
    for (const i of items) {
      const est = Number(i.data.estimated ?? 0);
      const act = Number(i.data.actual ?? 0);
      estimated += Number.isFinite(est) ? est : 0;
      actual += Number.isFinite(act) ? act : 0;
      if (act > est) overCount += 1;
    }
    const diff = estimated - actual;
    return { estimated, actual, diff, overCount };
  }, [items]);

  const addItem = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!draftTitle.trim()) return;
    try {
      setSubmitting(true);
      await apiPost(`/api/events/${eventId}/items`, {
        moduleSlug: schema.slug,
        data: {
          title: draftTitle.trim(),
          category: draftCategory.trim(),
          estimated: draftEstimated ? Number(draftEstimated) : 0,
          actual: 0,
          notes: "",
        },
      });
      setDraftTitle("");
      setDraftCategory("");
      setDraftEstimated("");
      toast("Budget item added.", "success");
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to add budget item.",
        "error",
      );
    } finally {
      setSubmitting(false);
    }
  };

  const startEdit = (item: ModuleItemView) => {
    setEditingId(item.id);
    setEditTitle(String(item.data.title ?? ""));
    setEditEstimated(String(item.data.estimated ?? ""));
    setEditActual(String(item.data.actual ?? ""));
    setEditNotes(String(item.data.notes ?? ""));
  };

  const saveEdit = async (itemId: string) => {
    try {
      const target = items.find((i) => i.id === itemId);
      if (!target) return;
      await apiPatch(`/api/events/${eventId}/items/${itemId}`, {
        data: {
          ...target.data,
          title: editTitle.trim(),
          estimated: editEstimated ? Number(editEstimated) : 0,
          actual: editActual ? Number(editActual) : 0,
          notes: editNotes,
        },
      });
      toast("Updated.", "success");
      setEditingId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to update.",
        "error",
      );
    }
  };

  const removeItem = async (itemId: string) => {
    try {
      await apiDelete(`/api/events/${eventId}/items/${itemId}`);
      toast("Deleted.", "info");
      setConfirmDeleteId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to delete.",
        "error",
      );
    }
  };

  const submitPayment = async (amount: number, paymentType: string, date: string) => {
    if (!payTarget) return;
    try {
      await apiPost(`/api/events/${eventId}/items`, {
        moduleSlug: "expense-tracker",
        data: {
          title: payTarget.data.title,
          category: String(payTarget.data.category ?? ""),
          amount,
          paymentType,
          paid: "yes",
          date,
          notes: `Payment for budget item ${payTarget.data.title}`,
          linkedBudgetItemId: payTarget.id,
        },
      });
      const paid = paidByBudget.get(payTarget.id) ?? 0;
      const newActual = paid + amount;
      await apiPatch(`/api/events/${eventId}/items/${payTarget.id}`, {
        data: { ...payTarget.data, actual: newActual },
      });
      toast("Payment recorded.", "success");
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to record payment.",
        "error",
      );
    }
  };

  const deletePayment = async (paymentId: string) => {
    try {
      const payment = expenses.find((e) => e.id === paymentId);
      await apiDelete(`/api/events/${eventId}/items/${paymentId}`);
      if (payment) {
        const bid = String(
          (payment.data as Record<string, unknown>).linkedBudgetItemId ?? "",
        );
        if (bid) {
          const target = items.find((i) => i.id === bid);
          if (target) {
            const remaining = (paidByBudget.get(bid) ?? 0) - Number(
              (payment.data as Record<string, unknown>).amount ?? 0,
            );
            await apiPatch(`/api/events/${eventId}/items/${bid}`, {
              data: { ...target.data, actual: Math.max(remaining, 0) },
            });
          }
        }
      }
      toast("Payment removed.", "info");
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to delete payment.",
        "error",
      );
    }
  };

  const overBudget = summary.diff < 0;
  const diffAbs = Math.abs(summary.diff);

  return (
    <div className="space-y-4">
      <header className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <h3 className="text-lg font-semibold text-foreground">{schema.name}</h3>
          <p className="mt-0.5 text-sm text-muted">{schema.description}</p>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <ExportMenu
            rows={items}
            columns={budgetColumns()}
            filenameBase={schema.name}
            title={schema.name}
            eventTitle={eventTitle}
          />
          <div className="flex items-center gap-2 text-xs">
            <Pill label="Items" value={items.length} tone="bg-surface-muted text-foreground" />
          <Pill
            label="Over budget"
            value={summary.overCount}
            tone={summary.overCount > 0 ? "bg-red-100 text-red-800" : "bg-surface-muted text-muted"}
          />
          </div>
        </div>
      </header>

      <div className="grid gap-3 sm:grid-cols-3">
        <SummaryCard
          label="Total estimated"
          value={`$${summary.estimated.toFixed(2)}`}
          tone="bg-surface border-border"
        />
        <SummaryCard
          label="Total actual"
          value={`$${summary.actual.toFixed(2)}`}
          tone="bg-surface border-border"
        />
        <SummaryCard
          label={overBudget ? "Over budget" : "Saved"}
          value={`$${diffAbs.toFixed(2)}`}
          tone={
            overBudget
              ? "bg-red-50 border-red-200"
              : "bg-emerald-50 border-emerald-200"
          }
          valueTone={overBudget ? "text-red-700" : "text-emerald-700"}
          subtitle={
            overBudget
              ? "You have exceeded your planned budget."
              : "You are under your planned budget."
          }
        />
      </div>

      <form
        onSubmit={addItem}
        className="grid gap-3 rounded-2xl border border-border bg-surface p-4 shadow-sm sm:grid-cols-[1fr_180px_140px_auto]"
      >
        <div>
          <label className="mb-1 block text-xs font-medium text-muted">
            Item <span className="text-red-500">*</span>
          </label>
          <input
            value={draftTitle}
            onChange={(e) => setDraftTitle(e.target.value)}
            placeholder="e.g. Photographer"
            className="input"
            required
          />
        </div>
        <div>
          <label className="mb-1 block text-xs font-medium text-muted">Category</label>
          <input
            value={draftCategory}
            onChange={(e) => setDraftCategory(e.target.value)}
            placeholder="e.g. Photography"
            className="input"
          />
        </div>
        <div>
          <label className="mb-1 block text-xs font-medium text-muted">Estimated</label>
          <input
            type="number"
            step="0.01"
            value={draftEstimated}
            onChange={(e) => setDraftEstimated(e.target.value)}
            placeholder="0.00"
            className="input"
          />
        </div>
        <div className="flex items-end">
          <Button type="submit" size="sm" isLoading={submitting}>
            Add budget item
          </Button>
        </div>
      </form>

      <div className="rounded-2xl border border-border bg-surface shadow-sm">
        <div className="flex items-center justify-between border-b border-border px-4 py-3">
          <h4 className="text-sm font-semibold text-foreground">Budget items</h4>
          <span className="text-xs text-muted">
            {items.length} {items.length === 1 ? "entry" : "entries"}
          </span>
        </div>
        {loading ? (
          <p className="p-6 text-center text-sm text-muted">Loading…</p>
        ) : items.length === 0 ? (
          <div className="p-10 text-center">
            <p className="text-base font-medium text-foreground">
              No budget items yet
            </p>
            <p className="mt-1 text-sm text-muted">
              Add an item above and track actual spend against the estimate.
            </p>
          </div>
        ) : (
          <ul className="divide-y divide-border">
            {items.map((item) => {
              const isEditing = editingId === item.id;
              const title = String(item.data.title ?? "Untitled item");
              const category = String(item.data.category ?? "");
              const estimated = Number(item.data.estimated ?? 0);
              const actual =
                Number(item.data.actual ?? 0) > 0
                  ? Number(item.data.actual ?? 0)
                  : paidByBudget.get(item.id) ?? 0;
              const diff = estimated - actual;
              const isOver = diff < 0;
              return (
                <li
                  key={item.id}
                  className={`px-4 py-3 ${isOver ? "bg-red-50/40" : ""}`}
                >
                  {isEditing ? (
                    <div className="space-y-3">
                      <div className="grid gap-3 sm:grid-cols-3">
                        <div>
                          <label className="mb-1 block text-xs font-medium text-muted">
                            Item
                          </label>
                          <input
                            value={editTitle}
                            onChange={(e) => setEditTitle(e.target.value)}
                            className="input"
                          />
                        </div>
                        <div>
                          <label className="mb-1 block text-xs font-medium text-muted">
                            Estimated
                          </label>
                          <input
                            type="number"
                            step="0.01"
                            value={editEstimated}
                            onChange={(e) => setEditEstimated(e.target.value)}
                            className="input"
                          />
                        </div>
                        <div>
                          <label className="mb-1 block text-xs font-medium text-muted">
                            Actual
                          </label>
                          <input
                            type="number"
                            step="0.01"
                            value={editActual}
                            onChange={(e) => setEditActual(e.target.value)}
                            className="input"
                          />
                        </div>
                      </div>
                      <div>
                        <label className="mb-1 block text-xs font-medium text-muted">
                          Notes
                        </label>
                        <textarea
                          value={editNotes}
                          onChange={(e) => setEditNotes(e.target.value)}
                          rows={2}
                          className="input"
                        />
                      </div>
                      <div className="flex justify-end gap-2">
                        <Button
                          variant="secondary"
                          size="sm"
                          onClick={() => setEditingId(null)}
                        >
                          Cancel
                        </Button>
                        <Button size="sm" onClick={() => saveEdit(item.id)}>
                          Save
                        </Button>
                      </div>
                    </div>
                  ) : (
                    <div>
                      <div className="flex items-start gap-3">
                        <div className="min-w-0 flex-1">
                          <div className="flex flex-wrap items-center gap-2">
                            <p className="truncate text-sm font-medium text-foreground">
                              {title}
                            </p>
                            {category ? (
                              <span className="rounded-full bg-surface-muted px-2 py-0.5 text-xs text-muted">
                                {category}
                              </span>
                            ) : null}
                            <DiffBadge diff={diff} estimated={estimated} actual={actual} />
                          </div>
                          <div className="mt-2 grid gap-3 sm:grid-cols-3">
                            <Stat label="Estimated" value={`$${estimated.toFixed(2)}`} />
                            <Stat
                              label="Actual"
                              value={`$${actual.toFixed(2)}`}
                              tone={isOver ? "text-red-700" : "text-foreground"}
                            />
                            <Stat
                              label={isOver ? "Over by" : "Saved"}
                              value={`$${Math.abs(diff).toFixed(2)}`}
                              tone={isOver ? "text-red-700" : "text-emerald-700"}
                            />
                          </div>
                        </div>
                        <div className="flex shrink-0 items-center gap-2">
                          <Button
                            size="sm"
                            variant="secondary"
                            onClick={() => setPayTarget(item)}
                          >
                            Pay
                          </Button>
                          <button
                            type="button"
                            onClick={() => startEdit(item)}
                            className="rounded-md px-2 py-1 text-xs font-medium text-muted hover:bg-surface-muted hover:text-foreground"
                          >
                            Edit
                          </button>
                          <button
                            type="button"
                            onClick={() => setConfirmDeleteId(item.id)}
                            className="rounded-md px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
                          >
                            Delete
                          </button>
                        </div>
                      </div>
                      <PaymentHistory
                        payments={paymentsForBudget.get(item.id) ?? []}
                      />
                    </div>
                  )}
                </li>
              );
            })}
          </ul>
        )}
      </div>

      <ConfirmDialog
        open={confirmDeleteId !== null}
        title="Delete this budget item?"
        description="The item and its link to expense payments will be removed."
        confirmLabel="Delete"
        tone="danger"
        onConfirm={() => {
          if (confirmDeleteId) void removeItem(confirmDeleteId);
        }}
        onCancel={() => setConfirmDeleteId(null)}
      />

      {payTarget ? (
        <PaymentDialog
          target={payTarget}
          estimated={Number(payTarget.data.estimated ?? 0)}
          alreadyPaid={paidByBudget.get(payTarget.id) ?? 0}
          history={paymentsForBudget.get(payTarget.id) ?? []}
          onCancel={() => setPayTarget(null)}
          onConfirm={submitPayment}
          onDeletePayment={deletePayment}
        />
      ) : null}

      <PanelStyles />
    </div>
  );
}

function GuestListPanel({
  eventId,
  eventTitle,
  schema,
}: {
  eventId: string;
  eventTitle: string;
  schema: PrebuiltModuleSchema;
}) {
  const [items, setItems] = useState<ModuleItemView[]>([]);
  const [loading, setLoading] = useState(true);
  const [draft, setDraft] = useState<Record<string, string>>({});
  const [submitting, setSubmitting] = useState(false);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editDraft, setEditDraft] = useState<Record<string, string>>({});
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
  const [query, setQuery] = useState("");
  const [rsvpFilter, setRsvpFilter] = useState<"all" | "pending" | "accepted" | "declined">("all");
  const [attendanceFilter, setAttendanceFilter] = useState<"all" | "came" | "not_came">("all");

  const reload = useCallback(async () => {
    try {
      setLoading(true);
      const res = await apiGet<{ items: ModuleItemView[] }>(
        `/api/events/${eventId}/items?moduleSlug=${encodeURIComponent(schema.slug)}`,
      );
      setItems(res.items);
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to load guests.",
        "error",
      );
    } finally {
      setLoading(false);
    }
  }, [eventId, schema.slug]);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    reload();
  }, [reload]);

  const normalizeRsvp = (v: unknown) => String(v ?? "pending").toLowerCase();
  const cameValue = (v: unknown) => {
    if (v === true || v === "yes" || v === "came") return "came";
    if (v === false || v === "no" || v === "no_show") return "not_came";
    return "unknown";
  };

  const counts = useMemo(() => {
    let totalGuests = 0;
    let accepted = 0;
    let pending = 0;
    let declined = 0;
    let came = 0;
    let notCame = 0;
    for (const i of items) {
      totalGuests += 1 + Number(i.data.plusOnes ?? 0);
      const r = normalizeRsvp(i.data.rsvp);
      if (r === "accepted") accepted += 1;
      else if (r === "declined") declined += 1;
      else pending += 1;
      const a = cameValue(i.data.attendance);
      if (a === "came") came += 1;
      else if (a === "not_came") notCame += 1;
    }
    return { totalGuests, accepted, pending, declined, came, notCame };
  }, [items]);

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    return items
      .filter((i) => {
        if (rsvpFilter === "all") return true;
        return normalizeRsvp(i.data.rsvp) === rsvpFilter;
      })
      .filter((i) => {
        if (attendanceFilter === "all") return true;
        return cameValue(i.data.attendance) === attendanceFilter;
      })
      .filter((i) => {
        if (!q) return true;
        const hay = [
          i.data.name,
          i.data.phone,
          i.data.email,
          i.data.notes,
        ]
          .map((v) => String(v ?? "").toLowerCase())
          .join(" ");
        return hay.includes(q);
      })
      .sort((a, b) => {
        const an = String(a.data.name ?? "").toLowerCase();
        const bn = String(b.data.name ?? "").toLowerCase();
        return an.localeCompare(bn);
      });
  }, [items, query, rsvpFilter, attendanceFilter]);

  const addItem = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      setSubmitting(true);
      const data: Record<string, unknown> = {};
      for (const f of schema.itemFields) {
        const v = draft[f.key] ?? "";
        if (f.type === "number") {
          data[f.key] = v ? Number(v) : 0;
        } else {
          data[f.key] = v;
        }
      }
      if (!data.rsvp) data.rsvp = "pending";
      await apiPost(`/api/events/${eventId}/items`, {
        moduleSlug: schema.slug,
        data,
      });
      setDraft({});
      toast("Guest added.", "success");
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to add guest.",
        "error",
      );
    } finally {
      setSubmitting(false);
    }
  };

  const startEdit = (item: ModuleItemView) => {
    setEditingId(item.id);
    const next: Record<string, string> = {};
    for (const f of schema.itemFields) {
      next[f.key] = item.data[f.key] == null ? "" : String(item.data[f.key]);
    }
    setEditDraft(next);
  };

  const saveEdit = async (itemId: string) => {
    try {
      const target = items.find((i) => i.id === itemId);
      if (!target) return;
      const data: Record<string, unknown> = { ...target.data };
      for (const f of schema.itemFields) {
        const v = editDraft[f.key] ?? "";
        if (f.type === "number") {
          data[f.key] = v ? Number(v) : 0;
        } else {
          data[f.key] = v;
        }
      }
      await apiPatch(`/api/events/${eventId}/items/${itemId}`, { data });
      toast("Updated.", "success");
      setEditingId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to update.",
        "error",
      );
    }
  };

  const removeItem = async (itemId: string) => {
    try {
      await apiDelete(`/api/events/${eventId}/items/${itemId}`);
      toast("Removed.", "info");
      setConfirmDeleteId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to remove.",
        "error",
      );
    }
  };

  const toggleAttendance = async (item: ModuleItemView) => {
    const next = cameValue(item.data.attendance) === "came" ? "no" : "yes";
    try {
      await apiPatch(`/api/events/${eventId}/items/${item.id}`, {
        data: { ...item.data, attendance: next },
      });
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to update attendance.",
        "error",
      );
    }
  };

  return (
    <div className="space-y-4">
      <header className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <h3 className="text-lg font-semibold text-foreground">{schema.name}</h3>
          <p className="mt-0.5 text-sm text-muted">{schema.description}</p>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <ExportMenu
            rows={filtered}
            columns={guestColumns()}
            filenameBase={schema.name}
            title={schema.name}
            eventTitle={eventTitle}
          />
          <div className="flex flex-wrap items-center gap-2 text-xs">
            <Pill label="Total people" value={counts.totalGuests} tone="bg-blue-100 text-blue-800" />
            <Pill label="Guests" value={items.length} tone="bg-surface-muted text-foreground" />
            <Pill label="Accepted" value={counts.accepted} tone="bg-emerald-100 text-emerald-800" />
            <Pill label="Pending" value={counts.pending} tone="bg-amber-100 text-amber-800" />
            <Pill label="Declined" value={counts.declined} tone="bg-red-100 text-red-800" />
            <Pill label="Came" value={counts.came} tone="bg-emerald-100 text-emerald-800" />
            <Pill label="Didn't come" value={counts.notCame} tone="bg-red-100 text-red-800" />
          </div>
        </div>
      </header>

      <form
        onSubmit={addItem}
        className="rounded-2xl border border-border bg-surface p-4 shadow-sm"
      >
        <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
          {schema.itemFields.map((f) => (
            <FieldInput
              key={f.key}
              field={f}
              value={draft[f.key] ?? (f.type === "date" ? todayISO() : "")}
              onChange={(v) => setDraft((d) => ({ ...d, [f.key]: v }))}
            />
          ))}
        </div>
        <div className="mt-3 flex justify-end">
          <Button type="submit" size="sm" isLoading={submitting}>
            Add guest
          </Button>
        </div>
      </form>

      <div className="flex flex-wrap items-center gap-2 rounded-2xl border border-border bg-surface px-4 py-3 shadow-sm">
        <div className="relative min-w-0 flex-1">
          <span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-muted">
            ⌕
          </span>
          <input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Search name, phone, email…"
            className="input pl-8"
          />
        </div>
        <Segmented
          label="RSVP"
          value={rsvpFilter}
          options={[
            { key: "all", label: "All" },
            { key: "pending", label: "Pending" },
            { key: "accepted", label: "Accepted" },
            { key: "declined", label: "Declined" },
          ]}
          onChange={(v) =>
            setRsvpFilter(v as "all" | "pending" | "accepted" | "declined")
          }
        />
        <Segmented
          label="Attendance"
          value={attendanceFilter}
          options={[
            { key: "all", label: "All" },
            { key: "came", label: "Came" },
            { key: "not_came", label: "No-show" },
          ]}
          onChange={(v) =>
            setAttendanceFilter(v as "all" | "came" | "not_came")
          }
        />
      </div>

      <div className="rounded-2xl border border-border bg-surface shadow-sm">
        {loading ? (
          <p className="p-6 text-center text-sm text-muted">Loading…</p>
        ) : items.length === 0 ? (
          <div className="p-10 text-center">
            <p className="text-base font-medium text-foreground">No guests yet</p>
            <p className="mt-1 text-sm text-muted">
              Add your first guest above.
            </p>
          </div>
        ) : filtered.length === 0 ? (
          <p className="p-6 text-center text-sm text-muted">
            No guests match your filters.
          </p>
        ) : (
          <ul className="divide-y divide-border">
            {filtered.map((item) => {
              const isEditing = editingId === item.id;
              const name = String(item.data.name ?? "Untitled");
              const rsvp = normalizeRsvp(item.data.rsvp);
              const attendance = cameValue(item.data.attendance);
              const plusOnes = Number(item.data.plusOnes ?? 0);
              const phone = String(item.data.phone ?? "");
              const email = String(item.data.email ?? "");
              return (
                <li key={item.id} className="px-4 py-3">
                  {isEditing ? (
                    <div className="space-y-3">
                      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
                        {schema.itemFields.map((f) => (
                          <FieldInput
                            key={f.key}
                            field={f}
                            value={editDraft[f.key] ?? ""}
                            onChange={(v) =>
                              setEditDraft((d) => ({ ...d, [f.key]: v }))
                            }
                          />
                        ))}
                      </div>
                      <div className="flex justify-end gap-2">
                        <Button
                          variant="secondary"
                          size="sm"
                          onClick={() => setEditingId(null)}
                        >
                          Cancel
                        </Button>
                        <Button size="sm" onClick={() => saveEdit(item.id)}>
                          Save
                        </Button>
                      </div>
                    </div>
                  ) : (
                    <div className="flex items-center gap-3">
                      <button
                        type="button"
                        onClick={() => void toggleAttendance(item)}
                        aria-label={
                          attendance === "came"
                            ? "Mark as didn't come"
                            : "Mark as came"
                        }
                        title={
                          attendance === "came"
                            ? "Came — click to undo"
                            : "Click to mark came"
                        }
                        className={`grid h-9 w-9 shrink-0 place-items-center rounded-full border-2 text-base transition-colors ${
                          attendance === "came"
                            ? "border-emerald-500 bg-emerald-500 text-white"
                            : attendance === "not_came"
                              ? "border-red-300 bg-red-50 text-red-600"
                              : "border-border bg-surface text-muted hover:border-primary"
                        }`}
                      >
                        {attendance === "came" ? "✓" : attendance === "not_came" ? "✕" : "?"}
                      </button>
                      <div className="min-w-0 flex-1">
                        <div className="flex flex-wrap items-center gap-2">
                          <p className="truncate text-sm font-medium text-foreground">
                            {name}
                          </p>
                          <span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${rsvpTone(rsvp)}`}>
                            {rsvpLabel(rsvp)}
                          </span>
                          {plusOnes > 0 ? (
                            <span className="rounded-full bg-blue-100 px-2 py-0.5 text-[10px] font-medium text-blue-800">
                              +{plusOnes} {plusOnes === 1 ? "person" : "people"}
                            </span>
                          ) : null}
                          {attendance === "came" ? (
                            <span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-800">
                              Came
                            </span>
                          ) : null}
                        </div>
                        {(phone || email) && (
                          <p className="mt-0.5 text-xs text-muted">
                            {[phone, email].filter(Boolean).join(" · ")}
                          </p>
                        )}
                      </div>
                      <div className="flex shrink-0 items-center gap-1">
                        <button
                          type="button"
                          onClick={() => startEdit(item)}
                          className="rounded-md px-2 py-1 text-xs font-medium text-muted hover:bg-surface-muted hover:text-foreground"
                        >
                          Edit
                        </button>
                        <button
                          type="button"
                          onClick={() => setConfirmDeleteId(item.id)}
                          className="rounded-md px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
                        >
                          Delete
                        </button>
                      </div>
                    </div>
                  )}
                </li>
              );
            })}
          </ul>
        )}
      </div>

      <ConfirmDialog
        open={confirmDeleteId !== null}
        title="Remove this guest?"
        description="The guest will be removed from this event."
        confirmLabel="Remove"
        tone="danger"
        onConfirm={() => {
          if (confirmDeleteId) void removeItem(confirmDeleteId);
        }}
        onCancel={() => setConfirmDeleteId(null)}
      />

      <PanelStyles />
    </div>
  );
}

function rsvpTone(value: string): string {
  switch (value) {
    case "accepted":
      return "bg-emerald-100 text-emerald-800";
    case "declined":
      return "bg-red-100 text-red-800";
    default:
      return "bg-amber-100 text-amber-800";
  }
}

function rsvpLabel(value: string): string {
  switch (value) {
    case "accepted":
      return "Accepted";
    case "declined":
      return "Declined";
    default:
      return "Pending";
  }
}

function Segmented({
  label,
  value,
  options,
  onChange,
}: {
  label: string;
  value: string;
  options: { key: string; label: string }[];
  onChange: (v: string) => void;
}) {
  return (
    <div className="flex items-center gap-1 rounded-lg border border-border bg-surface-muted/40 p-1">
      <span className="px-2 text-[10px] font-medium uppercase tracking-wide text-muted">
        {label}
      </span>
      {options.map((o) => (
        <button
          key={o.key}
          type="button"
          onClick={() => onChange(o.key)}
          className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
            value === o.key
              ? "bg-surface text-foreground shadow-sm"
              : "text-muted hover:text-foreground"
          }`}
        >
          {o.label}
        </button>
      ))}
    </div>
  );
}

function GuestPicker({
  guests,
  value,
  onChange,
  giftedIds,
}: {
  guests: { id: string; name: string }[];
  value: string;
  onChange: (guestId: string, guestName: string) => void;
  giftedIds: Set<string>;
}) {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");


  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      if (!target.closest("[data-guest-picker]")) {
        setOpen(false);
      }
    };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  const selected = guests.find((g) => g.id === value) ?? null;
  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return guests;
    return guests.filter((g) => g.name.toLowerCase().includes(q));
  }, [guests, query]);

  return (
    <div className="relative" data-guest-picker>
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className={`input flex w-full items-center justify-between gap-2 text-left ${
          selected ? "" : "text-muted"
        }`}
      >
        <span className="truncate">
          {selected
            ? selected.name + (giftedIds.has(selected.id) ? " ✓ Gifted" : "")
            : "— Manual entry / not from list —"}
        </span>
        <span className="text-xs text-muted">{open ? "▲" : "▼"}</span>
      </button>
      {open ? (
        <div className="absolute z-20 mt-1 w-full overflow-hidden rounded-xl border border-border bg-surface shadow-lg">
          <div className="border-b border-border p-2">
            <div className="relative">
              <span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-muted">
                ⌕
              </span>
              <input
                autoFocus
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                placeholder="Search guests…"
                className="input pl-8"
              />
            </div>
          </div>
          <ul className="max-h-56 overflow-y-auto py-1">
            <li>
              <button
                type="button"
                onClick={() => {
                  onChange("", "");
                  setOpen(false);
                  setQuery("");
                }}
                className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm text-muted hover:bg-surface-muted"
              >
                <span>— Manual entry / not from list —</span>
              </button>
            </li>
            {filtered.length === 0 ? (
              <li className="px-3 py-3 text-center text-xs text-muted">
                No guests match.
              </li>
            ) : (
              filtered.map((g) => {
                const gifted = giftedIds.has(g.id);
                return (
                  <li key={g.id}>
                    <button
                      type="button"
                      onClick={() => {
                        onChange(g.id, g.name);
                        setOpen(false);
                        setQuery("");
                      }}
                      className={`flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm hover:bg-surface-muted ${
                        gifted ? "text-emerald-700" : ""
                      }`}
                    >
                      <span className={gifted ? "font-semibold" : "font-medium"}>
                        {g.name}
                      </span>
                      {gifted ? (
                        <span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-800">
                          ✓ Gifted
                        </span>
                      ) : null}
                    </button>
                  </li>
                );
              })
            )}
          </ul>
        </div>
      ) : null}
    </div>
  );
}

function GiftPanel({
  eventId,
  eventTitle,
  schema,
}: {
  eventId: string;
  eventTitle: string;
  schema: PrebuiltModuleSchema;
}) {
  const [items, setItems] = useState<ModuleItemView[]>([]);
  const [guests, setGuests] = useState<ModuleItemView[]>([]);
  const [loading, setLoading] = useState(true);
  const [draft, setDraft] = useState<Record<string, string>>({});
  const [submitting, setSubmitting] = useState(false);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editDraft, setEditDraft] = useState<Record<string, string>>({});
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
  const [query, setQuery] = useState("");
  const [typeFilter, setTypeFilter] = useState<"all" | "money" | "other">("all");

  const reload = useCallback(async () => {
    try {
      setLoading(true);
      const [gRes, guRes] = await Promise.all([
        apiGet<{ items: ModuleItemView[] }>(
          `/api/events/${eventId}/items?moduleSlug=${encodeURIComponent(schema.slug)}`,
        ),
        apiGet<{ items: ModuleItemView[] }>(
          `/api/events/${eventId}/items?moduleSlug=guest-list`,
        ),
      ]);
      setItems(gRes.items);
      setGuests(guRes.items);
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to load gifts.",
        "error",
      );
    } finally {
      setLoading(false);
    }
  }, [eventId, schema.slug]);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    reload();
  }, [reload]);

  const guestOptions = useMemo(() => {
    return guests.map((g) => ({
      id: g.id,
      name: String(g.data.name ?? "Guest"),
    }));
  }, [guests]);

  const giftsByGuest = useMemo(() => {
    const map = new Map<string, ModuleItemView[]>();
    for (const i of items) {
      const bid = String((i.data as Record<string, unknown>).guestId ?? "");
      if (!bid) continue;
      const list = map.get(bid) ?? [];
      list.push(i);
      map.set(bid, list);
    }
    return map;
  }, [items]);

  const giftedIds = useMemo(() => new Set(giftsByGuest.keys()), [giftsByGuest]);

  const totals = useMemo(() => {
    let money = 0;
    let moneyCount = 0;
    let otherCount = 0;
    const byType = new Map<string, number>();
    for (const i of items) {
      const t = String(i.data.giftType ?? "cash").toLowerCase();
      const amt = Number(i.data.amount ?? 0);
      byType.set(t, (byType.get(t) ?? 0) + 1);
      if (MONEY_TYPES.has(t)) {
        money += Number.isFinite(amt) ? amt : 0;
        moneyCount += 1;
      } else {
        otherCount += 1;
      }
    }
    return { money, moneyCount, otherCount, byType };
  }, [items]);

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    return items
      .filter((i) => {
        const t = String(i.data.giftType ?? "").toLowerCase();
        if (typeFilter === "all") return true;
        if (typeFilter === "money") return MONEY_TYPES.has(t);
        return !MONEY_TYPES.has(t);
      })
      .filter((i) => {
        if (!q) return true;
        const hay = [
          i.data.title,
          i.data.guestName,
          i.data.notes,
        ]
          .map((v) => String(v ?? "").toLowerCase())
          .join(" ");
        return hay.includes(q);
      })
      .sort(
        (a, b) =>
          new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
      );
  }, [items, query, typeFilter]);

  const updateDraft = (patch: Record<string, string>) =>
    setDraft((d) => ({ ...d, ...patch }));

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      setSubmitting(true);
      const data: Record<string, unknown> = {};
      for (const f of schema.itemFields) {
        const v = draft[f.key] ?? (f.type === "date" ? todayISO() : "");
        if (f.type === "number" || f.type === "currency") {
          data[f.key] = v ? Number(v) : 0;
        } else {
          data[f.key] = v ?? "";
        }
      }
      await apiPost(`/api/events/${eventId}/items`, {
        moduleSlug: schema.slug,
        data,
      });
      setDraft({});
      toast("Gift added.", "success");
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to add gift.",
        "error",
      );
    } finally {
      setSubmitting(false);
    }
  };

  const startEdit = (item: ModuleItemView) => {
    setEditingId(item.id);
    const next: Record<string, string> = {};
    for (const f of schema.itemFields) {
      next[f.key] = item.data[f.key] == null ? "" : String(item.data[f.key]);
    }
    setEditDraft(next);
  };

  const saveEdit = async (itemId: string) => {
    try {
      const target = items.find((i) => i.id === itemId);
      if (!target) return;
      const data: Record<string, unknown> = { ...target.data };
      for (const f of schema.itemFields) {
        const v = editDraft[f.key] ?? "";
        if (f.type === "number" || f.type === "currency") {
          data[f.key] = v ? Number(v) : 0;
        } else {
          data[f.key] = v;
        }
      }
      await apiPatch(`/api/events/${eventId}/items/${itemId}`, { data });
      toast("Updated.", "success");
      setEditingId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to update.",
        "error",
      );
    }
  };

  const removeItem = async (itemId: string) => {
    try {
      await apiDelete(`/api/events/${eventId}/items/${itemId}`);
      toast("Deleted.", "info");
      setConfirmDeleteId(null);
      await reload();
    } catch (err) {
      toast(
        err instanceof FetchError ? err.message : "Failed to delete.",
        "error",
      );
    }
  };

  return (
    <div className="space-y-4">
      <header className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <h3 className="text-lg font-semibold text-foreground">{schema.name}</h3>
          <p className="mt-0.5 text-sm text-muted">{schema.description}</p>
        </div>
        <ExportMenu
          rows={filtered}
          columns={giftColumns()}
          filenameBase={schema.name}
          title={schema.name}
          eventTitle={eventTitle}
        />
      </header>

      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
        <SummaryCard
          label="Total money received"
          value={`$${totals.money.toFixed(2)}`}
          tone="bg-emerald-50 border-emerald-200"
          valueTone="text-emerald-700"
          subtitle={`${totals.moneyCount} monetary gift${totals.moneyCount === 1 ? "" : "s"}`}
        />
        <SummaryCard
          label="Money gifts"
          value={totals.moneyCount}
          tone="bg-surface border-border"
        />
        <SummaryCard
          label="Other gifts"
          value={totals.otherCount}
          tone="bg-blue-50 border-blue-200"
          valueTone="text-blue-700"
          subtitle="Cash equivalents excluded"
        />
        <SummaryCard
          label="Guests covered"
          value={`${giftedIds.size} / ${guestOptions.length}`}
          tone={
            giftedIds.size === guestOptions.length && guestOptions.length > 0
              ? "bg-emerald-50 border-emerald-200"
              : "bg-surface border-border"
          }
          valueTone={
            giftedIds.size === guestOptions.length && guestOptions.length > 0
              ? "text-emerald-700"
              : "text-foreground"
          }
          subtitle={`${guestOptions.length - giftedIds.size} not yet gifted`}
        />
      </div>

      <form
        onSubmit={submit}
        className="rounded-2xl border border-border bg-surface p-4 shadow-sm"
      >
        <div className="mb-3 flex items-center gap-2">
          <span className="grid h-6 w-6 place-items-center rounded-md bg-primary text-xs font-bold text-primary-foreground">
            +
          </span>
          <h4 className="text-sm font-semibold text-foreground">
            Add a new gift
          </h4>
        </div>
        <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
          <div>
            <label className="mb-1 block text-xs font-medium text-muted">
              Title <span className="text-red-500">*</span>
            </label>
            <input
              value={draft.title ?? ""}
              onChange={(e) => updateDraft({ title: e.target.value })}
              placeholder="e.g. Cash envelope, Photo album"
              className="input"
              required
            />
          </div>
          <div>
            <label className="mb-1 block text-xs font-medium text-muted">
              From guest
            </label>
            <GuestPicker
              guests={guestOptions}
              value={draft.guestId ?? ""}
              giftedIds={giftedIds}
              onChange={(id, name) => {
                updateDraft({ guestId: id, guestName: name });
                if (id && !draft.title) {
                  updateDraft({ title: `Gift from ${name}` });
                }
              }}
            />
          </div>
          <div>
            <label className="mb-1 block text-xs font-medium text-muted">
              Gift type
            </label>
            <GiftTypePicker
              value={draft.giftType ?? "cash"}
              onChange={(v) => updateDraft({ giftType: v })}
            />
          </div>
          <div>
            <label className="mb-1 block text-xs font-medium text-muted">
              Amount
            </label>
            <input
              type="number"
              step="0.01"
              value={draft.amount ?? ""}
              onChange={(e) => updateDraft({ amount: e.target.value })}
              placeholder="0.00"
              className="input"
            />
          </div>
          <div>
            <label className="mb-1 block text-xs font-medium text-muted">Date</label>
            <input
              type="date"
              value={draft.date ?? todayISO()}
              onChange={(e) => updateDraft({ date: e.target.value })}
              className="input"
            />
          </div>
          <div className="sm:col-span-2 lg:col-span-3">
            <label className="mb-1 block text-xs font-medium text-muted">Notes</label>
            <textarea
              value={draft.notes ?? ""}
              onChange={(e) => updateDraft({ notes: e.target.value })}
              rows={2}
              placeholder="Anything you'd like to remember about this gift"
              className="input"
            />
          </div>
        </div>
        <div className="mt-3 flex justify-end">
          <Button type="submit" size="sm" isLoading={submitting}>
            Add gift
          </Button>
        </div>
      </form>

      <div className="flex flex-wrap items-center gap-2 rounded-2xl border border-border bg-surface px-4 py-3 shadow-sm">
        <div className="relative min-w-0 flex-1">
          <span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-muted">
            ⌕
          </span>
          <input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Search gifts, guest, notes…"
            className="input pl-8"
          />
        </div>
        <Segmented
          label="Type"
          value={typeFilter}
          options={[
            { key: "all", label: "All" },
            { key: "money", label: "Money" },
            { key: "other", label: "Other" },
          ]}
          onChange={(v) =>
            setTypeFilter(v as "all" | "money" | "other")
          }
        />
      </div>

      <div className="rounded-2xl border border-border bg-surface shadow-sm">
        {loading ? (
          <p className="p-6 text-center text-sm text-muted">Loading…</p>
        ) : items.length === 0 ? (
          <div className="p-10 text-center">
            <p className="text-base font-medium text-foreground">No gifts yet</p>
            <p className="mt-1 text-sm text-muted">
              Log a gift above. You can pick from the guest list or add manually.
            </p>
          </div>
        ) : filtered.length === 0 ? (
          <p className="p-6 text-center text-sm text-muted">
            No gifts match your filters.
          </p>
        ) : (
          <ul className="divide-y divide-border">
            {filtered.map((item) => {
              const isEditing = editingId === item.id;
              const title = String(item.data.title ?? "Untitled");
              const guestName = String(item.data.guestName ?? "");
              const giftType = String(item.data.giftType ?? "money");
              const amount = Number(item.data.amount ?? 0);
              const date = String(item.data.date ?? item.createdAt);
              const notes = String(item.data.notes ?? "");
              return (
                <li key={item.id} className="px-4 py-3">
                  {isEditing ? (
                    <div className="space-y-3">
                      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
                        {schema.itemFields.map((f) => {
                          if (f.key === "guestId") {
                            return (
                              <div key={f.key}>
                                <label className="mb-1 block text-xs font-medium text-muted">
                                  {f.label}
                                </label>
                                <select
                                  value={editDraft[f.key] ?? ""}
                                  onChange={(e) =>
                                    setEditDraft((d) => ({
                                      ...d,
                                      guestId: e.target.value,
                                      guestName:
                                        guestOptions.find(
                                          (x) => x.id === e.target.value,
                                        )?.name ?? "",
                                    }))
                                  }
                                  className="input"
                                >
                                  <option value="">— Manual entry —</option>
                                  {guestOptions.map((g) => (
                                    <option key={g.id} value={g.id}>
                                      {g.name}
                                    </option>
                                  ))}
                                </select>
                              </div>
                            );
                          }
                          return (
                            <FieldInput
                              key={f.key}
                              field={f}
                              value={editDraft[f.key] ?? ""}
                              onChange={(v) =>
                                setEditDraft((d) => ({ ...d, [f.key]: v }))
                              }
                            />
                          );
                        })}
                      </div>
                      <div className="flex justify-end gap-2">
                        <Button
                          variant="secondary"
                          size="sm"
                          onClick={() => setEditingId(null)}
                        >
                          Cancel
                        </Button>
                        <Button size="sm" onClick={() => saveEdit(item.id)}>
                          Save
                        </Button>
                      </div>
                    </div>
                  ) : (
                    <div className="flex items-start gap-3">
                      <span
                        className={`grid h-9 w-9 shrink-0 place-items-center rounded-full text-sm font-bold ${giftTone(giftType)}`}
                      >
                        {giftIcon(giftType)}
                      </span>
                      <div className="min-w-0 flex-1">
                        <div className="flex flex-wrap items-center gap-2">
                          <p className="truncate text-sm font-medium text-foreground">
                            {title}
                          </p>
                          {giftType === "money" && amount > 0 ? (
                            <span className="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-semibold text-emerald-800">
                              ${amount.toFixed(2)}
                            </span>
                          ) : null}
                          <span className="rounded-full bg-surface-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted">
                            {giftTypeLabel(giftType)}
                          </span>
                          {guestName ? (
                            <span className="rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-medium text-blue-800">
                              From {guestName}
                            </span>
                          ) : (
                            <span className="rounded-full bg-surface-muted px-2 py-0.5 text-[10px] font-medium text-muted">
                              Manual entry
                            </span>
                          )}
                          <span className="text-xs text-muted">
                            {formatDateShort(date)}
                          </span>
                        </div>
                        {notes ? (
                          <p className="mt-1 line-clamp-2 text-xs text-muted">
                            {notes}
                          </p>
                        ) : null}
                      </div>
                      <div className="flex shrink-0 items-center gap-1">
                        <button
                          type="button"
                          onClick={() => startEdit(item)}
                          className="rounded-md px-2 py-1 text-xs font-medium text-muted hover:bg-surface-muted hover:text-foreground"
                        >
                          Edit
                        </button>
                        <button
                          type="button"
                          onClick={() => setConfirmDeleteId(item.id)}
                          className="rounded-md px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
                        >
                          Delete
                        </button>
                      </div>
                    </div>
                  )}
                </li>
              );
            })}
          </ul>
        )}
      </div>

      <ConfirmDialog
        open={confirmDeleteId !== null}
        title="Delete this gift?"
        description="The gift entry will be removed."
        confirmLabel="Delete"
        tone="danger"
        onConfirm={() => {
          if (confirmDeleteId) void removeItem(confirmDeleteId);
        }}
        onCancel={() => setConfirmDeleteId(null)}
      />

      <PanelStyles />
    </div>
  );
}

function giftTone(type: string): string {
  return giftMeta(type).tone;
}

function giftIcon(type: string): string {
  return giftMeta(type).icon;
}

function giftTypeLabel(type: string): string {
  return giftMeta(type).label;
}

function DiffBadge({
  diff,
  estimated,
  actual,
}: {
  diff: number;
  estimated: number;
  actual: number;
}) {
  if (estimated === 0 && actual === 0) return null;
  if (diff < 0) {
    return (
      <span className="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800">
        Over by ${Math.abs(diff).toFixed(2)}
      </span>
    );
  }
  if (diff > 0) {
    return (
      <span className="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-800">
        Saved ${diff.toFixed(2)}
      </span>
    );
  }
  return (
    <span className="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800">
      On budget
    </span>
  );
}

function SummaryCard({
  label,
  value,
  tone,
  valueTone = "text-foreground",
  subtitle,
}: {
  label: string;
  value: string | number;
  tone: string;
  valueTone?: string;
  subtitle?: string;
}) {
  return (
    <div className={`rounded-2xl border p-4 shadow-sm ${tone}`}>
      <p className="text-[10px] font-medium uppercase tracking-wide text-muted">
        {label}
      </p>
      <p className={`mt-1 text-2xl font-semibold ${valueTone}`}>{value}</p>
      {subtitle ? <p className="mt-1 text-xs text-muted">{subtitle}</p> : null}
    </div>
  );
}

function Stat({
  label,
  value,
  tone = "text-foreground",
}: {
  label: string;
  value: string;
  tone?: string;
}) {
  return (
    <div className="rounded-lg bg-surface-muted/40 px-3 py-2">
      <p className="text-[10px] font-medium uppercase tracking-wide text-muted">
        {label}
      </p>
      <p className={`mt-0.5 text-sm font-semibold ${tone}`}>{value}</p>
    </div>
  );
}

function PaymentDialog({
  target,
  estimated,
  alreadyPaid,
  history,
  onCancel,
  onConfirm,
  onDeletePayment,
}: {
  target: ModuleItemView;
  estimated: number;
  alreadyPaid: number;
  history: ModuleItemView[];
  onCancel: () => void;
  onConfirm: (amount: number, paymentType: string, date: string) => Promise<void>;
  onDeletePayment: (paymentId: string) => Promise<void>;
}) {
  const remaining = Math.max(estimated - alreadyPaid, 0);
  const [amount, setAmount] = useState(
    remaining > 0 ? remaining.toFixed(2) : "",
  );
  const [paymentType, setPaymentType] = useState("cash");
  const [date, setDate] = useState(todayISO());
  const [submitting, setSubmitting] = useState(false);

  const submit = async () => {
    const amt = Number(amount);
    if (!Number.isFinite(amt) || amt <= 0) {
      toast("Enter a payment amount greater than 0.", "error");
      return;
    }
    try {
      setSubmitting(true);
      await onConfirm(amt, paymentType, date);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="fixed inset-0 z-50 grid place-items-center bg-black/40 p-4">
      <div className="w-full max-w-2xl rounded-2xl border border-border bg-surface p-5 shadow-lg">
        <div className="mb-1 flex items-center gap-2">
          <span className="grid h-6 w-6 place-items-center rounded-md bg-primary text-xs font-bold text-primary-foreground">
            $
          </span>
          <h3 className="text-base font-semibold text-foreground">Record payment</h3>
        </div>
        <p className="text-sm text-muted">
          {String(target.data.title ?? "Budget item")}
        </p>

        <div className="mt-4 grid gap-3 sm:grid-cols-3">
          <SummaryMini label="Estimated" value={`$${estimated.toFixed(2)}`} />
          <SummaryMini label="Already paid" value={`$${alreadyPaid.toFixed(2)}`} />
          <SummaryMini
            label="Remaining"
            value={`$${Math.max(estimated - alreadyPaid, 0).toFixed(2)}`}
            tone={
              estimated - alreadyPaid > 0 ? "text-amber-700" : "text-emerald-700"
            }
          />
        </div>

        <div className="mt-4 grid gap-4 md:grid-cols-2">
          <div className="space-y-3 rounded-xl border border-border bg-surface-muted/30 p-3">
            <h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
              New payment
            </h4>
            <div>
              <label className="mb-1 block text-xs font-medium text-muted">
                Amount <span className="text-red-500">*</span>
              </label>
              <input
                type="number"
                step="0.01"
                value={amount}
                onChange={(e) => setAmount(e.target.value)}
                className="input"
                autoFocus
              />
            </div>
            <div>
              <label className="mb-1 block text-xs font-medium text-muted">
                Payment method
              </label>
              <select
                value={paymentType}
                onChange={(e) => setPaymentType(e.target.value)}
                className="input"
              >
                <option value="cash">Cash</option>
                <option value="card">Card</option>
                <option value="upi">UPI</option>
                <option value="bank_transfer">Bank transfer</option>
                <option value="other">Other</option>
              </select>
            </div>
            <div>
              <label className="mb-1 block text-xs font-medium text-muted">Date</label>
              <input
                type="date"
                value={date}
                onChange={(e) => setDate(e.target.value)}
                className="input"
              />
            </div>
          </div>

          <div className="rounded-xl border border-border bg-surface-muted/20 p-3">
            <div className="mb-2 flex items-center justify-between">
              <h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
                Payment history
              </h4>
              <span className="text-[10px] font-medium text-muted">
                {history.length} {history.length === 1 ? "payment" : "payments"}
              </span>
            </div>
            {history.length === 0 ? (
              <p className="py-6 text-center text-xs text-muted">
                No payments yet.
              </p>
            ) : (
              <ul className="max-h-56 space-y-2 overflow-y-auto pr-1">
                {history.map((p) => {
                  const pdata = p.data as Record<string, unknown>;
                  return (
                    <li
                      key={p.id}
                      className="rounded-lg border border-border bg-surface p-2"
                    >
                      <div className="flex items-start justify-between gap-2">
                        <div className="min-w-0 flex-1">
                          <div className="flex flex-wrap items-center gap-2">
                            <span className="text-sm font-semibold text-foreground">
                              ${Number(pdata.amount ?? 0).toFixed(2)}
                            </span>
                            <span className="rounded-full bg-surface-muted px-2 py-0.5 text-[10px] font-medium text-muted">
                              {paymentLabel(String(pdata.paymentType ?? "other"))}
                            </span>
                          </div>
                          <div className="mt-1 flex flex-wrap gap-x-2 gap-y-0.5 text-[10px] text-muted">
                            <span>📅 {formatDateShort(String(pdata.date ?? p.createdAt))}</span>
                            <span>•</span>
                            <span>Logged {formatDateShort(p.createdAt)}</span>
                          </div>
                          {pdata.notes ? (
                            <p className="mt-1 line-clamp-2 text-[11px] text-muted">
                              {String(pdata.notes)}
                            </p>
                          ) : null}
                        </div>
                        <button
                          type="button"
                          onClick={() => {
                            if (
                              confirm(
                                "Delete this payment? It will be removed from the expense list.",
                              )
                            ) {
                              void onDeletePayment(p.id);
                            }
                          }}
                          className="shrink-0 rounded-md p-1 text-xs text-red-600 hover:bg-red-50"
                          aria-label="Delete payment"
                        >
                          ✕
                        </button>
                      </div>
                    </li>
                  );
                })}
              </ul>
            )}
          </div>
        </div>

        <div className="mt-5 flex justify-end gap-2">
          <Button variant="secondary" size="sm" onClick={onCancel}>
            Close
          </Button>
          <Button size="sm" onClick={submit} isLoading={submitting}>
            Record payment
          </Button>
        </div>
      </div>
    </div>
  );
}

function paymentLabel(value: string): string {
  switch (value) {
    case "cash":
      return "Cash";
    case "card":
      return "Card";
    case "upi":
      return "UPI";
    case "bank_transfer":
      return "Bank transfer";
    default:
      return "Other";
  }
}

function PaymentHistory({ payments }: { payments: ModuleItemView[] }) {
  const [expanded, setExpanded] = useState(false);
  if (payments.length === 0) {
    return (
      <p className="mt-2 text-[11px] text-muted">
        No payments recorded yet.
      </p>
    );
  }
  const total = payments.reduce(
    (acc, p) => acc + Number((p.data as Record<string, unknown>).amount ?? 0),
    0,
  );
  const visible = expanded ? payments : payments.slice(0, 2);
  return (
    <div className="mt-3 rounded-xl border border-dashed border-border bg-surface-muted/30 p-3">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-2 text-xs text-muted">
          <span className="grid h-5 w-5 place-items-center rounded-md bg-primary/10 text-[10px] font-bold text-primary">
            {payments.length}
          </span>
          <span>
            {payments.length === 1 ? "1 payment" : `${payments.length} payments`} ·{" "}
            <span className="font-semibold text-foreground">
              ${total.toFixed(2)}
            </span>{" "}
            paid so far
          </span>
        </div>
        {payments.length > 2 ? (
          <button
            type="button"
            onClick={() => setExpanded((v) => !v)}
            className="text-xs font-medium text-primary hover:underline"
          >
            {expanded ? "Show less" : `Show all (${payments.length})`}
          </button>
        ) : null}
      </div>
      <ul className="mt-2 grid gap-1.5 sm:grid-cols-2">
        {visible.map((p) => {
          const pdata = p.data as Record<string, unknown>;
          return (
            <li
              key={p.id}
              className="flex items-center justify-between gap-2 rounded-md bg-surface px-2 py-1.5 text-xs"
            >
              <span className="truncate">
                <span className="font-semibold text-foreground">
                  ${Number(pdata.amount ?? 0).toFixed(2)}
                </span>{" "}
                <span className="text-muted">·</span>{" "}
                <span className="text-muted">
                  {paymentLabel(String(pdata.paymentType ?? "other"))}
                </span>{" "}
                <span className="text-muted">·</span>{" "}
                <span className="text-muted">
                  {formatDateShort(String(pdata.date ?? p.createdAt))}
                </span>
              </span>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

function SummaryMini({
  label,
  value,
  tone = "text-foreground",
}: {
  label: string;
  value: string;
  tone?: string;
}) {
  return (
    <div className="rounded-lg border border-border bg-surface-muted/40 p-2">
      <p className="text-[10px] font-medium uppercase tracking-wide text-muted">
        {label}
      </p>
      <p className={`mt-0.5 text-sm font-semibold ${tone}`}>{value}</p>
    </div>
  );
}

function Pill({ label, value, tone }: { label: string; value: number | string; tone: string }) {
  return (
    <span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ${tone}`}>
      <span className="h-1.5 w-1.5 rounded-full bg-current opacity-70" />
      {label}: {value}
    </span>
  );
}


function FieldInput({
  field,
  value,
  onChange,
  autoFilled = false,
}: {
  field: ModuleFieldSchema;
  value: string;
  onChange: (v: string) => void;
  autoFilled?: boolean;
}) {
  const labelEl = (
    <label className="mb-1 block text-xs font-medium text-muted">
      {field.label}
      {field.required ? <span className="text-red-500"> *</span> : null}
    </label>
  );
  if (field.options?.length) {
    const opt = field.options.find((o) => o.value === value);
    return (
      <div>
        {labelEl}
        <div className="relative">
          <select
            value={value}
            onChange={(e) => onChange(e.target.value)}
            className="input appearance-none pr-8"
          >
            <option value="">Select…</option>
            {field.options.map((o) => (
              <option key={o.value} value={o.value}>
                {o.label}
              </option>
            ))}
          </select>
          <span className="pointer-events-none absolute inset-y-0 right-2 flex items-center text-xs text-muted">
            ▾
          </span>
          {value ? (
            <span className="sr-only">{opt?.label ?? value}</span>
          ) : null}
        </div>
      </div>
    );
  }
  let inputType = "text";
  if (field.type === "date") inputType = "date";
  else if (field.type === "datetime") inputType = "datetime-local";
  else if (field.type === "time") inputType = "time";
  else if (field.type === "email") inputType = "email";
  else if (field.type === "phone") inputType = "tel";
  else if (field.type === "url") inputType = "url";
  else if (field.type === "number" || field.type === "currency")
    inputType = "number";
  return (
    <div>
      {labelEl}
      {field.type === "textarea" ? (
        <textarea
          value={value}
          onChange={(e) => onChange(e.target.value)}
          rows={2}
          placeholder={field.placeholder}
          className="input"
        />
      ) : (
        <div className="relative">
          <input
            type={inputType}
            value={value}
            onChange={(e) => onChange(e.target.value)}
            step={field.type === "currency" ? "0.01" : undefined}
            placeholder={field.placeholder ?? defaultPlaceholder(field)}
            className="input"
          />
          {autoFilled ? (
            <span className="pointer-events-none absolute inset-y-0 right-2 flex items-center text-[10px] font-medium uppercase tracking-wide text-muted">
              Today
            </span>
          ) : null}
        </div>
      )}
    </div>
  );
}

function FieldView({
  field,
  value,
}: {
  field: ModuleFieldSchema;
  value: unknown;
}) {
  let display = "—";
  if (value != null && value !== "") {
    if (field.type === "date" || field.type === "datetime") {
      display = formatDateShort(String(value));
    } else if (field.type === "currency") {
      display = `$${Number(value).toFixed(2)}`;
    } else if (field.type === "number") {
      display = String(Number(value));
    } else if (field.options?.length) {
      const opt = field.options.find((o) => o.value === String(value));
      display = opt?.label ?? String(value);
    } else {
      display = String(value);
    }
  }
  return (
    <div>
      <p className="text-[10px] font-medium uppercase tracking-wide text-muted">
        {field.label}
      </p>
      <p className="text-sm text-foreground">{display}</p>
    </div>
  );
}

function MetaPill({ field, value }: { field: ModuleFieldSchema; value: unknown }) {
  let text = "";
  let tone = "bg-surface-muted text-muted";
  if (field.type === "currency") {
    text = `$${Number(value).toFixed(2)}`;
    tone = "bg-emerald-50 text-emerald-700";
  } else if (field.type === "date" || field.type === "datetime") {
    text = formatDateShort(String(value));
    tone = "bg-blue-50 text-blue-700";
  } else if (field.options?.length) {
    const opt = field.options.find((o) => o.value === String(value));
    text = opt?.label ?? String(value);
    const color = optionTone(field, String(value));
    if (color) tone = color;
  } else if (field.type === "boolean" || field.type === "checkbox") {
    text = value ? "Yes" : "No";
    tone = value
      ? "bg-emerald-50 text-emerald-700"
      : "bg-surface-muted text-muted";
  } else {
    text = String(value);
  }
  return (
    <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${tone}`}>
      {text}
    </span>
  );
}

function optionTone(field: ModuleFieldSchema, value: string): string | null {
  const lower = value.toLowerCase();
  if (lower === "yes" || lower === "paid" || lower === "done" || lower === "completed")
    return "bg-emerald-50 text-emerald-700";
  if (lower === "no" || lower === "pending" || lower === "overdue")
    return "bg-amber-50 text-amber-700";
  if (lower === "in_progress" || lower === "partial")
    return "bg-blue-50 text-blue-700";
  if (field.options?.some((o) => o.value === "inquiry") && lower === "inquiry")
    return "bg-amber-50 text-amber-700";
  if (lower === "booked")
    return "bg-blue-50 text-blue-700";
  return "bg-surface-muted text-muted";
}

function summarize(
  slug: string,
  items: ModuleItemView[],
): { label: string; value: string | number; tone: string }[] {
  if (items.length === 0) {
    return [{ label: "Total", value: 0, tone: "bg-surface-muted text-muted" }];
  }
  const total = { label: "Total", value: items.length, tone: "bg-surface-muted text-foreground" };
  const extras: { label: string; value: string | number; tone: string }[] = [];
  if (slug === "todo") {
    const done = items.filter((i) => {
      const s = String(i.data.status ?? "").toLowerCase();
      return s === "done" || s === "completed";
    }).length;
    extras.push({ label: "Done", value: done, tone: "bg-emerald-50 text-emerald-700" });
    extras.push({
      label: "Pending",
      value: items.length - done,
      tone: "bg-amber-50 text-amber-700",
    });
  } else if (slug === "expense-tracker" || slug === "budget" || slug === "payments") {
    const sum = items.reduce((acc, i) => acc + Number(i.data.amount ?? 0), 0);
    extras.push({ label: "Amount", value: `$${sum.toFixed(2)}`, tone: "bg-emerald-50 text-emerald-700" });
  } else if (slug === "guest-list") {
    const accepted = items.filter((i) => String(i.data.rsvp ?? "") === "accepted").length;
    extras.push({ label: "Accepted", value: accepted, tone: "bg-emerald-50 text-emerald-700" });
    extras.push({
      label: "Pending",
      value: items.length - accepted,
      tone: "bg-amber-50 text-amber-700",
    });
  }
  return [total, ...extras];
}

function buildPayload(
  fields: ModuleFieldSchema[],
  draft: Record<string, string>,
): Record<string, unknown> {
  const data: Record<string, unknown> = {};
  for (const f of fields) {
    const v = draft[f.key] ?? (f.type === "date" ? todayISO() : "");
    if (f.type === "number" || f.type === "currency") {
      data[f.key] = v ? Number(v) : 0;
    } else {
      data[f.key] = v ?? "";
    }
  }
  return data;
}

function todayISO(): string {
  const d = new Date();
  const pad = (n: number) => `${n}`.padStart(2, "0");
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}

function defaultPlaceholder(field: ModuleFieldSchema): string {
  switch (field.type) {
    case "currency":
      return "0.00";
    case "number":
      return "0";
    case "email":
      return "name@example.com";
    case "phone":
      return "+1 555 123 4567";
    case "url":
      return "https://…";
    default:
      return "";
  }
}

function ExportMenu<T>({
  rows,
  columns,
  filenameBase,
  title,
  subtitle,
  eventTitle,
}: {
  rows: T[];
  columns: ExportColumn<T>[];
  filenameBase: string;
  title: string;
  subtitle?: string;
  eventTitle: string;
}) {
  const [open, setOpen] = useState(false);
  const ref = useCallback((node: HTMLDivElement | null) => {
    void node;
  }, []);

  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      if (!target.closest("[data-export-menu]")) setOpen(false);
    };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  const handle = (kind: "csv" | "xls" | "pdf") => {
    setOpen(false);
    const base = buildExportFilename(eventTitle, filenameBase, kind);
    if (kind === "csv") exportToCsv(rows, columns, base);
    else if (kind === "xls") exportToXls(rows, columns, base);
    else
      exportToPdf(rows, columns, base, title, subtitle ?? `${rows.length} row${rows.length === 1 ? "" : "s"} exported`);
  };

  return (
    <div className="relative" data-export-menu>
      <button
        ref={ref as never}
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="inline-flex items-center gap-2 rounded-lg border border-border bg-surface px-3 py-1.5 text-xs font-medium text-foreground hover:bg-surface-muted"
      >
        <span>⤓</span>
        <span>Export</span>
        <span className="text-muted">▾</span>
      </button>
      {open ? (
        <div className="absolute right-0 z-20 mt-1 w-44 overflow-hidden rounded-xl border border-border bg-surface shadow-lg">
          <button
            type="button"
            onClick={() => handle("csv")}
            className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm hover:bg-surface-muted"
          >
            <span>CSV</span>
            <span className="text-[10px] text-muted">.csv</span>
          </button>
          <button
            type="button"
            onClick={() => handle("xls")}
            className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm hover:bg-surface-muted"
          >
            <span>Excel</span>
            <span className="text-[10px] text-muted">.xls</span>
          </button>
          <button
            type="button"
            onClick={() => handle("pdf")}
            className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm hover:bg-surface-muted"
          >
            <span>PDF</span>
            <span className="text-[10px] text-muted">print</span>
          </button>
        </div>
      ) : null}
    </div>
  );
}

function giftColumns(): ExportColumn<ModuleItemView>[] {
  return [
    { header: "Title", value: (r) => String(r.data.title ?? "") },
    {
      header: "Guest",
      value: (r) => String(r.data.guestName ?? ""),
    },
    {
      header: "Gift type",
      value: (r) => {
        const t = String(r.data.giftType ?? "");
        const meta = GIFT_TYPE_META[t];
        return meta?.label ?? t;
      },
    },
    { header: "Amount", value: (r) => Number(r.data.amount ?? 0) },
    { header: "Date", value: (r) => String(r.data.date ?? "") },
    { header: "Notes", value: (r) => String(r.data.notes ?? "") },
    {
      header: "Created",
      value: (r) => formatDateShort(r.createdAt),
    },
  ];
}

function todoColumns(): ExportColumn<ModuleItemView>[] {
  return [
    { header: "Task", value: (r) => String(r.data.title ?? "") },
    { header: "Assignee", value: (r) => String(r.data.assignee ?? "") },
    {
      header: "Status",
      value: (r) => statusMeta(normalizeStatus(r.data.status)).label,
    },
    { header: "Due date", value: (r) => String(r.data.dueDate ?? "") },
    { header: "Notes", value: (r) => String(r.data.notes ?? "") },
    { header: "Created", value: (r) => formatDateShort(r.createdAt) },
  ];
}

function guestColumns(): ExportColumn<ModuleItemView>[] {
  return [
    { header: "Name", value: (r) => String(r.data.name ?? "") },
    { header: "Phone", value: (r) => String(r.data.phone ?? "") },
    { header: "Email", value: (r) => String(r.data.email ?? "") },
    {
      header: "RSVP",
      value: (r) => rsvpLabel(String(r.data.rsvp ?? "pending").toLowerCase()),
    },
    { header: "Plus ones", value: (r) => Number(r.data.plusOnes ?? 0) },
    {
      header: "Attendance",
      value: (r) => {
        const a = String(r.data.attendance ?? "");
        if (a === "yes") return "Came";
        if (a === "no") return "Didn't come";
        return "Unknown";
      },
    },
    { header: "Notes", value: (r) => String(r.data.notes ?? "") },
    { header: "Created", value: (r) => formatDateShort(r.createdAt) },
  ];
}

function expenseColumns(): ExportColumn<ModuleItemView>[] {
  return [
    { header: "Title", value: (r) => String(r.data.title ?? "") },
    { header: "Category", value: (r) => String(r.data.category ?? "") },
    { header: "Amount", value: (r) => Number(r.data.amount ?? 0) },
    {
      header: "Payment type",
      value: (r) => paymentLabel(String(r.data.paymentType ?? "other")),
    },
    {
      header: "Paid",
      value: (r) => (String(r.data.paid ?? "") === "yes" ? "Yes" : "No"),
    },
    { header: "Date", value: (r) => String(r.data.date ?? "") },
    { header: "Notes", value: (r) => String(r.data.notes ?? "") },
    {
      header: "Linked budget item",
      value: (r) => String(r.data.linkedBudgetItemId ?? ""),
    },
    { header: "Created", value: (r) => formatDateShort(r.createdAt) },
  ];
}

function budgetColumns(): ExportColumn<ModuleItemView>[] {
  return [
    { header: "Item", value: (r) => String(r.data.title ?? "") },
    { header: "Category", value: (r) => String(r.data.category ?? "") },
    { header: "Estimated", value: (r) => Number(r.data.estimated ?? 0) },
    { header: "Actual", value: (r) => Number(r.data.actual ?? 0) },
    {
      header: "Difference",
      value: (r) =>
        Number(r.data.estimated ?? 0) - Number(r.data.actual ?? 0),
    },
    { header: "Notes", value: (r) => String(r.data.notes ?? "") },
    { header: "Created", value: (r) => formatDateShort(r.createdAt) },
  ];
}

function AdvancePurchasePanel({
  eventId,
  attachedFolderIds,
}: {
  eventId: string;
  eventTitle: string;
  schema: PrebuiltModuleSchema;
  attachedFolderIds: string[];
}) {
  return (
    <AdvanceModulesPanelForEvent
      eventId={eventId}
      attachedFolderIds={attachedFolderIds}
    />
  );
}

function AdvanceModulesPanelForEvent({
  eventId,
  attachedFolderIds,
}: {
  eventId: string;
  attachedFolderIds: string[];
}) {
  const attachedFolderIdsKey = attachedFolderIds.join(",");
  const [advanceModules, setAdvanceModules] = useState<
    Array<{
      advanceModuleId: string;
      folderId: string;
      name: string;
      slug: string;
      image: string | null;
      description: string;
    }>
  >([]);
  const [advanceFolders, setAdvanceFolders] = useState<
    Array<{
      folderId: string;
      name: string;
      description: string;
    }>
  >([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const folderRes = await apiGet<
          Array<{
            id: string;
            name: string;
            description: string;
          }>
        >(`/api/advance-folders`);
        if (cancelled) return;
        const allFolders = (folderRes ?? []).map((f) => ({
          folderId: f.id,
          name: f.name,
          description: f.description,
        }));
        // Only show folders the template has attached.
        const folderScope =
          attachedFolderIds.length > 0
            ? allFolders.filter((f) => attachedFolderIds.includes(f.folderId))
            : allFolders;
        setAdvanceFolders(folderScope);
        if (folderScope.length === 0) {
          setAdvanceModules([]);
          return;
        }
        const qs = folderScope
          .map((f) => `folderIds=${encodeURIComponent(f.folderId)}`)
          .join("&");
        const productRes = await apiGet<{
          items: Array<{
            id: string;
            folderId: string;
            name: string;
            slug: string;
            image: string | null;
            description: string;
          }>;
        }>(`/api/advance-modules?${qs}`);
        if (cancelled) return;
        setAdvanceModules(
          (productRes.items ?? []).map((m) => ({
            advanceModuleId: m.id,
            folderId: m.folderId,
            name: m.name,
            slug: m.slug,
            image: m.image,
            description: m.description,
          })),
        );
      } catch {
        if (!cancelled) {
          setAdvanceFolders([]);
          setAdvanceModules([]);
        }
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [attachedFolderIdsKey, attachedFolderIds]);
  if (loading) {
    return <p className="p-6 text-center text-sm text-muted">Loading…</p>;
  }
  return (
    <AdvanceModulesPanel
      eventId={eventId}
      advanceFolders={advanceFolders}
      advanceModules={advanceModules}
      enabled
    />
  );
}

function genericColumns(
  schema: PrebuiltModuleSchema,
): ExportColumn<ModuleItemView>[] {
  return schema.itemFields.map((f) => ({
    header: f.label,
    value: (r) => {
      const v = r.data[f.key];
      if (v == null) return "";
      if (f.options?.length) {
        const opt = f.options.find((o) => o.value === String(v));
        return opt?.label ?? String(v);
      }
      if (f.type === "currency" || f.type === "number") return Number(v);
      return String(v);
    },
  }));
}

function PanelStyles() {
  return (
    <style jsx>{`
      :global(.input) {
        width: 100%;
        border-radius: 0.5rem;
        border: 1px solid var(--border);
        background: var(--surface);
        padding: 0.5rem 0.75rem;
        font-size: 0.875rem;
        color: var(--foreground);
        transition: border-color 0.15s ease, box-shadow 0.15s ease;
      }
      :global(.input::placeholder) {
        color: var(--muted);
      }
      :global(.input:focus) {
        outline: none;
        border-color: var(--primary);
        box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
      }
    `}</style>
  );
}
