import React, {
  ChangeEventHandler,
  MouseEventHandler,
  useCallback,
  useMemo,
  useState,
} from 'react';
import FocusTrap from 'focus-trap-react';
import { Room } from 'matrix-js-sdk';
import { useTranslation } from 'react-i18next';
import {
  Avatar,
  Box,
  Icon,
  Icons,
  Input,
  Line,
  MenuItem,
  Modal,
  Overlay,
  OverlayBackdrop,
  OverlayCenter,
  Scroll,
  Spinner,
  Text,
  color,
  config,
  toRem,
} from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import {
  SearchItemStrGetter,
  UseAsyncSearchOptions,
  useAsyncSearch,
} from '../../hooks/useAsyncSearch';
import { IPowerLevels, readPowerLevel } from '../../hooks/usePowerLevels';
import { getStateEvent, isSpace } from '../../utils/room';
import { getMxIdServer } from '../../utils/matrix';
import { useDirectRooms } from '../../pages/client/direct/useDirectRooms';
import { nameInitials } from '../../utils/common';
import { Membership, StateEvent } from '../../../types/matrix/room';
import type { BotPreset } from './catalog';

const SEARCH_OPTIONS: UseAsyncSearchOptions = {
  matchOptions: { contain: true },
  normalizeOptions: { ignoreWhitespace: false },
};

type BotAddToChatPickerProps = {
  preset: BotPreset;
  requestClose: () => void;
};

// Host-owned room picker for the elevated `add-to-chat` verb. This modal is
// the SOLE authorization boundary for inviting the bot into a room outside its
// control DM (the side-channel verb carries no room/mxid — the host substitutes
// `preset.mxid` and the user picks the room here). NOT built on `useRoomSearch`,
// which returns every room/direct/space with no predicates (F14); the candidate
// set is filtered to the plan's predicates and every invite is re-checked
// fail-closed immediately before `mx.invite` (F9).
export function BotAddToChatPicker({ preset, requestClose }: BotAddToChatPickerProps) {
  const { t } = useTranslation();
  const mx = useMatrixClient();
  const myUserId = mx.getSafeUserId();
  const myServer = mx.getDomain();

  // The bot may be added only to a personal chat — the «Личные»/Direct set
  // (orphan ∪ m.direct rooms, already minus catalog bot control DMs and bridged
  // portals), never a Channel/space room.
  const directRoomIds = useDirectRooms();

  // Plan's predicates (owner decision): joined, non-space, the bot isn't
  // already a member, the user holds invite power, AND the room is hosted
  // entirely on the user's own homeserver (no federated third parties — mirrors
  // the bot's server-side «stays on allowed servers» rule). Encryption is handled
  // separately at render (disabled row + note) and re-checked in the invite.
  // The power read tolerates partial `m.room.power_levels` content —
  // `readPowerLevel` fills Matrix defaults (users_default 0 / invite 0), so a
  // default-open room correctly passes and a restricted room correctly fails.
  const isListable = useCallback(
    (room: Room): boolean => {
      if (room.getMyMembership() !== Membership.Join) return false;
      if (isSpace(room)) return false;
      const botMembership = room.getMember(preset.mxid)?.membership;
      if (botMembership === Membership.Join || botMembership === Membership.Invite) return false;
      const pl =
        getStateEvent(room, StateEvent.RoomPowerLevels, '')?.getContent<IPowerLevels>() ?? {};
      if (readPowerLevel.user(pl, myUserId) < readPowerLevel.action(pl, 'invite')) return false;
      // Hosted entirely on the user's own homeserver: every active (joined or
      // invited) member shares myServer, so the bot is never invited into a
      // federated room (where it would leave anyway).
      const active = room
        .getMembers()
        .filter(
          (mem) => mem.membership === Membership.Join || mem.membership === Membership.Invite
        );
      if (!myServer || !active.every((mem) => getMxIdServer(mem.userId) === myServer)) return false;
      return true;
    },
    [preset.mxid, myUserId, myServer]
  );

  // Snapshot at open — stable identity so `useAsyncSearch` doesn't reset its
  // result every render. Restricted to the «Личные»/Direct set (no Channels);
  // staleness is fine — the per-invite live re-check below is the authoritative
  // gate, not this list.
  const candidateRoomIds = useMemo(
    () =>
      directRoomIds.filter((roomId) => {
        const room = mx.getRoom(roomId);
        return !!room && isListable(room);
      }),
    [mx, directRoomIds, isListable]
  );

  const getItemStr: SearchItemStrGetter<string> = useCallback(
    (roomId) => mx.getRoom(roomId)?.name ?? roomId,
    [mx]
  );
  const [result, search, resetSearch] = useAsyncSearch(
    candidateRoomIds,
    getItemStr,
    SEARCH_OPTIONS
  );
  const roomsToRender = result ? result.items : candidateRoomIds;

  const handleInputChange: ChangeEventHandler<HTMLInputElement> = useCallback(
    (evt) => {
      const value = evt.currentTarget.value.trim();
      if (value === '') {
        resetSearch();
        return;
      }
      search(value);
    },
    [search, resetSearch]
  );

  const [pendingRoomId, setPendingRoomId] = useState<string | null>(null);
  const [inviteState, invite] = useAsyncCallback<void, Error, [string]>(
    useCallback(
      async (roomId: string) => {
        const room = mx.getRoom(roomId);
        // Fail-closed live re-check (F9). The snapshot above can be stale —
        // membership lost, invite power revoked, the bot invited by another
        // device, or encryption enabled since the modal opened. Re-verify the
        // full predicate (incl. NOT encrypted) right before the privileged
        // invite. try/catch upstream surfaces server races (e.g. M_FORBIDDEN
        // re-inviting a banned bot, F13) as an inline error.
        if (!room || !isListable(room) || room.hasEncryptionStateEvent()) {
          throw new Error(t('Bots.add_to_chat_unavailable', { name: preset.name }));
        }
        await mx.invite(roomId, preset.mxid);
      },
      [mx, preset.mxid, preset.name, isListable, t]
    )
  );

  const inviting = inviteState.status === AsyncStatus.Loading;

  const handleRoomClick: MouseEventHandler<HTMLButtonElement> = useCallback(
    (evt) => {
      if (inviting) return;
      const roomId = evt.currentTarget.getAttribute('data-room-id');
      if (!roomId) return;
      setPendingRoomId(roomId);
      invite(roomId)
        .then(() => requestClose())
        .catch(() => undefined); // error rendered inline via inviteState
    },
    [inviting, invite, requestClose]
  );

  return (
    <Overlay open backdrop={<OverlayBackdrop />}>
      <OverlayCenter>
        <FocusTrap
          focusTrapOptions={{
            returnFocusOnDeactivate: false,
            allowOutsideClick: true,
            clickOutsideDeactivates: true,
            onDeactivate: requestClose,
            escapeDeactivates: (evt) => {
              evt.stopPropagation();
              return true;
            },
          }}
        >
          <Modal size="400" style={{ maxHeight: toRem(480), borderRadius: config.radii.R500 }}>
            <Box
              shrink="No"
              direction="Column"
              gap="300"
              style={{ padding: config.space.S400, paddingBottom: config.space.S300 }}
            >
              <Box direction="Column" gap="100">
                <Text size="H4">{t('Bots.add_to_chat_title', { name: preset.name })}</Text>
                <Text size="T200" priority="300">
                  {t('Bots.add_to_chat_subtitle', { name: preset.name })}
                </Text>
              </Box>
              {/* Direct child of a Column Box → stretches to full width (same as
               * the canonical Search modal). A wrapping Row Box collapsed it to
               * content width, which read as a cramped «Поиск» pill. */}
              <Input
                size="500"
                variant="Background"
                radii="400"
                outlined
                autoFocus
                placeholder={t('Bots.add_to_chat_search_placeholder')}
                before={<Icon size="200" src={Icons.Search} />}
                onChange={handleInputChange}
              />
            </Box>
            <Box grow="Yes">
              {roomsToRender.length === 0 ? (
                <Box
                  style={{ padding: config.space.S700 }}
                  grow="Yes"
                  alignItems="Center"
                  justifyContent="Center"
                  direction="Column"
                  gap="100"
                >
                  <Text size="T300" align="Center" priority="300">
                    {result
                      ? t('Bots.add_to_chat_no_match')
                      : t('Bots.add_to_chat_empty', { name: preset.name })}
                  </Text>
                </Box>
              ) : (
                <Scroll size="300" hideTrack>
                  <div style={{ padding: config.space.S400, paddingRight: config.space.S200 }}>
                    {roomsToRender.map((roomId) => {
                      const room = mx.getRoom(roomId);
                      if (!room) return null;
                      const encrypted = room.hasEncryptionStateEvent();
                      const rowPending = pendingRoomId === roomId && inviting;
                      return (
                        <MenuItem
                          key={roomId}
                          as="button"
                          data-room-id={roomId}
                          onClick={handleRoomClick}
                          variant="Surface"
                          radii="400"
                          disabled={encrypted || inviting}
                          aria-disabled={encrypted || inviting}
                          before={
                            <Avatar size="200" radii="300">
                              <Text as="span" size="H6">
                                {nameInitials(room.name)}
                              </Text>
                            </Avatar>
                          }
                          after={
                            // eslint-disable-next-line no-nested-ternary
                            rowPending ? (
                              <Spinner size="200" variant="Secondary" />
                            ) : encrypted ? (
                              <Icon size="100" src={Icons.Lock} />
                            ) : undefined
                          }
                        >
                          <Box direction="Column" grow="Yes" style={{ minWidth: 0 }}>
                            <Text size="T300" truncate>
                              {room.name}
                            </Text>
                            {encrypted && (
                              <Text size="T200" priority="300" truncate>
                                {t('Bots.encrypted_room_disabled', { name: preset.name })}
                              </Text>
                            )}
                          </Box>
                        </MenuItem>
                      );
                    })}
                  </div>
                </Scroll>
              )}
            </Box>
            {inviteState.status === AsyncStatus.Error && (
              <>
                <Line size="300" />
                <Box
                  shrink="No"
                  alignItems="Center"
                  gap="200"
                  style={{ padding: config.space.S300 }}
                >
                  <Icon
                    size="100"
                    src={Icons.Warning}
                    filled
                    style={{ color: color.Critical.Main }}
                  />
                  <Text size="T200" style={{ color: color.Critical.Main }}>
                    {inviteState.error.message ||
                      t('Bots.add_to_chat_error', { name: preset.name })}
                  </Text>
                </Box>
              </>
            )}
          </Modal>
        </FocusTrap>
      </OverlayCenter>
    </Overlay>
  );
}
