import React, { ChangeEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { Room } from 'matrix-js-sdk';
import { useAtom } from 'jotai';
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import {
  Avatar,
  Box,
  Button,
  Header,
  Icon,
  IconButton,
  Icons,
  Input,
  Scroll,
  Text,
  color,
  config,
} from 'folds';
import classNames from 'classnames';
import { useBookmarks, Bookmark } from '../../hooks/useBookmarks';
import {
  BookmarkSort,
  isBookmarkSort,
  sortBookmarks,
  groupBookmarksByRoom,
} from '../../utils/bookmarks';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomEvent } from '../../hooks/useRoomEvent';
import { MessageDeletedContent } from '../../components/message/content/FallbackContent';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { RoomAvatar } from '../../components/room-avatar';
import { getRoomAvatarUrl, getMemberName } from '../../utils/room';
import { nameInitials } from '../../utils/common';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { stopPropagation } from '../../utils/keyboard';
import * as css from './BookmarksPanel.css';

function formatTimeAgo(ts: number): string {
  const diff = Date.now() - ts;
  const minutes = Math.floor(diff / 60_000);
  if (minutes < 1) return 'just now';
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.floor(hours / 24);
  if (days === 1) return 'yesterday';
  if (days < 7) return `${days}d ago`;
  return new Date(ts).toLocaleDateString();
}

// Remember the last-chosen sort across panel opens (the panel unmounts on close).
// getOnInit reads localStorage synchronously at init so the persisted sort is
// applied on the very first render (no flash from the 'newest' default).
const bookmarkSortAtom = atomWithStorage<BookmarkSort>(
  'cinny_bookmarks_sort_v1',
  'newest',
  createJSONStorage(() => localStorage),
  { getOnInit: true },
);

const SORT_OPTIONS: { value: BookmarkSort; label: string }[] = [
  { value: 'newest', label: 'Newest' },
  { value: 'oldest', label: 'Oldest' },
  { value: 'room', label: 'By room' },
];

// Segmented sort button — mirrors MediaGallery's tab styling for house consistency.
function SortButton({
  label,
  active,
  onClick,
}: {
  label: string;
  active: boolean;
  onClick: () => void;
}) {
  return (
    <Button
      size="300"
      variant={active ? 'Primary' : 'Secondary'}
      fill={active ? 'Solid' : 'Soft'}
      radii="300"
      aria-pressed={active}
      onClick={onClick}
    >
      <Text size="B300">{label}</Text>
    </Button>
  );
}

type BookmarkItemProps = {
  bookmark: Bookmark;
  onJump: (roomId: string, eventId: string) => void;
  onRemove: (eventId: string) => void;
  // Optional live-rendered preview node; falls back to the stored snapshot when absent.
  preview?: ReactNode;
  // Live-resolved author name; falls back to the stored snapshot when absent.
  senderName?: string;
};

function BookmarkItem({ bookmark, onJump, onRemove, preview, senderName }: BookmarkItemProps) {
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();
  const room = mx.getRoom(bookmark.roomId) ?? undefined;
  const displayRoomName = room?.name ?? bookmark.roomName;
  const avatarUrl = room
    ? (getRoomAvatarUrl(mx, room, 96, useAuthentication) ?? undefined)
    : undefined;
  // Prefer a live-resolved author name, then the stored snapshot.
  const author = senderName ?? bookmark.senderName;
  const timeAgo = formatTimeAgo(bookmark.savedAt);

  return (
    <Box
      direction="Column"
      gap="200"
      style={{
        padding: config.space.S300,
        borderRadius: config.radii.R300,
        background: color.SurfaceVariant.Container,
      }}
    >
      {/* Room identity + remove */}
      <Box alignItems="Center" gap="200">
        <Avatar size="200" radii="300">
          <RoomAvatar
            roomId={bookmark.roomId}
            src={avatarUrl}
            alt={displayRoomName}
            renderFallback={() => <Text size="H6">{nameInitials(displayRoomName)}</Text>}
          />
        </Avatar>
        <Box grow="Yes" direction="Column" style={{ minWidth: 0 }}>
          <Text size="T200" truncate style={{ fontWeight: config.fontWeight.W600 }}>
            {displayRoomName}
          </Text>
          <Text size="T200" priority="300" truncate>
            {author ? `${author} · ${timeAgo}` : timeAgo}
          </Text>
        </Box>
        <IconButton
          size="300"
          variant="SurfaceVariant"
          fill="None"
          radii="300"
          onClick={() => onRemove(bookmark.eventId)}
          aria-label="Remove saved message"
        >
          <Icon size="100" src={Icons.Delete} />
        </IconButton>
      </Box>

      {/* Message preview — clicking jumps to the message */}
      <Button
        variant="Secondary"
        fill="Soft"
        size="300"
        radii="300"
        onClick={() => onJump(bookmark.roomId, bookmark.eventId)}
        aria-label="Jump to saved message"
        style={{ justifyContent: 'flex-start', height: 'unset', padding: config.space.S200 }}
      >
        <Text className={css.BookmarkPreview} size="T200" priority="400">
          {preview ?? (bookmark.previewText || '(no preview)')}
        </Text>
      </Button>
    </Box>
  );
}

type LiveBookmarkItemProps = BookmarkItemProps & { room: Room };

// Renders the same layout as BookmarkItem, but resolves the message body live so
// edits (m.replace, applied by useRoomEvent) and redactions are reflected. The
// stored snapshot (previewText) remains the fallback for loading/failed/empty states.
function LiveBookmarkItem({ room, bookmark, onJump, onRemove }: LiveBookmarkItemProps) {
  const liveEvent = useRoomEvent(room, bookmark.eventId, () =>
    room.findEventById(bookmark.eventId),
  );

  const snapshot = bookmark.previewText || '(no preview)';
  let preview: ReactNode = snapshot;

  // undefined (loading) and null (fetch failed / not found) both keep the snapshot.
  if (liveEvent) {
    if (liveEvent.isRedacted()) {
      preview = (
        <MessageDeletedContent
          reason={liveEvent.getUnsigned().redacted_because?.content?.reason as string | undefined}
        />
      );
    } else {
      // body is already the edited text since useRoomEvent applied m.replace.
      const { body } = liveEvent.getContent();
      preview = typeof body === 'string' && body ? body : snapshot;
    }
  }

  // Resolve the author's current display name from the live event when available.
  const liveSender = liveEvent?.getSender();
  const senderName = liveSender ? getMemberName(room, liveSender) : undefined;

  return (
    <BookmarkItem
      bookmark={bookmark}
      onJump={onJump}
      onRemove={onRemove}
      preview={preview}
      senderName={senderName}
    />
  );
}

type RoomGroupHeaderProps = {
  roomId: string;
  roomName: string;
  count: number;
  collapsed: boolean;
  contentId: string;
  onToggle: () => void;
};

// Collapsible section header for the "By room" grouping. Uses the live room name
// / avatar when the room is joined, falling back to the stored snapshot name.
function RoomGroupHeader({
  roomId,
  roomName,
  count,
  collapsed,
  contentId,
  onToggle,
}: RoomGroupHeaderProps) {
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();
  const room = mx.getRoom(roomId) ?? undefined;
  const displayRoomName = room?.name ?? roomName;
  const avatarUrl = room
    ? (getRoomAvatarUrl(mx, room, 96, useAuthentication) ?? undefined)
    : undefined;

  return (
    <Button
      variant="Secondary"
      fill="None"
      size="300"
      radii="300"
      onClick={onToggle}
      aria-expanded={!collapsed}
      aria-controls={contentId}
      // Explicit name avoids the avatar alt + visible name being announced twice,
      // and gives the bare count meaning for screen readers.
      aria-label={`${displayRoomName}, ${count} saved message${count !== 1 ? 's' : ''}`}
      style={{ justifyContent: 'flex-start', padding: config.space.S200 }}
    >
      <Box grow="Yes" alignItems="Center" gap="200" style={{ minWidth: 0 }}>
        <Icon size="100" src={collapsed ? Icons.ChevronRight : Icons.ChevronBottom} />
        <Avatar size="200" radii="300">
          <RoomAvatar
            roomId={roomId}
            src={avatarUrl}
            alt=""
            renderFallback={() => <Text size="H6">{nameInitials(displayRoomName)}</Text>}
          />
        </Avatar>
        <Text size="T200" truncate style={{ flexGrow: 1, fontWeight: config.fontWeight.W600 }}>
          {displayRoomName}
        </Text>
        <Text size="T200" priority="300" style={{ flexShrink: 0 }}>
          {count}
        </Text>
      </Box>
    </Button>
  );
}

type BookmarksPanelProps = {
  onClose: () => void;
};

export function BookmarksPanel({ onClose }: BookmarksPanelProps) {
  const mx = useMatrixClient();
  const { bookmarks, removeBookmark } = useBookmarks();
  const { navigateRoom } = useRoomNavigate();
  const [filter, setFilter] = useState('');
  const [storedSort, setSort] = useAtom(bookmarkSortAtom);
  // Normalize a stale/corrupt persisted value so exactly one sort is always active.
  const sort: BookmarkSort = isBookmarkSort(storedSort) ? storedSort : 'newest';
  // roomIds whose group section is collapsed (only relevant in "By room" mode).
  const [collapsed, setCollapsed] = useState<Set<string>>(new Set());

  const toggleGroup = useCallback((roomId: string) => {
    setCollapsed((prev) => {
      const next = new Set(prev);
      if (next.has(roomId)) next.delete(roomId);
      else next.add(roomId);
      return next;
    });
  }, []);

  // Escape closes the panel (parity with the app's other overlays/drawers).
  useEffect(() => {
    const handleKeyDown = (evt: KeyboardEvent) => {
      if (evt.key === 'Escape') {
        stopPropagation(evt);
        onClose();
      }
    };
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [onClose]);

  const handleJump = useCallback(
    (roomId: string, eventId: string) => {
      navigateRoom(roomId, eventId);
      onClose();
    },
    [navigateRoom, onClose],
  );

  const handleFilterChange = (e: ChangeEvent<HTMLInputElement>) => {
    setFilter(e.target.value);
  };

  const query = filter.trim().toLowerCase();
  const filtered: Bookmark[] = useMemo(
    () =>
      query.length === 0
        ? bookmarks
        : bookmarks.filter(
            (bk) =>
              bk.previewText.toLowerCase().includes(query) ||
              bk.roomName.toLowerCase().includes(query) ||
              (bk.senderName?.toLowerCase().includes(query) ?? false),
          ),
    [bookmarks, query],
  );

  // Prune collapsed roomIds that no longer have any bookmark, so a room re-saved
  // later doesn't reappear pre-collapsed and the Set can't grow unbounded.
  useEffect(() => {
    setCollapsed((prev) => {
      if (prev.size === 0) return prev;
      const live = new Set(bookmarks.map((bk) => bk.roomId));
      let changed = false;
      const next = new Set<string>();
      prev.forEach((roomId) => {
        if (live.has(roomId)) next.add(roomId);
        else changed = true;
      });
      return changed ? next : prev;
    });
  }, [bookmarks]);

  // Live render when the room is joined (useRoomEvent needs a non-null Room);
  // otherwise fall back to the stored snapshot for rooms we've left. Shared by
  // both the flat (newest/oldest) and grouped (by room) render paths.
  const renderItem = useCallback(
    (bk: Bookmark) => {
      const room = mx.getRoom(bk.roomId);
      return room ? (
        <LiveBookmarkItem
          key={bk.eventId}
          room={room}
          bookmark={bk}
          onJump={handleJump}
          onRemove={removeBookmark}
        />
      ) : (
        <BookmarkItem
          key={bk.eventId}
          bookmark={bk}
          onJump={handleJump}
          onRemove={removeBookmark}
        />
      );
    },
    [mx, handleJump, removeBookmark],
  );

  const sortedItems = useMemo(
    () => (sort === 'room' ? filtered : sortBookmarks(filtered, sort)),
    [filtered, sort],
  );
  const groups = useMemo(
    () => (sort === 'room' ? groupBookmarksByRoom(filtered) : []),
    [filtered, sort],
  );

  return (
    <Box
      className={classNames(css.BookmarksPanel, ContainerColor({ variant: 'Background' }))}
      shrink="No"
      direction="Column"
    >
      <Header className={css.BookmarksHeader} variant="Background" size="600">
        <Box grow="Yes" alignItems="Center" gap="200">
          <Icon src={Icons.Star} size="200" />
          <Box grow="Yes">
            <Text size="H4" truncate>
              Saved Messages
            </Text>
          </Box>
          <IconButton size="300" radii="300" aria-label="Close saved messages" onClick={onClose}>
            <Icon src={Icons.Cross} />
          </IconButton>
        </Box>
      </Header>

      {/* Search */}
      <Box className={css.BookmarksToolbar} direction="Column" gap="100">
        <Input
          variant="SurfaceVariant"
          size="400"
          radii="300"
          placeholder="Search saved messages…"
          value={filter}
          onChange={handleFilterChange}
          before={<Icon size="100" src={Icons.Search} />}
          after={
            filter.length > 0 ? (
              <IconButton
                size="300"
                variant="SurfaceVariant"
                fill="None"
                radii="300"
                aria-label="Clear search"
                onClick={() => setFilter('')}
              >
                <Icon size="100" src={Icons.Cross} />
              </IconButton>
            ) : undefined
          }
        />
        {bookmarks.length > 0 && (
          <>
            <Text size="T200" priority="300" truncate>
              {filtered.length === bookmarks.length
                ? `${bookmarks.length} saved message${bookmarks.length !== 1 ? 's' : ''}`
                : `${filtered.length} of ${bookmarks.length} messages`}
            </Text>
            <Box as="div" role="group" aria-label="Sort saved messages" gap="100">
              {SORT_OPTIONS.map((opt) => (
                <SortButton
                  key={opt.value}
                  label={opt.label}
                  active={sort === opt.value}
                  onClick={() => setSort(opt.value)}
                />
              ))}
            </Box>
          </>
        )}
      </Box>

      {/* List */}
      <Box grow="Yes" style={{ minHeight: 0 }}>
        <Scroll variant="Background" size="300" visibility="Hover" hideTrack>
          {filtered.length === 0 ? (
            <Box
              direction="Column"
              alignItems="Center"
              justifyContent="Center"
              gap="300"
              style={{ padding: config.space.S700, textAlign: 'center' }}
            >
              <Icon size="600" src={Icons.Star} style={{ opacity: config.opacity.Disabled }} />
              <Text size="T300" priority="300" align="Center">
                {bookmarks.length === 0
                  ? 'No saved messages yet. Right-click any message to save it.'
                  : 'No saved messages match your search.'}
              </Text>
            </Box>
          ) : (
            <Box className={css.BookmarksContent} direction="Column" gap="200">
              {sort === 'room'
                ? groups.map((group) => {
                    const isCollapsed = collapsed.has(group.roomId);
                    const contentId = `bookmark-group-${group.roomId}`;
                    return (
                      <Box key={group.roomId} direction="Column" gap="200">
                        <RoomGroupHeader
                          roomId={group.roomId}
                          roomName={group.roomName}
                          count={group.items.length}
                          collapsed={isCollapsed}
                          contentId={contentId}
                          onToggle={() => toggleGroup(group.roomId)}
                        />
                        {!isCollapsed && (
                          <Box id={contentId} direction="Column" gap="200">
                            {group.items.map((bk) => renderItem(bk))}
                          </Box>
                        )}
                      </Box>
                    );
                  })
                : sortedItems.map((bk) => renderItem(bk))}
            </Box>
          )}
        </Scroll>
      </Box>
    </Box>
  );
}
