import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useAtom } from 'jotai';
import { IContent } from 'matrix-js-sdk';
import { Box, Button, Icon, IconButton, Icons, Text, color, config } from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { scheduledMessagesAtom, ScheduledMessage } from '../../state/scheduledMessages';
import { cancelScheduledMessage, sendScheduledMessageNow } from '../../utils/scheduledMessages';
import { ScheduleMessageModal } from './ScheduleMessageModal';

interface ScheduledMessagesTrayProps {
  roomId: string;
}

function formatSendAt(sendAt: number): string {
  const date = new Date(sendAt);
  const now = new Date();
  const isToday =
    date.getFullYear() === now.getFullYear() &&
    date.getMonth() === now.getMonth() &&
    date.getDate() === now.getDate();
  const tomorrow = new Date(now);
  tomorrow.setDate(tomorrow.getDate() + 1);
  const isTomorrow =
    date.getFullYear() === tomorrow.getFullYear() &&
    date.getMonth() === tomorrow.getMonth() &&
    date.getDate() === tomorrow.getDate();
  const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
  if (isToday) return `Today ${timeStr}`;
  if (isTomorrow) return `Tomorrow ${timeStr}`;
  return `${date.toLocaleDateString()} ${timeStr}`;
}

export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) {
  const mx = useMatrixClient();
  const [scheduledMessages, setScheduledMessages] = useAtom(scheduledMessagesAtom);
  const [expanded, setExpanded] = useState(false);
  const [cancelling, setCancelling] = useState<Set<string>>(new Set());
  const [cancelErrors, setCancelErrors] = useState<Set<string>>(new Set());
  const [sendErrors, setSendErrors] = useState<Set<string>>(new Set());
  const [editing, setEditing] = useState<ScheduledMessage | null>(null);

  const messages = useMemo(() => scheduledMessages.get(roomId) ?? [], [scheduledMessages, roomId]);

  // Remove scheduled messages whose time has passed
  useEffect(() => {
    if (messages.length === 0) return undefined;

    const nearestSendAt = Math.min(...messages.map((m) => m.sendAt));
    const delay = nearestSendAt - Date.now();

    const timer = setTimeout(
      () => {
        const now = Date.now();
        setScheduledMessages((prev) => {
          const next = new Map(prev);
          const current = next.get(roomId) ?? [];
          const remaining = current.filter((m) => m.sendAt > now);
          if (remaining.length === 0) {
            next.delete(roomId);
          } else {
            next.set(roomId, remaining);
          }
          return next;
        });
      },
      Math.max(0, delay) + 2000,
    ); // 2s grace after scheduled time

    return () => clearTimeout(timer);
  }, [messages, roomId, setScheduledMessages]);

  const handleCancel = useCallback(
    async (msg: ScheduledMessage) => {
      if (cancelling.has(msg.delayId)) return;
      setCancelling((prev) => new Set(prev).add(msg.delayId));
      setCancelErrors((prev) => {
        if (!prev.has(msg.delayId)) return prev;
        const next = new Set(prev);
        next.delete(msg.delayId);
        return next;
      });
      try {
        await cancelScheduledMessage(mx, msg.delayId);
        // Only prune local state once the server confirms cancellation. If we
        // removed it optimistically the still-live delayed event would fire and
        // the "cancelled" message would send anyway.
        setScheduledMessages((prev) => {
          const next = new Map(prev);
          const current = next.get(roomId) ?? [];
          const remaining = current.filter((m) => m.delayId !== msg.delayId);
          if (remaining.length === 0) {
            next.delete(roomId);
          } else {
            next.set(roomId, remaining);
          }
          return next;
        });
      } catch {
        // Keep the item (still cancellable) and surface an inline error; the
        // delayed event is still scheduled on the server.
        setCancelErrors((prev) => new Set(prev).add(msg.delayId));
      } finally {
        setCancelling((prev) => {
          const next = new Set(prev);
          next.delete(msg.delayId);
          return next;
        });
      }
    },
    [mx, roomId, cancelling, setScheduledMessages],
  );

  const handleSendNow = useCallback(
    async (msg: ScheduledMessage) => {
      if (cancelling.has(msg.delayId)) return;
      setCancelling((prev) => new Set(prev).add(msg.delayId));
      setSendErrors((prev) => {
        if (!prev.has(msg.delayId)) return prev;
        const next = new Set(prev);
        next.delete(msg.delayId);
        return next;
      });
      try {
        await sendScheduledMessageNow(mx, msg.delayId);
        // Only prune once the server confirms the send. The delayed event is now
        // consumed and appears in the timeline; dropping it before confirmation
        // could hide a still-live event that never actually sent.
        setScheduledMessages((prev) => {
          const next = new Map(prev);
          const current = next.get(roomId) ?? [];
          const remaining = current.filter((m) => m.delayId !== msg.delayId);
          if (remaining.length === 0) {
            next.delete(roomId);
          } else {
            next.set(roomId, remaining);
          }
          return next;
        });
      } catch {
        // Keep the item (still sendable/editable/cancellable) and surface an
        // inline error; the delayed event is still scheduled on the server.
        setSendErrors((prev) => new Set(prev).add(msg.delayId));
      } finally {
        setCancelling((prev) => {
          const next = new Set(prev);
          next.delete(msg.delayId);
          return next;
        });
      }
    },
    [mx, roomId, cancelling, setScheduledMessages],
  );

  // Editing = cancel-old + schedule-new (MSC4140 has no in-place edit). The modal has
  // already scheduled the NEW message by the time this fires; add it, then cancel the
  // old one — removing the old from state only once the server confirms, so a failed
  // cancel leaves it visible (and retriable) instead of letting it silently fire.
  const handleEdit = useCallback(
    (oldMsg: ScheduledMessage, newDelayId: string, sendAt: number, content: IContent) => {
      // Add the newly-scheduled message up front (nothing lost yet).
      setScheduledMessages((prev) => {
        const next = new Map(prev);
        const current = (next.get(roomId) ?? []).filter((m) => m.delayId !== newDelayId);
        next.set(roomId, [{ delayId: newDelayId, roomId, content, sendAt }, ...current]);
        return next;
      });
      // Mark the old message as cancelling so its row's Edit/Cancel buttons are
      // disabled while we tear it down. Without this the old row stays live during
      // the in-flight cancel and a second edit could orphan a still-scheduled event
      // (both would fire). Also clear any stale error from a prior failed cancel.
      setCancelling((prev) => new Set(prev).add(oldMsg.delayId));
      setCancelErrors((prev) => {
        if (!prev.has(oldMsg.delayId)) return prev;
        const next = new Set(prev);
        next.delete(oldMsg.delayId);
        return next;
      });
      setSendErrors((prev) => {
        if (!prev.has(oldMsg.delayId)) return prev;
        const next = new Set(prev);
        next.delete(oldMsg.delayId);
        return next;
      });
      setEditing(null);
      cancelScheduledMessage(mx, oldMsg.delayId)
        .then(() => {
          setScheduledMessages((prev) => {
            const next = new Map(prev);
            const remaining = (next.get(roomId) ?? []).filter((m) => m.delayId !== oldMsg.delayId);
            if (remaining.length === 0) next.delete(roomId);
            else next.set(roomId, remaining);
            return next;
          });
        })
        .catch(() => {
          // Cancel failed — the old delayed event is still live server-side, so it
          // must stay visible and retriable. Re-insert it if auto-prune removed the
          // row while the modal was open, otherwise the failure (and the duplicate
          // it will send) would be invisible.
          setScheduledMessages((prev) => {
            const next = new Map(prev);
            const current = next.get(roomId) ?? [];
            if (!current.some((m) => m.delayId === oldMsg.delayId)) {
              next.set(roomId, [...current, oldMsg]);
            }
            return next;
          });
          setCancelErrors((prev) => new Set(prev).add(oldMsg.delayId));
        })
        .finally(() => {
          setCancelling((prev) => {
            const next = new Set(prev);
            next.delete(oldMsg.delayId);
            return next;
          });
        });
    },
    [mx, roomId, setScheduledMessages],
  );

  if (messages.length === 0 && !editing) return null;

  return (
    <>
      {editing && (
        <ScheduleMessageModal
          roomId={roomId}
          initialBody={typeof editing.content.body === 'string' ? editing.content.body : ''}
          initialSendAt={editing.sendAt}
          title="Edit scheduled message"
          submitLabel="Reschedule"
          onScheduled={(newDelayId, sendAt, content) =>
            handleEdit(editing, newDelayId, sendAt, content)
          }
          onClose={() => setEditing(null)}
        />
      )}
      <Box
        direction="Column"
        style={{
          borderBottom: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
          background: color.SurfaceVariant.Container,
        }}
      >
        {/* Tray header */}
        <Button
          variant="Secondary"
          fill="None"
          radii="0"
          onClick={() => setExpanded((v) => !v)}
          aria-expanded={expanded}
          aria-label={`${messages.length} scheduled message${messages.length !== 1 ? 's' : ''}`}
          before={<Icon src={Icons.Clock} size="50" />}
          after={<Icon src={expanded ? Icons.ChevronTop : Icons.ChevronBottom} size="50" />}
          style={{
            padding: `${config.space.S100} ${config.space.S300}`,
            justifyContent: 'flex-start',
          }}
        >
          <Text size="T200" style={{ flex: 1, fontWeight: 600, textAlign: 'left' }}>
            {messages.length} scheduled message{messages.length !== 1 ? 's' : ''}
          </Text>
        </Button>

        {/* Tray items */}
        {expanded && (
          <Box direction="Column">
            {messages.map((msg) => {
              const bodyPreview =
                typeof msg.content.body === 'string' ? (msg.content.body as string) : '(message)';
              const rowDesc = `${bodyPreview} at ${formatSendAt(msg.sendAt)}`;
              return (
                <Box
                  key={msg.delayId}
                  direction="Column"
                  style={{
                    padding: `${config.space.S100} ${config.space.S300}`,
                    borderTop: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
                  }}
                >
                  <Box alignItems="Center" gap="200">
                    <Text
                      size="T200"
                      priority="400"
                      style={{
                        flex: 1,
                        overflow: 'hidden',
                        textOverflow: 'ellipsis',
                        whiteSpace: 'nowrap',
                      }}
                    >
                      {bodyPreview}
                    </Text>
                    <Text
                      size="T200"
                      priority="300"
                      style={{ whiteSpace: 'nowrap', flexShrink: 0 }}
                    >
                      {formatSendAt(msg.sendAt)}
                    </Text>
                    <IconButton
                      size="300"
                      radii="300"
                      variant="SurfaceVariant"
                      aria-label={`Send scheduled message now: ${rowDesc}`}
                      disabled={cancelling.has(msg.delayId)}
                      onClick={(e) => {
                        e.stopPropagation();
                        handleSendNow(msg);
                      }}
                    >
                      <Icon src={Icons.Send} size="50" />
                    </IconButton>
                    <IconButton
                      size="300"
                      radii="300"
                      variant="SurfaceVariant"
                      aria-label={`Edit scheduled message: ${rowDesc}`}
                      disabled={cancelling.has(msg.delayId)}
                      onClick={(e) => {
                        e.stopPropagation();
                        setEditing(msg);
                      }}
                    >
                      <Icon src={Icons.Pencil} size="50" />
                    </IconButton>
                    <IconButton
                      size="300"
                      radii="300"
                      variant="SurfaceVariant"
                      aria-label={`Cancel scheduled message: ${rowDesc}`}
                      disabled={cancelling.has(msg.delayId)}
                      onClick={(e) => {
                        e.stopPropagation();
                        handleCancel(msg);
                      }}
                    >
                      <Icon src={Icons.Cross} size="50" />
                    </IconButton>
                  </Box>
                  {cancelErrors.has(msg.delayId) && (
                    <Text
                      size="T200"
                      style={{ color: color.Critical.Main, paddingTop: config.space.S100 }}
                    >
                      Could not cancel this message. Try again.
                    </Text>
                  )}
                  {sendErrors.has(msg.delayId) && (
                    <Text
                      size="T200"
                      style={{ color: color.Critical.Main, paddingTop: config.space.S100 }}
                    >
                      Could not send now. Try again.
                    </Text>
                  )}
                </Box>
              );
            })}
          </Box>
        )}
      </Box>
    </>
  );
}
