"use client";

import { useEffect, useMemo, useState } from "react";
import type { AttendeeStatus, CalendarEvent, EventAttendee } from "@slackwsh/contracts";
import { describeRecurrence, parseRecurrenceRule, WEEKDAY_CODES } from "@slackwsh/core";
import { api } from "../lib/api";
import { avatarColor, initials } from "../lib/avatar";
import type { ConversationRow } from "../lib/conversations";
import { fromLocalDateTime, localTimeZone, toLocalDateInput, toLocalTimeInput } from "../lib/datetime";
import { IconX } from "./icons";

export interface EventModalMember {
  user: { id: string | number; name: string; email: string };
}

/** The repeat presets the modal offers. Anything more exotic than these still
 * round-trips untouched — an unrecognised stored rule shows as "Custom" and is
 * left alone unless the user picks a different preset. */
export type RepeatPreset = "none" | "daily" | "weekly" | "weekdays" | "monthly" | "custom";

export interface EventModalValues {
  title: string;
  description: string;
  location: string;
  /** Local calendar date, YYYY-MM-DD. */
  startDate: string;
  startTime: string;
  endDate: string;
  endTime: string;
  allDay: boolean;
  repeat: RepeatPreset;
  /** "never" | "on" | "after" */
  endsMode: "never" | "on" | "after";
  endsOnDate: string;
  endsAfterCount: number;
  channelId: string | null;
  attendeeUserIds: string[];
}

export interface EventModalSubmit extends EventModalValues {
  /** Serialised rule for the API, or null when it doesn't repeat. */
  recurrenceRule: string | null;
  startsAt: string;
  endsAt: string;
  timezone: string;
  scope: "series" | "occurrence";
}

const WEEKDAY_LABEL = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

function presetFromRule(rule: string | null): RepeatPreset {
  const parsed = parseRecurrenceRule(rule);
  if (!parsed) return "none";
  if (parsed.interval !== 1 || parsed.count) return "custom";
  if (parsed.freq === "daily") return "daily";
  if (parsed.freq === "monthly") return "monthly";
  if (parsed.freq === "weekly") {
    const days = parsed.byDay ?? [];
    if (days.length === 5 && ["MO", "TU", "WE", "TH", "FR"].every((d) => days.includes(d as never))) return "weekdays";
    if (days.length <= 1) return "weekly";
  }
  return "custom";
}

/** Builds the stored rule from the preset + "ends" controls. Weekly presets
 * pin BYDAY to the event's own start weekday so the series can't drift when
 * the start date is changed in the same edit. */
function ruleFromValues(values: EventModalValues, existingRule: string | null): string | null {
  if (values.repeat === "none") return null;
  if (values.repeat === "custom") return existingRule;

  const startWeekday = WEEKDAY_CODES[new Date(`${values.startDate}T12:00:00`).getDay()]!;
  const parts: string[] = [];
  if (values.repeat === "daily") parts.push("FREQ=DAILY");
  else if (values.repeat === "monthly") parts.push("FREQ=MONTHLY");
  else if (values.repeat === "weekdays") parts.push("FREQ=WEEKLY", "BYDAY=MO,TU,WE,TH,FR");
  else parts.push("FREQ=WEEKLY", `BYDAY=${startWeekday}`);

  if (values.endsMode === "after" && values.endsAfterCount > 0) parts.push(`COUNT=${values.endsAfterCount}`);
  if (values.endsMode === "on" && values.endsOnDate) {
    parts.push(`UNTIL=${new Date(`${values.endsOnDate}T23:59:59`).toISOString()}`);
  }
  return parts.join(";");
}

const RSVP_CHOICES: Array<{ status: AttendeeStatus; label: string }> = [
  { status: "going", label: "Going" },
  { status: "maybe", label: "Maybe" },
  { status: "declined", label: "Can't make it" },
];

/** Plain-language length of the event, shown under the when-row. Cheap
 * feedback that catches the classic "meeting ends before it starts" and the
 * accidental 25-hour booking without a validation error. */
function durationLabel(values: EventModalValues): string {
  if (values.allDay) {
    const days =
      Math.round(
        (new Date(`${values.endDate}T12:00:00`).getTime() - new Date(`${values.startDate}T12:00:00`).getTime()) /
          86_400_000,
      ) + 1;
    return days <= 1 ? "All day" : `${days} days`;
  }
  const start = new Date(`${values.startDate}T${values.startTime || "00:00"}`);
  const end = new Date(`${values.endDate}T${values.endTime || "00:00"}`);
  const minutes = Math.round((end.getTime() - start.getTime()) / 60_000);
  if (Number.isNaN(minutes)) return "";
  if (minutes <= 0) return "Ends before it starts";
  if (minutes < 60) return `${minutes} min`;
  const hours = Math.floor(minutes / 60);
  const rest = minutes % 60;
  if (hours >= 24) {
    const days = Math.floor(hours / 24);
    const spareHours = hours % 24;
    return spareHours ? `${days}d ${spareHours}h` : `${days} day${days > 1 ? "s" : ""}`;
  }
  return rest ? `${hours} hr ${rest} min` : `${hours} hr`;
}

/**
 * Create/edit Calendar event dialog. Shares the task modal's backdrop/dialog
 * shell, with the two things a calendar needs that a task doesn't: a
 * start/end pair with an all-day toggle, and recurrence — which is also why
 * this dialog has to ask about *scope*. Editing one occurrence of a series is
 * a different write from editing the series, so when the event repeats the
 * footer offers the choice explicitly rather than guessing.
 */
export function EventModal({
  mode,
  event,
  initialDate,
  members,
  channels,
  myUserId,
  zoomConnected,
  busy,
  error,
  onClose,
  onSubmit,
  onDelete,
  onRsvp,
}: {
  mode: "create" | "edit";
  /** The occurrence being edited (edit mode only). */
  event?: CalendarEvent | null;
  /** Day the user clicked, prefilled in create mode. */
  initialDate?: Date;
  members: EventModalMember[];
  channels: ConversationRow[];
  myUserId: number | null;
  /** When true, show “Add Zoom meeting” for the location field. */
  zoomConnected?: boolean;
  busy?: boolean;
  error?: string | null;
  onClose: () => void;
  onSubmit: (values: EventModalSubmit) => void;
  onDelete?: (scope: "series" | "occurrence") => void;
  onRsvp?: (status: AttendeeStatus) => void;
}) {
  const isRecurring = Boolean(event?.recurrenceRule) || Boolean(event?.isOverride);
  const [zoomBusy, setZoomBusy] = useState(false);
  const [zoomError, setZoomError] = useState<string | null>(null);

  const [values, setValues] = useState<EventModalValues>(() => {
    if (event) {
      return {
        title: event.title,
        description: event.description ?? "",
        location: event.location ?? "",
        startDate: toLocalDateInput(event.startsAt),
        startTime: toLocalTimeInput(event.startsAt),
        endDate: toLocalDateInput(event.endsAt),
        endTime: toLocalTimeInput(event.endsAt),
        allDay: event.allDay,
        repeat: presetFromRule(event.recurrenceRule),
        endsMode: parseRecurrenceRule(event.recurrenceRule)?.count
          ? "after"
          : parseRecurrenceRule(event.recurrenceRule)?.until
            ? "on"
            : "never",
        endsOnDate: parseRecurrenceRule(event.recurrenceRule)?.until?.slice(0, 10) ?? "",
        endsAfterCount: parseRecurrenceRule(event.recurrenceRule)?.count ?? 10,
        channelId: event.channelId != null ? String(event.channelId) : null,
        attendeeUserIds: event.attendees.map((a) => String(a.userId)),
      };
    }
    const base = initialDate ?? new Date();
    // Default to the next full hour, 30 minutes long — the same courtesy
    // every calendar extends rather than dropping you at 00:00.
    const start = new Date(base);
    start.setMinutes(0, 0, 0);
    start.setHours(base.getHours() + 1);
    const end = new Date(start.getTime() + 30 * 60 * 1000);
    return {
      title: "",
      description: "",
      location: "",
      startDate: toLocalDateInput(start),
      startTime: toLocalTimeInput(start),
      endDate: toLocalDateInput(end),
      endTime: toLocalTimeInput(end),
      allDay: false,
      repeat: "none",
      endsMode: "never",
      endsOnDate: "",
      endsAfterCount: 10,
      channelId: null,
      attendeeUserIds: myUserId != null ? [String(myUserId)] : [],
    };
  });

  // Series vs this-occurrence. Only meaningful for a repeating event; a
  // one-off always writes its own row.
  const [scope, setScope] = useState<"series" | "occurrence">("series");
  // Collapsed by default — the member list is only needed while changing the
  // invite list, and open it took over the dialog.
  const [guestPickerOpen, setGuestPickerOpen] = useState(false);

  useEffect(() => {
    function onKeyDown(e: KeyboardEvent) {
      if (e.key === "Escape") onClose();
    }
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [onClose]);

  const sortedMembers = useMemo(
    () => [...members].sort((a, b) => a.user.name.localeCompare(b.user.name)),
    [members],
  );
  const attendeeSet = useMemo(() => new Set(values.attendeeUserIds), [values.attendeeUserIds]);
  const myAttendance: EventAttendee | undefined = event?.attendees.find(
    (a) => myUserId != null && a.userId === myUserId,
  );

  const repeatLabel = describeRecurrence(parseRecurrenceRule(ruleFromValues(values, event?.recurrenceRule ?? null)));
  const duration = durationLabel(values);
  const invalidRange = duration === "Ends before it starts";

  function set<K extends keyof EventModalValues>(key: K, value: EventModalValues[K]) {
    setValues((v) => {
      const next = { ...v, [key]: value };
      // Keep the end from landing before the start: nudge the end date along
      // with the start, which is what a user means when they move a meeting.
      if (key === "startDate" && v.endDate < String(value)) next.endDate = String(value);
      if (key === "startTime" && next.startDate === next.endDate && next.endTime <= String(value)) {
        const [h, m] = String(value).split(":").map(Number);
        const bumped = new Date(2000, 0, 1, h ?? 0, (m ?? 0) + 30);
        next.endTime = toLocalTimeInput(bumped);
      }
      return next;
    });
  }

  async function addZoomMeeting() {
    if (zoomBusy || busy) return;
    setZoomBusy(true);
    setZoomError(null);
    try {
      const allDay = values.allDay;
      const startsAt = fromLocalDateTime(values.startDate, allDay ? "09:00" : values.startTime);
      const endsAt = allDay
        ? fromLocalDateTime(values.endDate, "10:00")
        : fromLocalDateTime(values.endDate, values.endTime);
      const durationMinutes = Math.max(15, Math.round((new Date(endsAt).getTime() - new Date(startsAt).getTime()) / 60_000));
      const meeting = await api.createZoomMeeting({
        topic: values.title.trim() || "Meeting",
        startsAt,
        durationMinutes,
        timezone: localTimeZone(),
      });
      set("location", meeting.joinUrl);
    } catch (err) {
      setZoomError(err instanceof Error ? err.message : "Could not create Zoom meeting");
    } finally {
      setZoomBusy(false);
    }
  }

  function submit() {
    if (!values.title.trim() || busy) return;
    const allDay = values.allDay;
    const startsAt = fromLocalDateTime(values.startDate, allDay ? "00:00" : values.startTime);
    // An all-day event covers the whole of its last day, so the exclusive end
    // is the following midnight.
    const endsAt = allDay
      ? fromLocalDateTime(values.endDate, "23:59")
      : fromLocalDateTime(values.endDate, values.endTime);
    onSubmit({
      ...values,
      title: values.title.trim(),
      startsAt,
      endsAt,
      timezone: localTimeZone(),
      recurrenceRule: ruleFromValues(values, event?.recurrenceRule ?? null),
      scope: isRecurring ? scope : "series",
    });
  }

  return (
    <div className="channel-invite-backdrop" role="presentation" onClick={onClose}>
      <div
        className="task-modal event-modal"
        role="dialog"
        aria-modal="true"
        aria-labelledby="event-modal-title"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="channel-invite-head">
          <div>
            <h2 id="event-modal-title">{mode === "create" ? "New event" : "Edit event"}</h2>
            <p>
              {mode === "create"
                ? "Invite whoever needs to be there — they'll get a notification."
                : isRecurring
                  ? // Not toLowerCase()'d: the label carries weekday
                    // abbreviations ("Weekly on Thu") that must keep their case.
                    `Repeats: ${repeatLabel}`
                  : "Update the details below."}
            </p>
          </div>
          <button className="thread-close" type="button" onClick={onClose} aria-label="Close">
            <IconX />
          </button>
        </div>

        <div className="task-modal-body event-modal-body">
          {/* Title carries the dialog: borderless and oversized so the eye
              starts there rather than on a row of identical labelled fields. */}
          <input
            className="event-modal-title"
            type="text"
            value={values.title}
            maxLength={200}
            autoFocus
            onChange={(e) => set("title", e.target.value)}
            placeholder="Add a title"
            aria-label="Event title"
          />

          {myAttendance && onRsvp && (
            <div className="event-modal-rsvp">
              <span className="event-modal-rsvp-label">Going?</span>
              <div className="event-rsvp-row">
                {RSVP_CHOICES.map((choice) => (
                  <button
                    key={choice.status}
                    type="button"
                    className={
                      myAttendance.status === choice.status ? `event-rsvp ${choice.status} active` : "event-rsvp"
                    }
                    disabled={busy}
                    onClick={() => onRsvp(choice.status)}
                  >
                    {choice.label}
                  </button>
                ))}
              </div>
            </div>
          )}

          {/* When: one chronological left-to-right row (date → time → time →
              date) instead of separate "Starts"/"Ends" columns, which made the
              eye zigzag and squeezed four controls into half the width each. */}
          <section className="event-modal-group">
            <div className="event-modal-group-head">
              <span className="event-modal-group-label">When</span>
              <label className="event-modal-toggle">
                <input type="checkbox" checked={values.allDay} onChange={(e) => set("allDay", e.target.checked)} />
                <span>All day</span>
              </label>
            </div>

            <div className="event-modal-whenrow">
              <input
                type="date"
                className="event-modal-date"
                value={values.startDate}
                aria-label="Start date"
                onChange={(e) => set("startDate", e.target.value)}
              />
              {!values.allDay && (
                <input
                  type="time"
                  className="event-modal-time"
                  value={values.startTime}
                  aria-label="Start time"
                  onChange={(e) => set("startTime", e.target.value)}
                />
              )}
              <span className="event-modal-arrow" aria-hidden="true">
                →
              </span>
              {!values.allDay && (
                <input
                  type="time"
                  className="event-modal-time"
                  value={values.endTime}
                  aria-label="End time"
                  onChange={(e) => set("endTime", e.target.value)}
                />
              )}
              <input
                type="date"
                className="event-modal-date"
                value={values.endDate}
                min={values.startDate}
                aria-label="End date"
                onChange={(e) => set("endDate", e.target.value)}
              />
            </div>

            <div className={invalidRange ? "event-modal-hint invalid" : "event-modal-hint"}>{duration}</div>

            <div className="event-modal-repeatrow">
              <select
                className="event-modal-repeat"
                value={values.repeat}
                aria-label="Repeats"
                onChange={(e) => set("repeat", e.target.value as RepeatPreset)}
              >
                <option value="none">Does not repeat</option>
                <option value="daily">Daily</option>
                <option value="weekly">
                  Weekly on {WEEKDAY_LABEL[new Date(`${values.startDate}T12:00:00`).getDay()]}
                </option>
                <option value="weekdays">Every weekday (Mon–Fri)</option>
                <option value="monthly">Monthly</option>
                {values.repeat === "custom" && <option value="custom">Custom (kept as-is)</option>}
              </select>

              {values.repeat !== "none" && (
                <>
                  <select
                    className="event-modal-ends"
                    value={values.endsMode}
                    aria-label="Repeat ends"
                    onChange={(e) => set("endsMode", e.target.value as EventModalValues["endsMode"])}
                  >
                    <option value="never">Forever</option>
                    <option value="on">Until</option>
                    <option value="after">For</option>
                  </select>
                  {values.endsMode === "on" && (
                    <input
                      type="date"
                      className="event-modal-date"
                      value={values.endsOnDate}
                      min={values.startDate}
                      aria-label="Repeat until"
                      onChange={(e) => set("endsOnDate", e.target.value)}
                    />
                  )}
                  {values.endsMode === "after" && (
                    <span className="event-modal-count">
                      <input
                        type="number"
                        min={1}
                        max={400}
                        value={values.endsAfterCount}
                        aria-label="Number of occurrences"
                        onChange={(e) => set("endsAfterCount", Number(e.target.value))}
                      />
                      <span>times</span>
                    </span>
                  )}
                </>
              )}
            </div>

            {values.repeat !== "none" && <div className="event-modal-hint">{repeatLabel}</div>}
          </section>

          {/* Guests: avatar chips plus an expand-on-demand picker. The old
              always-open member list was a permanent scroll box that pushed
              everything below it out of view. */}
          <section className="event-modal-group">
            <div className="event-modal-group-head">
              <span className="event-modal-group-label">
                Guests{values.attendeeUserIds.length > 0 ? ` · ${values.attendeeUserIds.length}` : ""}
              </span>
              <button
                type="button"
                className="event-modal-link"
                aria-expanded={guestPickerOpen}
                onClick={() => setGuestPickerOpen((open) => !open)}
              >
                {guestPickerOpen ? "Done" : "Add guests"}
              </button>
            </div>

            {values.attendeeUserIds.length === 0 && !guestPickerOpen && (
              <p className="event-modal-hint">No one invited yet.</p>
            )}

            {values.attendeeUserIds.length > 0 && (
              <div className="event-modal-chips">
                {values.attendeeUserIds.map((id) => {
                  const member = members.find((row) => String(row.user.id) === id);
                  const color = avatarColor(id);
                  const rsvp = event?.attendees.find((a) => String(a.userId) === id);
                  const name = member?.user.name ?? `User ${id}`;
                  return (
                    <span key={id} className={rsvp ? `event-chip ${rsvp.status}` : "event-chip"}>
                      <span className="event-chip-avatar" style={{ background: color.bg, color: color.fg }}>
                        {initials(name)}
                      </span>
                      <span className="event-chip-name">{name.split(" ")[0]}</span>
                      {rsvp && rsvp.status !== "needs_action" && (
                        <span className="event-chip-rsvp" aria-hidden="true">
                          {rsvp.status === "going" ? "✓" : rsvp.status === "maybe" ? "?" : "✕"}
                        </span>
                      )}
                      <button
                        type="button"
                        className="event-chip-remove"
                        aria-label={`Remove ${name}`}
                        onClick={() => set("attendeeUserIds", values.attendeeUserIds.filter((x) => x !== id))}
                      >
                        <IconX size={11} />
                      </button>
                    </span>
                  );
                })}
              </div>
            )}

            {guestPickerOpen && (
              <div className="task-modal-assignees">
                {sortedMembers.map((row) => {
                  const id = String(row.user.id);
                  const selected = attendeeSet.has(id);
                  const color = avatarColor(id);
                  return (
                    <button
                      key={id}
                      type="button"
                      className={selected ? "channel-invite-row selected" : "channel-invite-row"}
                      onClick={() =>
                        set(
                          "attendeeUserIds",
                          selected
                            ? values.attendeeUserIds.filter((existing) => existing !== id)
                            : [...values.attendeeUserIds, id],
                        )
                      }
                    >
                      <span className="list-row-avatar" style={{ background: color.bg, color: color.fg }}>
                        {initials(row.user.name)}
                      </span>
                      <span className="channel-invite-meta">
                        <strong>{row.user.name}</strong>
                        <span>{row.user.email}</span>
                      </span>
                      <span className="channel-invite-check" aria-hidden="true">
                        {selected ? "✓" : ""}
                      </span>
                    </button>
                  );
                })}
              </div>
            )}
          </section>

          <section className="event-modal-group">
            <div className="event-modal-group-head">
              <span className="event-modal-group-label">Details</span>
            </div>
            <div className="event-modal-detailrow">
              <input
                type="text"
                value={values.location}
                maxLength={300}
                onChange={(e) => set("location", e.target.value)}
                placeholder="Add a room, link, or address"
                aria-label="Location"
              />
              <select
                value={values.channelId ?? ""}
                aria-label="Channel"
                onChange={(e) => set("channelId", e.target.value || null)}
              >
                <option value="">No channel</option>
                {channels.map((row) => (
                  <option key={row.channel.id} value={row.channel.id}>
                    #{row.channel.name ?? "untitled"}
                  </option>
                ))}
              </select>
            </div>
            {zoomConnected && (
              <div className="event-modal-zoom">
                <button className="screen-btn" type="button" disabled={busy || zoomBusy} onClick={() => void addZoomMeeting()}>
                  {zoomBusy ? "Creating Zoom…" : "Add Zoom meeting"}
                </button>
                {zoomError && <span className="error-text">{zoomError}</span>}
              </div>
            )}
            <textarea
              value={values.description}
              maxLength={10_000}
              rows={2}
              onChange={(e) => set("description", e.target.value)}
              placeholder="Agenda or notes (optional)"
              aria-label="Description"
            />
          </section>

          {error && <p className="error-text">{error}</p>}
        </div>

        {/* The scope choice sits immediately above the footer: it is a
            property of the save you're about to make, not of the event, so it
            belongs at the decision point rather than buried mid-form. */}
        {isRecurring && (
          <fieldset className="event-modal-scope">
            <legend>Apply changes to</legend>
            <label>
              <input
                type="radio"
                name="event-scope"
                checked={scope === "occurrence"}
                onChange={() => setScope("occurrence")}
              />
              <span>This event{event?.occurrenceDate ? ` · ${event.occurrenceDate}` : ""}</span>
            </label>
            <label>
              <input type="radio" name="event-scope" checked={scope === "series"} onChange={() => setScope("series")} />
              <span>The whole series</span>
            </label>
          </fieldset>
        )}

        <div className="channel-invite-foot">
          {mode === "edit" && onDelete && (
            <button
              className="screen-btn danger task-modal-delete"
              type="button"
              disabled={busy}
              onClick={() => onDelete(isRecurring ? scope : "series")}
            >
              {isRecurring && scope === "occurrence" ? "Delete this one" : "Delete"}
            </button>
          )}
          <button className="screen-btn" type="button" onClick={onClose}>
            Cancel
          </button>
          <button
            className="screen-btn primary"
            type="button"
            disabled={busy || !values.title.trim() || invalidRange}
            onClick={submit}
          >
            {busy ? "Saving…" : mode === "create" ? "Create event" : "Save changes"}
          </button>
        </div>
      </div>
    </div>
  );
}
