"use client";

import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
import type { AttendeeStatus, CalendarEvent, Task } from "@slackwsh/contracts";
import { AppShell, useWorkspaceIdParam } from "../../components/AppShell";
import { EventModal, type EventModalSubmit } from "../../components/EventModal";
import { api, getCurrentUserId } from "../../lib/api";
import { isTextChannel, normalizeChannelRows, type ConversationRow } from "../../lib/conversations";
import {
  addDays,
  addMonths,
  formatTimeLabel,
  isSameMonth,
  monthGridRange,
  toLocalDateInput,
  weekRange,
} from "../../lib/datetime";
import { IconChevron, IconPlus, IconX } from "../../components/icons";

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

type ModalState = { mode: "create"; date: Date } | { mode: "edit"; event: CalendarEvent } | null;
type CalendarView = "month" | "week";

interface ExternalCalEvent {
  id: string;
  provider: "google_calendar" | "outlook_calendar" | "zoom";
  title: string;
  startsAt: string;
  endsAt: string;
  allDay: boolean;
  location: string | null;
  htmlLink: string | null;
}

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

/** Height reserved for the "+N more" row when a day overflows. */
const MORE_ROW_PX = 17;
/** Fallback chip height before the first real chip has been measured. */
const CHIP_PX = 21;

function CalendarView() {
  const workspaceId = useWorkspaceIdParam();
  const [view, setView] = useState<CalendarView>("month");
  const [anchor, setAnchor] = useState(() => new Date());
  const [events, setEvents] = useState<CalendarEvent[]>([]);
  const [externalEvents, setExternalEvents] = useState<ExternalCalEvent[]>([]);
  const [dueTasks, setDueTasks] = useState<Task[]>([]);
  const [zoomConnected, setZoomConnected] = useState(false);  const [loading, setLoading] = useState(true);
  const [members, setMembers] = useState<MemberRow[]>([]);
  const [channels, setChannels] = useState<ConversationRow[]>([]);
  const [myUserId, setMyUserId] = useState<number | null>(null);
  const [modal, setModal] = useState<ModalState>(null);
  const [expandedDay, setExpandedDay] = useState<string | null>(null);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);

  const range = useMemo(() => (view === "month" ? monthGridRange(anchor) : weekRange(anchor)), [view, anchor]);
  const days = useMemo(
    () => Array.from({ length: view === "month" ? 42 : 7 }, (_, i) => addDays(range.start, i)),
    [range.start, view],
  );
  const todayKey = toLocalDateInput(new Date());

  useEffect(() => {
    getCurrentUserId().then(setMyUserId);
  }, []);

  useEffect(() => {
    if (!workspaceId) return;
    api
      .members(workspaceId)
      .then((res) => setMembers(res.members as MemberRow[]))
      .catch(() => undefined);
    api
      .listChannels(workspaceId)
      .then((res) => setChannels(normalizeChannelRows(res.channels).filter((row) => isTextChannel(row.channel.type))))
      .catch(() => undefined);
  }, [workspaceId]);

  const load = useCallback(() => {
    if (!workspaceId) return;
    const from = range.start.toISOString();
    const to = range.end.toISOString();
    setLoading(true);
    Promise.all([
      api.listEvents(workspaceId, { from, to }).then((res) => res.events),
      // The task overlay: dueBefore/dueAfter have existed on listTasks since
      // the tasks feature shipped — this is their first caller.
      api
        .listTasks(workspaceId, { dueAfter: from, dueBefore: to })
        .then((res) => res.tasks)
        .catch(() => [] as Task[]),
      api
        .listExternalCalendarEvents({ from, to })
        .then((res) => res.events as ExternalCalEvent[])
        .catch(() => [] as ExternalCalEvent[]),
      api
        .listIntegrations()
        .then((res) => Boolean(res.integrations.find((row) => row.provider === "zoom" && row.connected)))
        .catch(() => false),
    ])
      .then(([nextEvents, nextTasks, nextExternal, zoom]) => {
        setEvents(nextEvents);
        setDueTasks(nextTasks);
        setExternalEvents(nextExternal);
        setZoomConnected(zoom);
      })
      .catch(() => {
        setEvents([]);
        setDueTasks([]);
        setExternalEvents([]);
      })
      .finally(() => setLoading(false));
  }, [workspaceId, range.start, range.end]);

  useEffect(() => {
    load();
  }, [load]);

  // Realtime: any calendar mutation refetches the visible range. The payload
  // carries the stored series row, so merging one occurrence into a window
  // the client may not be showing isn't possible — see RealtimeProvider.
  useEffect(() => {
    function onCalendarEvent(e: Event) {
      const detail = (e as CustomEvent).detail as { workspaceId?: number; event?: CalendarEvent } | undefined;
      const eventWorkspaceId = detail?.event?.workspaceId ?? detail?.workspaceId;
      if (!workspaceId || String(eventWorkspaceId) !== String(workspaceId)) return;
      load();
    }
    window.addEventListener("slackwsh:calendar", onCalendarEvent);
    window.addEventListener("slackwsh:task", onCalendarEvent);
    return () => {
      window.removeEventListener("slackwsh:calendar", onCalendarEvent);
      window.removeEventListener("slackwsh:task", onCalendarEvent);
    };
  }, [workspaceId, load]);

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

  // How many chips actually fit in a month cell. Measured rather than assumed:
  // the six rows share whatever height the viewport leaves, so a fixed cap
  // renders chips (and the "+N more" button that would reveal them) below the
  // cell's clip boundary on a short screen.
  const gridRef = useRef<HTMLDivElement | null>(null);
  const [chipFit, setChipFit] = useState({ plain: 3, withMore: 2 });

  useEffect(() => {
    if (view !== "month") return;
    const grid = gridRef.current;
    if (!grid) return;
    function measure() {
      const items = grid!.querySelector(".cal-cell-items") as HTMLElement | null;
      if (!items) return;
      const available = items.clientHeight;
      const chip = items.querySelector(".cal-chip") as HTMLElement | null;
      const chipHeight = chip ? chip.getBoundingClientRect().height + 3 : CHIP_PX + 3;
      setChipFit({
        plain: Math.max(1, Math.floor(available / chipHeight)),
        withMore: Math.max(1, Math.floor((available - MORE_ROW_PX) / chipHeight)),
      });
    }
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(grid);
    // ResizeObserver alone isn't enough: its callbacks are delivered on the
    // rendering path, so a background or non-compositing tab can miss the
    // resize entirely and keep a stale capacity. The window event is the
    // cheap backstop.
    window.addEventListener("resize", measure);
    return () => {
      observer.disconnect();
      window.removeEventListener("resize", measure);
    };
  }, [view, events, dueTasks, externalEvents]);

  const nameById = useMemo(() => new Map(members.map((row) => [String(row.user.id), row.user.name])), [members]);

  const eventsByDay = useMemo(() => {
    const map = new Map<string, CalendarEvent[]>();
    for (const event of events) {
      const key = toLocalDateInput(event.startsAt);
      const list = map.get(key) ?? [];
      list.push(event);
      map.set(key, list);
    }
    for (const list of map.values()) {
      // All-day first, then chronological — the ordering every calendar uses,
      // since an all-day event has no time to sort by.
      list.sort((a, b) => (a.allDay === b.allDay ? a.startsAt.localeCompare(b.startsAt) : a.allDay ? -1 : 1));
    }
    return map;
  }, [events]);

  const externalByDay = useMemo(() => {
    const map = new Map<string, ExternalCalEvent[]>();
    for (const event of externalEvents) {
      const key = toLocalDateInput(event.startsAt);
      const list = map.get(key) ?? [];
      list.push(event);
      map.set(key, list);
    }
    for (const list of map.values()) {
      list.sort((a, b) => (a.allDay === b.allDay ? a.startsAt.localeCompare(b.startsAt) : a.allDay ? -1 : 1));
    }
    return map;
  }, [externalEvents]);

  const tasksByDay = useMemo(() => {
    const map = new Map<string, Task[]>();
    for (const task of dueTasks) {
      if (!task.dueAt) continue;
      const key = toLocalDateInput(task.dueAt);
      const list = map.get(key) ?? [];
      list.push(task);
      map.set(key, list);
    }
    return map;
  }, [dueTasks]);

  const headline =
    view === "month"
      ? anchor.toLocaleDateString(undefined, { month: "long", year: "numeric" })
      : `${range.start.toLocaleDateString(undefined, { month: "short", day: "numeric" })} – ${addDays(range.end, -1).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}`;

  function step(direction: 1 | -1) {
    setAnchor((current) => (view === "month" ? addMonths(current, direction) : addDays(current, direction * 7)));
  }

  function openCreate(date: Date) {
    setSaveError(null);
    setExpandedDay(null);
    setModal({ mode: "create", date });
  }

  function openEdit(event: CalendarEvent) {
    setSaveError(null);
    setExpandedDay(null);
    setModal({ mode: "edit", event });
  }

  async function handleSubmit(values: EventModalSubmit) {
    if (!workspaceId) return;
    setSaving(true);
    setSaveError(null);
    try {
      if (modal?.mode === "create") {
        await api.createEvent(workspaceId, {
          title: values.title,
          description: values.description || null,
          location: values.location || null,
          startsAt: values.startsAt,
          endsAt: values.endsAt,
          allDay: values.allDay,
          timezone: values.timezone,
          recurrenceRule: values.recurrenceRule,
          channelId: values.channelId,
          attendeeUserIds: values.attendeeUserIds,
        });
      } else if (modal?.mode === "edit") {
        const original = modal.event;
        // Occurrence scope addresses the series row plus the date; series
        // scope rewrites the row itself. An override is already its own row,
        // so it is edited directly.
        const targetId = values.scope === "occurrence" ? String(original.seriesId) : String(original.id);
        await api.updateEvent(workspaceId, targetId, {
          scope: values.scope,
          occurrenceDate: values.scope === "occurrence" ? original.occurrenceDate : undefined,
          title: values.title,
          description: values.description || null,
          location: values.location || null,
          startsAt: values.startsAt,
          endsAt: values.endsAt,
          allDay: values.allDay,
          timezone: values.timezone,
          // The rule lives on the series; sending it with occurrence scope is
          // rejected by the contract.
          recurrenceRule: values.scope === "series" ? values.recurrenceRule : undefined,
          channelId: values.channelId,
        });
        const before = new Set(original.attendees.map((a) => String(a.userId)));
        const after = new Set(values.attendeeUserIds);
        const changed = before.size !== after.size || [...after].some((id) => !before.has(id));
        if (changed) {
          await api.inviteToEvent(workspaceId, String(original.id), values.attendeeUserIds);
        }
      }
      setModal(null);
      load();
    } catch (err) {
      setSaveError(err instanceof Error ? err.message : "Couldn't save this event.");
    } finally {
      setSaving(false);
    }
  }

  async function handleDelete(scope: "series" | "occurrence") {
    if (!workspaceId || modal?.mode !== "edit") return;
    const target = modal.event;
    const label =
      scope === "occurrence"
        ? `Delete the ${target.occurrenceDate} occurrence of “${target.title}”?`
        : `Delete “${target.title}”${target.recurrenceRule ? " and every occurrence" : ""}? This can't be undone.`;
    if (!window.confirm(label)) return;
    setSaving(true);
    setSaveError(null);
    try {
      await api.deleteEvent(workspaceId, String(scope === "occurrence" ? target.seriesId : target.id), {
        scope,
        occurrenceDate: scope === "occurrence" ? target.occurrenceDate : undefined,
      });
      setModal(null);
      load();
    } catch (err) {
      setSaveError(err instanceof Error ? err.message : "Couldn't delete this event.");
    } finally {
      setSaving(false);
    }
  }

  async function handleRsvp(status: AttendeeStatus) {
    if (!workspaceId || modal?.mode !== "edit") return;
    setSaving(true);
    setSaveError(null);
    try {
      const updated = await api.rsvpEvent(workspaceId, String(modal.event.id), status);
      // Keep the open dialog in step with the answer, preserving the
      // occurrence identity the modal was opened with.
      setModal({ mode: "edit", event: { ...modal.event, attendees: updated.attendees } });
      load();
    } catch (err) {
      setSaveError(err instanceof Error ? err.message : "Couldn't save your response.");
    } finally {
      setSaving(false);
    }
  }

  function renderEventChip(event: CalendarEvent, compact: boolean) {
    const myStatus = event.attendees.find((a) => a.userId === myUserId)?.status;
    return (
      <button
        key={`${event.id}-${event.occurrenceDate}`}
        type="button"
        className={`cal-chip${event.allDay ? " all-day" : ""}${myStatus === "declined" ? " declined" : ""}`}
        title={`${event.title}${event.location ? ` · ${event.location}` : ""}`}
        onClick={(e) => {
          e.stopPropagation();
          openEdit(event);
        }}
      >
        {!event.allDay && <span className="cal-chip-time">{formatTimeLabel(event.startsAt)}</span>}
        <span className="cal-chip-title">{event.title}</span>
        {!compact && (event.recurrenceRule || event.isOverride) && (
          <span className="cal-chip-repeat" title="Repeating event">
            ↻
          </span>
        )}
      </button>
    );
  }

  function renderExternalChip(event: ExternalCalEvent) {
    const label = event.provider === "google_calendar" ? "Google" : event.provider === "outlook_calendar" ? "Outlook" : "Zoom";
    const body = (
      <>
        {!event.allDay && <span className="cal-chip-time">{formatTimeLabel(event.startsAt)}</span>}
        <span className="cal-chip-title">{event.title}</span>
        <span className="cal-chip-mark external" title={label}>
          {label[0]}
        </span>
      </>
    );
    if (event.htmlLink) {
      return (
        <a
          key={event.id}
          className="cal-chip external"
          href={event.htmlLink}
          target="_blank"
          rel="noreferrer"
          title={`${event.title} · ${label}`}
          onClick={(e) => e.stopPropagation()}
        >
          {body}
        </a>
      );
    }
    return (
      <span key={event.id} className="cal-chip external" title={`${event.title} · ${label}`}>
        {body}
      </span>
    );
  }

  function renderTaskChip(task: Task) {
    return (
      <Link
        key={`task-${task.id}`}
        href={`/tasks?workspaceId=${workspaceId}`}
        className={task.status === "done" ? "cal-chip task done" : "cal-chip task"}
        title={`Task due${task.assigneeUserId != null ? ` · ${nameById.get(String(task.assigneeUserId)) ?? "assigned"}` : ""}`}
        onClick={(e) => e.stopPropagation()}
      >
        <span className="cal-chip-mark" aria-hidden="true">
          ✓
        </span>
        <span className="cal-chip-title">{task.title}</span>
      </Link>
    );
  }

  const expandedEvents = expandedDay ? (eventsByDay.get(expandedDay) ?? []) : [];
  const expandedExternal = expandedDay ? (externalByDay.get(expandedDay) ?? []) : [];
  const expandedTasks = expandedDay ? (tasksByDay.get(expandedDay) ?? []) : [];

  return (
    <AppShell
      active="calendar"
      title="Calendar"
      subtitle={loading ? "Loading…" : headline}
      actions={
        <div className="cal-head-actions">
          <div className="cal-viewswitch" role="group" aria-label="Calendar view">
            <button
              type="button"
              className={view === "month" ? "active" : undefined}
              aria-pressed={view === "month"}
              onClick={() => setView("month")}
            >
              Month
            </button>
            <button
              type="button"
              className={view === "week" ? "active" : undefined}
              aria-pressed={view === "week"}
              onClick={() => setView("week")}
            >
              Week
            </button>
          </div>
          <div className="cal-nav" role="group" aria-label={view === "month" ? "Change month" : "Change week"}>
            <button
              type="button"
              className="cal-nav-btn"
              aria-label={view === "month" ? "Previous month" : "Previous week"}
              onClick={() => step(-1)}
            >
              <span className="cal-nav-prev">
                <IconChevron />
              </span>
            </button>
            <button type="button" className="cal-nav-today" onClick={() => setAnchor(new Date())}>
              Today
            </button>
            <button
              type="button"
              className="cal-nav-btn"
              aria-label={view === "month" ? "Next month" : "Next week"}
              onClick={() => step(1)}
            >
              <IconChevron />
            </button>
          </div>
          <button className="head-action primary" type="button" onClick={() => openCreate(new Date())}>
            <IconPlus size={14} />
            <span>New event</span>
          </button>
        </div>
      }
    >
      <div className={view === "month" ? "cal-body month" : "cal-body"}>
        <div className="cal-grid-head" aria-hidden="true">
          {WEEKDAY_HEADERS.map((label) => (
            <span key={label}>{label}</span>
          ))}
        </div>

        <div className={view === "month" ? "cal-grid month" : "cal-grid week"} ref={gridRef}>
          {days.map((day) => {
            const key = toLocalDateInput(day);
            const dayEvents = eventsByDay.get(key) ?? [];
            const dayExternal = externalByDay.get(key) ?? [];
            const dayTasks = tasksByDay.get(key) ?? [];
            const isToday = key === todayKey;
            const outside = view === "month" && !isSameMonth(day, anchor);
            const items = [...dayEvents, ...dayExternal, ...dayTasks];
            // When a day overflows, one row has to go to the "+N more" button
            // itself — otherwise the button is the thing that gets clipped.
            const cap =
              view === "week" ? items.length : items.length > chipFit.plain ? chipFit.withMore : chipFit.plain;
            const hidden = Math.max(0, items.length - cap);
            const shownEvents = dayEvents.slice(0, cap);
            const shownExternal = dayExternal.slice(0, Math.max(0, cap - shownEvents.length));
            const shownTasks = dayTasks.slice(0, Math.max(0, cap - shownEvents.length - shownExternal.length));

            return (
              <div
                key={key}
                className={`cal-cell${isToday ? " today" : ""}${outside ? " outside" : ""}`}
                onClick={() => openCreate(day)}
                role="gridcell"
                tabIndex={-1}
              >
                <div className="cal-cell-head">
                  <span className="cal-cell-num">{day.getDate()}</span>
                  {view === "week" && <span className="cal-cell-dow">{WEEKDAY_HEADERS[(day.getDay() + 6) % 7]}</span>}
                </div>

                <div className="cal-cell-items">
                  {shownEvents.map((event) => renderEventChip(event, view === "month"))}
                  {shownExternal.map(renderExternalChip)}
                  {shownTasks.map(renderTaskChip)}
                  {hidden > 0 && (
                    <button
                      type="button"
                      className="cal-more"
                      onClick={(e) => {
                        e.stopPropagation();
                        setExpandedDay(key);
                      }}
                    >
                      +{hidden} more
                    </button>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {expandedDay && (
        <div className="channel-invite-backdrop" role="presentation" onClick={() => setExpandedDay(null)}>
          <div
            className="cal-day-dialog"
            role="dialog"
            aria-modal="true"
            aria-label={new Date(`${expandedDay}T12:00:00`).toDateString()}
            onClick={(e) => e.stopPropagation()}
          >
            <div className="channel-invite-head">
              <div>
                <h2>
                  {new Date(`${expandedDay}T12:00:00`).toLocaleDateString(undefined, {
                    weekday: "long",
                    month: "long",
                    day: "numeric",
                  })}
                </h2>
                <p>
                  {expandedEvents.length + expandedExternal.length} event
                  {expandedEvents.length + expandedExternal.length === 1 ? "" : "s"}
                  {expandedTasks.length > 0 ? ` · ${expandedTasks.length} task${expandedTasks.length === 1 ? "" : "s"} due` : ""}
                </p>
              </div>
              <button className="thread-close" type="button" onClick={() => setExpandedDay(null)} aria-label="Close">
                <IconX />
              </button>
            </div>
            <div className="cal-day-dialog-body">
              {expandedEvents.map((event) => renderEventChip(event, false))}
              {expandedExternal.map(renderExternalChip)}
              {expandedTasks.map(renderTaskChip)}
            </div>
            <div className="channel-invite-foot">
              <button
                className="screen-btn primary"
                type="button"
                onClick={() => openCreate(new Date(`${expandedDay}T12:00:00`))}
              >
                Add event
              </button>
            </div>
          </div>
        </div>
      )}

      {modal && (
        <EventModal
          mode={modal.mode}
          event={modal.mode === "edit" ? modal.event : null}
          initialDate={modal.mode === "create" ? modal.date : undefined}
          members={members}
          channels={channels}
          myUserId={myUserId}
          zoomConnected={zoomConnected}
          busy={saving}
          error={saveError}
          onClose={() => setModal(null)}
          onSubmit={handleSubmit}
          onDelete={modal.mode === "edit" ? handleDelete : undefined}
          onRsvp={modal.mode === "edit" ? handleRsvp : undefined}
        />
      )}
    </AppShell>
  );
}

export default function CalendarPage() {
  return (
    <Suspense fallback={<main className="page"><div className="empty-state">Loading calendar…</div></main>}>
      <CalendarView />
    </Suspense>
  );
}
