import React, { ReactNode, useCallback, useRef, useState } from 'react';
import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
import {
  Avatar,
  Badge,
  Box,
  Button,
  Dialog,
  Icon,
  Icons,
  Overlay,
  OverlayBackdrop,
  OverlayCenter,
  Spinner,
  Text,
  as,
  color,
  config,
} from 'folds';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import * as css from './style.css';
import { RoomAvatar } from '../room-avatar';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { nameInitials } from '../../utils/common';
import { millify } from '../../plugins/millify';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
import { RoomType, StateEvent } from '../../../types/matrix/room';
import { useJoinedRoomId } from '../../hooks/useJoinedRoomId';
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
import { getRoomAvatarUrl, getStateEvent } from '../../utils/room';
import { useStateEventCallback } from '../../hooks/useStateEventCallback';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';

type GridColumnCount = '1' | '2' | '3';
const getGridColumnCount = (gridWidth: number): GridColumnCount => {
  if (gridWidth <= 498) return '1';
  if (gridWidth <= 748) return '2';
  return '3';
};

const setGridColumnCount = (grid: HTMLElement, count: GridColumnCount): void => {
  grid.style.setProperty('grid-template-columns', `repeat(${count}, 1fr)`);
};

export function RoomCardGrid({ children }: { children: ReactNode }) {
  const gridRef = useRef<HTMLDivElement>(null);

  useElementSizeObserver(
    useCallback(() => gridRef.current, []),
    useCallback((width, _, target) => setGridColumnCount(target, getGridColumnCount(width)), []),
  );

  return (
    <Box className={css.CardGrid} direction="Row" gap="400" wrap="Wrap" ref={gridRef}>
      {children}
    </Box>
  );
}

export const RoomCardBase = as<'div'>(({ className, ...props }, ref) => (
  <Box
    direction="Column"
    gap="300"
    className={classNames(css.RoomCardBase, className)}
    {...props}
    ref={ref}
  />
));

export const RoomCardName = as<'h6'>(({ ...props }, ref) => (
  <Text as="h6" size="H6" truncate {...props} ref={ref} />
));

export const RoomCardTopic = as<'p'>(({ className, ...props }, ref) => (
  <Text
    as="p"
    size="T200"
    className={classNames(css.RoomCardTopic, className)}
    {...props}
    priority="400"
    ref={ref}
  />
));

function ErrorDialog({
  title,
  message,
  children,
}: {
  title: string;
  message: string;
  children: (openError: () => void) => ReactNode;
}) {
  const [viewError, setViewError] = useState(false);
  const closeError = () => setViewError(false);
  const openError = () => setViewError(true);

  return (
    <>
      {children(openError)}
      <Overlay open={viewError} backdrop={<OverlayBackdrop />}>
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              clickOutsideDeactivates: true,
              onDeactivate: closeError,
              escapeDeactivates: stopPropagation,
            }}
          >
            <Dialog variant="Surface">
              <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
                <Box direction="Column" gap="100">
                  <Text>{title}</Text>
                  <Text style={{ color: color.Critical.Main }} size="T300" priority="400">
                    {message}
                  </Text>
                </Box>
                <Button size="400" variant="Secondary" fill="Soft" onClick={closeError}>
                  <Text size="B400">Cancel</Text>
                </Button>
              </Box>
            </Dialog>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
    </>
  );
}

type RoomCardProps = {
  roomIdOrAlias: string;
  allRooms: string[];
  avatarUrl?: string;
  name?: string;
  topic?: string;
  memberCount?: number;
  roomType?: string;
  joinRule?: string;
  encrypted?: boolean;
  canonicalAlias?: string;
  worldReadable?: boolean;
  /** The viewer's own membership over federation (leave/invite/knock/ban). */
  membership?: string;
  /** Larger, centered treatment for the single-room preview page. */
  hero?: boolean;
  viaServers?: string[];
  onView?: (roomId: string) => void;
  renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
};

// Map a room's join rule to a short preview chip. Public is the common case and
// gets no chip (avoids noise); the rest tell the user how entry works.
const joinRuleLabel = (joinRule?: string): string | undefined => {
  if (joinRule === JoinRule.Restricted || joinRule === 'knock_restricted') return 'Restricted';
  if (joinRule === JoinRule.Knock) return 'Ask to join';
  if (joinRule === JoinRule.Invite) return 'Invite only';
  if (joinRule === JoinRule.Private) return 'Private';
  return undefined;
};

export const RoomCard = as<'div', RoomCardProps>(
  (
    {
      roomIdOrAlias,
      allRooms,
      avatarUrl,
      name,
      topic,
      memberCount,
      roomType,
      joinRule,
      encrypted,
      canonicalAlias,
      worldReadable,
      membership,
      hero,
      viaServers,
      onView,
      renderTopicViewer,
      ...props
    },
    ref,
  ) => {
    const mx = useMatrixClient();
    const useAuthentication = useMediaAuthentication();
    const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias);
    const joinedRoom = mx.getRoom(joinedRoomId);
    const [topicEvent, setTopicEvent] = useState(() =>
      joinedRoom ? getStateEvent(joinedRoom, StateEvent.RoomTopic) : undefined,
    );

    // Name falls back to the alias (readable), then the alias localpart, then the
    // raw id — never the raw `!id` when a nicer form exists.
    const fallbackName = canonicalAlias || getMxIdLocalPart(roomIdOrAlias) || roomIdOrAlias;

    const avatar = joinedRoom
      ? getRoomAvatarUrl(mx, joinedRoom, 96, useAuthentication)
      : avatarUrl && mxcUrlToHttp(mx, avatarUrl, useAuthentication, 96, 96, 'crop');

    const roomName = joinedRoom?.name || name || fallbackName;
    // No topic → render a muted placeholder, NOT the raw room id.
    const roomTopic = (topicEvent?.getContent().topic as string) || topic || undefined;
    // Only show an alias line when it adds info beyond the displayed name.
    const aliasLine = canonicalAlias && canonicalAlias !== roomName ? canonicalAlias : undefined;
    const joinedMemberCount = joinedRoom?.getJoinedMemberCount() ?? memberCount;

    useStateEventCallback(
      mx,
      useCallback(
        (event) => {
          if (
            joinedRoom &&
            event.getRoomId() === joinedRoom.roomId &&
            event.getType() === StateEvent.RoomTopic
          ) {
            setTopicEvent(getStateEvent(joinedRoom, StateEvent.RoomTopic));
          }
        },
        [joinedRoom],
      ),
    );

    const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
      useCallback(
        () => mx.joinRoom(roomIdOrAlias, { viaServers }),
        [mx, roomIdOrAlias, viaServers],
      ),
    );
    const [knockState, knock] = useAsyncCallback<{ room_id: string }, MatrixError, []>(
      useCallback(
        () => mx.knockRoom(roomIdOrAlias, { viaServers }),
        [mx, roomIdOrAlias, viaServers],
      ),
    );
    // A knock-rule room can't be joined directly — request to join instead.
    const isKnock = joinRule === JoinRule.Knock;
    // Membership known from the summary survives reloads (unlike local knockState).
    const invited = membership === 'invite';
    const alreadyKnocked = membership === 'knock';
    const banned = membership === 'ban';
    const action = isKnock ? knock : join;
    const actionState = isKnock ? knockState : joinState;
    const acting =
      actionState.status === AsyncStatus.Loading || actionState.status === AsyncStatus.Success;
    const requested = alreadyKnocked || knockState.status === AsyncStatus.Success;
    let actionLabel: string;
    if (banned) actionLabel = 'Banned';
    else if (invited) actionLabel = acting ? 'Joining' : 'Accept invite';
    else if (isKnock)
      actionLabel = requested ? 'Requested' : acting ? 'Requesting' : 'Request to join';
    else actionLabel = acting ? 'Joining' : 'Join';
    const actionDisabled = banned || requested || acting;
    const chip = joinRuleLabel(joinRule);

    const [viewTopic, setViewTopic] = useState(false);
    const closeTopic = () => setViewTopic(false);
    const openTopic = () => setViewTopic(true);

    return (
      <RoomCardBase {...props} ref={ref}>
        <Box gap="200" justifyContent="SpaceBetween">
          <Avatar size="500">
            <RoomAvatar
              roomId={roomIdOrAlias}
              src={avatar ?? undefined}
              alt={roomIdOrAlias}
              renderFallback={() => (
                <Text as="span" size="H3">
                  {nameInitials(roomName)}
                </Text>
              )}
            />
          </Avatar>
          {(roomType === RoomType.Space || joinedRoom?.isSpaceRoom()) && (
            <Badge variant="Secondary" fill="Soft" outlined>
              <Text size="L400">Space</Text>
            </Badge>
          )}
        </Box>
        <Box grow="Yes" direction="Column" gap="100">
          <RoomCardName>{roomName}</RoomCardName>
          {aliasLine && (
            <Text size="T200" priority="300" truncate>
              {aliasLine}
            </Text>
          )}
          {roomTopic ? (
            <RoomCardTopic
              onClick={openTopic}
              onKeyDown={onEnterOrSpace(openTopic)}
              tabIndex={0}
              style={hero ? { WebkitLineClamp: 10 } : undefined}
            >
              {roomTopic}
            </RoomCardTopic>
          ) : (
            <Text size="T200" priority="300" style={{ opacity: 0.6 }}>
              No description
            </Text>
          )}

          <Overlay open={viewTopic} backdrop={<OverlayBackdrop />}>
            <OverlayCenter>
              <FocusTrap
                focusTrapOptions={{
                  initialFocus: false,
                  clickOutsideDeactivates: true,
                  onDeactivate: closeTopic,
                  escapeDeactivates: stopPropagation,
                }}
              >
                {renderTopicViewer(roomName, roomTopic ?? '', closeTopic)}
              </FocusTrap>
            </OverlayCenter>
          </Overlay>
        </Box>
        {typeof joinedMemberCount === 'number' && (
          <Box gap="100">
            <Icon size="50" src={Icons.User} />
            <Text size="T200">{`${millify(joinedMemberCount)} Members`}</Text>
          </Box>
        )}
        {!joinedRoom && (chip || encrypted || worldReadable) && (
          <Box gap="100" wrap="Wrap">
            {chip && (
              <Badge variant="Secondary" fill="Soft" outlined>
                <Text size="L400">{chip}</Text>
              </Badge>
            )}
            {encrypted && (
              <Badge variant="Success" fill="Soft" outlined>
                <Text size="L400">Encrypted</Text>
              </Badge>
            )}
            {worldReadable && (
              <Badge variant="Secondary" fill="Soft" outlined>
                <Text size="L400">Readable</Text>
              </Badge>
            )}
          </Box>
        )}
        {typeof joinedRoomId === 'string' && (
          <Button
            onClick={onView ? () => onView(joinedRoomId) : undefined}
            variant="Secondary"
            fill="Soft"
            size="300"
          >
            <Text size="B300" truncate>
              View
            </Text>
          </Button>
        )}
        {typeof joinedRoomId !== 'string' && actionState.status !== AsyncStatus.Error && (
          <Button
            onClick={action}
            variant={hero ? 'Primary' : 'Secondary'}
            fill={hero ? 'Solid' : undefined}
            size="300"
            disabled={actionDisabled}
            before={acting && <Spinner size="50" variant="Secondary" fill="Soft" />}
          >
            <Text size="B300" truncate>
              {actionLabel}
            </Text>
          </Button>
        )}
        {typeof joinedRoomId !== 'string' && actionState.status === AsyncStatus.Error && (
          <Box gap="200">
            <Button
              onClick={action}
              className={css.ActionButton}
              variant="Critical"
              fill="Solid"
              size="300"
            >
              <Text size="B300" truncate>
                Retry
              </Text>
            </Button>
            <ErrorDialog
              title={isKnock ? 'Request Error' : 'Join Error'}
              message={actionState.error.message || 'Failed to join. Unknown Error.'}
            >
              {(openError) => (
                <Button
                  onClick={openError}
                  className={css.ActionButton}
                  variant="Critical"
                  fill="Soft"
                  outlined
                  size="300"
                >
                  <Text size="B300" truncate>
                    View Error
                  </Text>
                </Button>
              )}
            </ErrorDialog>
          </Box>
        )}
      </RoomCardBase>
    );
  },
);
