"use client";

import Link from "next/link";
import { useEffect, useState } from "react";

import { api, type AdminEvent, type EventUpdateResult } from "@/lib/api/browser";

import CardDesignPanel from "./CardDesignPanel";
import EventHostFields, {
  hostChanged,
  hostPatch,
  isHostIncomplete,
  seedHost,
  type HostValues,
} from "./EventHostFields";
import EventMessages, {
  isOverLimit,
  messagesPayload,
  seedMessages,
  type MessageValues,
} from "./EventMessages";
import Modal from "./Modal";
import { Button, ErrorNote, Field, Loading, formatDateTime, inputClass } from "./ui";

/**
 * Everything editable about one event, in one dialog (design D2, D3).
 *
 * All of the form state lives here rather than on the tile, which means it is born when the
 * modal opens and dies when it closes. A half-typed venue therefore cannot survive a close
 * and reappear on the next open — the bug a longer-lived version of this state would have,
 * and one that stays invisible until someone hits it.
 *
 * Moving `starts_at` is still the most consequential edit in the admin area: every reminder
 * wave is computed from it and hundreds of jobs are already queued against the old date. The
 * API re-points them automatically and reports what it did. Announcing the change to guests
 * stays a separate, explicit button, because emailing 800 people is a decision and
 * re-planning a queue is not.
 */

/** Ties the footer's Save button to the form it submits, across the modal's scroll boundary. */
const FORM_ID = "event-settings-form";

export default function EventEditorModal({
  event,
  hasPublishedCard,
  inheritedMessages,
  canEdit,
  onClose,
  onSaved,
  onDeleted,
}: {
  event: AdminEvent;
  hasPublishedCard: boolean;
  /** What this event's greeting reads when it stores no override of its own (design D11). */
  inheritedMessages: MessageValues;
  canEdit: boolean;
  onClose: () => void;
  onSaved: () => void;
  onDeleted: () => void;
}) {
  const [startsAt, setStartsAt] = useState(() => toLocalInput(event.starts_at));
  const [venue, setVenue] = useState(event.venue_name);
  const [capacity, setCapacity] = useState(event.capacity ?? 0);
  const [published, setPublished] = useState(event.is_published);
  const [result, setResult] = useState<EventUpdateResult | null>(null);
  const [error, setError] = useState<unknown>(null);
  const [saving, setSaving] = useState(false);
  const [broadcast, setBroadcast] = useState<string | null>(null);
  const [deleting, setDeleting] = useState(false);

  const seededMessages = seedMessages(event.invitation_messages, inheritedMessages);
  const [messages, setMessages] = useState<MessageValues>(seededMessages);

  const seededHost = seedHost(event);
  const [host, setHost] = useState<HostValues>(seededHost);

  const dateChanged = toLocalInput(event.starts_at) !== startsAt;
  const venueChanged = event.venue_name !== venue;
  const messagesChanged =
    messages.single !== seededMessages.single || messages.family !== seededMessages.family;
  const hostEdited = hostChanged(host, seededHost);

  // Compared against the event this form was seeded from, so a successful save — which
  // refreshes that event — clears the flag without any explicit reset (design D4).
  const dirty =
    dateChanged ||
    venueChanged ||
    messagesChanged ||
    hostEdited ||
    (event.capacity ?? 0) !== capacity ||
    event.is_published !== published;

  const overLimit = isOverLimit(messages);
  // An event with no host name or no phone cannot render its invitation footer, so the save
  // is refused here rather than by the API — the admin finds out while looking at the field.
  const hostIncomplete = isHostIncomplete(host);

  /**
   * The one place a close is decided (design D4).
   *
   * `Modal` already routes the ✕ button, the backdrop click and Escape through `onClose` —
   * that is the invariant its header comment exists to protect — so guarding here covers all
   * three with no per-path handling, and leaves no fourth path able to slip past.
   *
   * A native confirm rather than another dialog: this modal can already open the delete
   * confirmation on top of itself, and stacking a third `<dialog>` for a transient yes/no
   * would compound exactly the risk design D8 flags.
   */
  function requestClose() {
    if (dirty && !window.confirm("Discard your unsaved changes to this event?")) return;
    onClose();
  }

  async function save(e: React.FormEvent) {
    e.preventDefault();
    setSaving(true);
    setError(null);
    setBroadcast(null);
    try {
      const patch: Partial<AdminEvent> = {
        venue_name: venue,
        capacity: capacity || null,
        is_published: published,
      };
      // Only send starts_at when it actually moved. Sending it unchanged would still be
      // correct, but it makes every save look like a date change in the audit log.
      if (dateChanged) patch.starts_at = fromLocalInput(startsAt);
      // Same reasoning, and the map is sent whole so that clearing an override is
      // expressible — with a partial update, "not sent" and "cleared" would be one request.
      if (messagesChanged) {
        patch.invitation_messages = messagesPayload(messages, inheritedMessages);
      }
      // Same reasoning again: sending the host unchanged would be correct but would make
      // every save look like a host edit in the audit log.
      if (hostEdited) Object.assign(patch, hostPatch(host));
      setResult(await api.updateEvent(event.id, patch));
      onSaved();
    } catch (err) {
      setError(err);
    } finally {
      setSaving(false);
    }
  }

  async function announce() {
    try {
      const outcome = await api.broadcastDateChange(event.id, true);
      setBroadcast(
        `Queued ${outcome.queued} announcement(s); ${outcome.skipped_no_email} guest(s) have no email.`,
      );
    } catch (err) {
      setError(err);
    }
  }

  return (
    <>
      <Modal
        title={event.title_en}
        onClose={requestClose}
        size="full"
        footer={
          <div className="flex flex-wrap items-center gap-2">
            {/* A real link, not a router.push, so middle-click and open-in-new-tab work
                (design D6). It carries the Button's secondary styling by hand because
                Button renders a <button>. */}
            <Link
              href={`/admin/guests?event_id=${event.id}`}
              className="inline-flex min-h-11 items-center justify-center gap-2 rounded-md border border-stone-300 bg-white px-4 text-sm font-medium text-stone-800 transition-colors hover:bg-stone-50"
            >
              Guests
            </Link>

            {canEdit && (
              <button
                type="button"
                onClick={() => setDeleting(true)}
                className="min-h-11 rounded-md px-2 text-sm text-red-700 underline underline-offset-2 hover:bg-red-50"
              >
                Delete this event
              </button>
            )}

            <span className="ml-auto flex items-center gap-2">
              <Button type="button" variant="secondary" onClick={requestClose}>
                Close
              </Button>
              {canEdit && (
                <Button
                  type="submit"
                  form={FORM_ID}
                  disabled={saving || overLimit || hostIncomplete}
                  title={
                    overLimit
                      ? "One of the messages is over the 100-character limit"
                      : hostIncomplete
                        ? "A host name and a host phone are required"
                        : undefined
                  }
                >
                  {saving ? "Saving…" : "Save"}
                </Button>
              )}
            </span>
          </div>
        }
      >
        <div className="space-y-6">
          <section>
            <h3 className="mb-3 text-sm font-semibold text-stone-900">Settings</h3>

            <form id={FORM_ID} onSubmit={save} className="space-y-4">
              <div className="grid gap-4 sm:grid-cols-3">
                <Field label="Starts at" hint="Asia/Dhaka">
                  <input
                    type="datetime-local"
                    value={startsAt}
                    onChange={(e) => setStartsAt(e.target.value)}
                    disabled={!canEdit}
                    className={inputClass}
                  />
                </Field>
                <Field label="Venue">
                  <input
                    value={venue}
                    onChange={(e) => setVenue(e.target.value)}
                    disabled={!canEdit}
                    className={inputClass}
                  />
                </Field>
                <Field label="Capacity" hint="0 for no limit">
                  <input
                    type="number"
                    min={0}
                    value={capacity}
                    onChange={(e) => setCapacity(Number(e.target.value))}
                    disabled={!canEdit}
                    className={inputClass}
                  />
                </Field>
              </div>

              <label className="flex min-h-11 items-center gap-2 text-sm">
                <input
                  type="checkbox"
                  checked={published}
                  onChange={(e) => setPublished(e.target.checked)}
                  disabled={!canEdit}
                  className="size-4 rounded border-stone-300"
                />
                Published — unpublished events return 404 to guests
              </label>

              {dateChanged && (
                <p className="rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-900">
                  Saving will move every queued reminder for this event to the new date. Waves
                  that the new date puts in the past are dropped rather than sent late.
                </p>
              )}

              {/* The card is fixed artwork with the date and venue typed into it (design D2 of
                  add-event-invitation-card), so changing either here leaves the live card
                  stating the old one. Nothing can fix that automatically — the design has to
                  be re-made and re-published. */}
              {hasPublishedCard && (dateChanged || venueChanged) && (
                <p className="rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-900">
                  This event has a published invitation card with its date and venue printed on
                  it. Saving does not change the card — ask the designer for a new version and
                  publish it, or guests will read the old details off the artwork.
                </p>
              )}

              <ErrorNote error={error} />

              <p className="text-xs text-stone-500">
                Currently {formatDateTime(event.starts_at)}
              </p>
            </form>

            {result?.date_changed && (
              <div className="mt-4 space-y-2 rounded-lg bg-stone-50 p-3 text-sm">
                <p>
                  Moved {result.reminders_rescheduled} reminder(s), dropped{" "}
                  {result.reminders_cancelled} now-past wave(s), created{" "}
                  {result.reminders_created} new.
                </p>
                {result.broadcast_recommended && (
                  <div className="flex flex-wrap items-center gap-2">
                    <span className="text-stone-600">
                      Guests were told the old date. Announce the change?
                    </span>
                    <Button variant="secondary" onClick={() => void announce()}>
                      Email accepted guests
                    </Button>
                  </div>
                )}
                {broadcast && <p className="text-emerald-700">{broadcast}</p>}
              </div>
            )}
          </section>

          {/* Between Settings and Card design (design D9): everything above this point is
              staged behind the footer's Save, everything below it acts immediately. Placing
              the host after Card design would put a staged field below an immediate one and
              make that section's "Save above applies to the settings only" note false. */}
          <section className="border-t border-stone-100 pt-5">
            <h3 className="mb-1 text-sm font-semibold text-stone-900">Host</h3>
            <p className="mb-3 text-xs text-stone-500">
              Printed at the foot of the invitation under “Invited By”. Two names are joined
              by “And”.
            </p>
            <EventHostFields
              values={host}
              canEdit={canEdit}
              error={error}
              onChange={setHost}
            />
          </section>

          <section className="border-t border-stone-100 pt-5">
            <h3 className="mb-1 text-sm font-semibold text-stone-900">Card design</h3>
            {/* Uploading a file is a transfer and publishing is a state transition; neither
                can be staged behind the footer's Save, so this section acts on press. */}
            <p className="mb-3 text-xs text-stone-500">
              Uploading, publishing and deleting a design take effect immediately — Save above
              applies to the settings only.
            </p>
            <CardDesignPanel eventId={event.id} canEdit={canEdit} />
          </section>

          <section className="border-t border-stone-100 pt-5">
            <h3 className="mb-1 text-sm font-semibold text-stone-900">Invitation messages</h3>
            {/* Held in this component's state and written by the footer's Save, unlike the
                card design above it — these are plain fields, so they can be staged. */}
            <EventMessages
              values={messages}
              inherited={inheritedMessages}
              canEdit={canEdit}
              onChange={setMessages}
            />
          </section>
        </div>
      </Modal>

      {deleting && (
        <DeleteEventDialog
          event={event}
          onClose={() => setDeleting(false)}
          onDeleted={() => {
            setDeleting(false);
            // Straight past the dirty guard: the event is gone, so asking whether to discard
            // edits to it would be nonsense (design D8).
            onDeleted();
          }}
        />
      )}
    </>
  );
}

/**
 * Deleting an event destroys its guest list with it, so the confirmation states the cost in
 * numbers before it offers the button — and the numbers come from the API rather than from
 * whatever the screen happened to have loaded.
 */
function DeleteEventDialog({
  event,
  onClose,
  onDeleted,
}: {
  event: AdminEvent;
  onClose: () => void;
  onDeleted: () => void;
}) {
  const [impact, setImpact] = useState<{ guests: number; responses: number } | null>(null);
  const [error, setError] = useState<unknown>(null);
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    void api.eventDeleteImpact(event.id).then(setImpact).catch(setError);
  }, [event.id]);

  return (
    <Modal title={`Delete ${event.title_en}?`} onClose={onClose}>
      <div className="space-y-4">
        {impact === null ? (
          <Loading label="Checking what this removes…" />
        ) : (
          <p className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-900">
            This permanently removes <strong>{impact.guests}</strong> guest record(s) and{" "}
            <strong>{impact.responses}</strong> response(s) along with the event. Their
            invitation links stop working. It cannot be undone.
          </p>
        )}

        <ErrorNote error={error} />

        <div className="flex justify-end gap-2 border-t border-stone-100 pt-4">
          <Button type="button" variant="secondary" onClick={onClose}>
            Keep it
          </Button>
          <Button
            type="button"
            disabled={busy || impact === null}
            onClick={async () => {
              setBusy(true);
              try {
                await api.deleteEvent(event.id);
                onDeleted();
              } catch (err) {
                setError(err);
                setBusy(false);
              }
            }}
          >
            {busy ? "Deleting…" : "Delete event and its guests"}
          </Button>
        </div>
      </div>
    </Modal>
  );
}

/**
 * `datetime-local` speaks naive local time, the API speaks UTC ISO.
 *
 * The browser's local zone is whatever the laptop is set to, which for the couple is Dhaka
 * but for a host abroad is not. Both conversions go through the same Date object so the
 * round trip is lossless regardless.
 */
function toLocalInput(iso: string): string {
  const date = new Date(iso);
  const offset = date.getTimezoneOffset() * 60_000;
  return new Date(date.getTime() - offset).toISOString().slice(0, 16);
}

function fromLocalInput(value: string): string {
  return new Date(value).toISOString();
}
