import { useEffect, useState } from 'react';
import {
  ClientEvent,
  EventType,
  MatrixClient,
  MatrixEvent,
  Room,
  RoomEvent,
  RoomStateEvent,
} from 'matrix-js-sdk';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { Membership } from '../../../types/matrix/room';
import { removeRoomIdFromMDirect } from '../../utils/matrix';
import { isBotControlRoom, isBridgeStateEvent, isPortalRoomForOtherBridge } from './room';
import type { BotPreset } from './catalog';

// Discriminated union over the lifecycle of a bot DM. Mount-eligible only at
// `kind === 'ready'`; everything else is a UI state with a different CTA.
//
// Using getDMRoomFor here would be wrong: it returns only self-Joined active
// 1:1 rooms, so self-invite and bot-kicked rooms would silently degrade to
// `none` and the «Connect» button would create a duplicate control DM.
// Instead we read m.direct[preset.mxid] raw plus legacy untagged control rooms,
// walk the rooms, and classify by (myMembership, botMembership) tuple.
export type BotRoomState =
  | { kind: 'none' }
  | { kind: 'self-invite'; room: Room }
  | { kind: 'bot-invite'; room: Room }
  | { kind: 'bot-kicked'; room: Room }
  | { kind: 'unsafe-membership'; room: Room }
  | { kind: 'ready'; room: Room };

type MDirectMap = Record<string, string[]>;

const classifyRoom = (
  mx: MatrixClient,
  room: Room,
  preset: BotPreset
): BotRoomState | undefined => {
  if (isPortalRoomForOtherBridge(room, preset.mxid)) return undefined;

  const my = room.getMyMembership();
  const bot = room.getMember(preset.mxid)?.membership;

  // A bot control room is mount-ready only while active membership is exactly
  // self + bot. Extra joined/invited members block the bot surface before any
  // sensitive commands can be read or sent.
  const activeMembers = room
    .getMembers()
    .filter((m) => m.membership === Membership.Join || m.membership === Membership.Invite);
  const allowedMembers = new Set([mx.getSafeUserId(), preset.mxid]);
  if (activeMembers.some((m) => !allowedMembers.has(m.userId))) {
    return { kind: 'unsafe-membership', room };
  }

  if (my === Membership.Invite) return { kind: 'self-invite', room };
  if (my !== Membership.Join) return undefined;
  if (bot === Membership.Invite) return { kind: 'bot-invite', room };
  if (bot === Membership.Leave || bot === Membership.Ban) return { kind: 'bot-kicked', room };
  if (bot === Membership.Join && activeMembers.length === 2) return { kind: 'ready', room };
  return undefined;
};

// Order: prefer `ready` first, then live invites, then stale states. Within a
// kind, sort by recent activity (last room timestamp). Without this, walking
// `m.direct[mxid]` in JSON order can pin the UI on a Leave-state stale room
// while a fresh Join-state room sits later in the array.
const STATE_PRIORITY: Record<BotRoomState['kind'], number> = {
  ready: 0,
  'self-invite': 1,
  'bot-invite': 2,
  'unsafe-membership': 3,
  'bot-kicked': 4,
  none: 5,
};

const DIRECT_ACCOUNT_DATA = EventType.Direct;

const getBotRoomIds = (mx: MatrixClient, preset: BotPreset, mDirect: MDirectMap): string[] => {
  const ids = new Set(mDirect[preset.mxid] ?? []);
  mx.getRooms().forEach((room) => {
    if (isBotControlRoom(mx, room, preset)) {
      ids.add(room.roomId);
    }
  });
  return Array.from(ids);
};

const computeBotRoomState = (mx: MatrixClient, preset: BotPreset): BotRoomState => {
  const mDirectsEvent = mx.getAccountData(DIRECT_ACCOUNT_DATA);
  const mDirect = (mDirectsEvent?.getContent() ?? {}) as MDirectMap;
  const roomIds = getBotRoomIds(mx, preset, mDirect);

  // Sweep dead pointers (purged rooms, my=Leave/Ban) from m.direct so the
  // array doesn't grow unbounded across reconnect cycles. Fire-and-forget;
  // the next reactive tick will recompute against the cleaned account data.
  const deadIds: string[] = [];
  const candidates: BotRoomState[] = [];
  roomIds.forEach((roomId) => {
    const room = mx.getRoom(roomId);
    if (!room) {
      deadIds.push(roomId);
      return;
    }
    const my = room.getMyMembership();
    if (my === Membership.Leave || my === Membership.Ban) {
      deadIds.push(roomId);
      return;
    }
    const classified = classifyRoom(mx, room, preset);
    if (classified) candidates.push(classified);
  });

  if (deadIds.length > 0) {
    Promise.all(deadIds.map((id) => removeRoomIdFromMDirect(mx, id))).catch(() => undefined);
  }

  if (candidates.length === 0) return { kind: 'none' };

  candidates.sort((a, b) => {
    const byKind = STATE_PRIORITY[a.kind] - STATE_PRIORITY[b.kind];
    if (byKind !== 0) return byKind;
    const aRoom = 'room' in a ? a.room : undefined;
    const bRoom = 'room' in b ? b.room : undefined;
    const aTs = aRoom?.getLastActiveTimestamp() ?? 0;
    const bTs = bRoom?.getLastActiveTimestamp() ?? 0;
    return bTs - aTs;
  });

  return candidates[0];
};

// Subscribe to client-level events that can change the (room set × membership)
// tuple. Listeners are gated by `roomId` membership of the bot's room set so
// we don't recompute on every member event in every room of the account.
//
//   - ClientEvent.AccountData       — m.direct mutated (createRoom flow)
//   - ClientEvent.Room              — first sync of a freshly-created room
//   - RoomEvent.MyMembership        — accept/decline of self-invite
//   - RoomStateEvent.Members        — bot accepted invite, was kicked, etc.
//   - RoomStateEvent.Events         — late m.bridge state demotes a portal
//                                     room out of the Robots control surface
//
// We snapshot the «interesting» room id set on each recompute so the listeners
// know which room ids to react to. Bot `m.direct` arrays are tiny (≤ a couple
// of entries per bot) so the set lookup is O(1) effectively.
export const useBotRoom = (preset: BotPreset | undefined): BotRoomState => {
  const mx = useMatrixClient();
  const [state, setState] = useState<BotRoomState>(() =>
    preset ? computeBotRoomState(mx, preset) : { kind: 'none' }
  );

  useEffect(() => {
    if (!preset) {
      setState({ kind: 'none' });
      return undefined;
    }

    const interestingRoomIds = new Set<string>();
    const refreshInterestingRoomIds = () => {
      const mDirect = (mx.getAccountData(DIRECT_ACCOUNT_DATA)?.getContent() ?? {}) as MDirectMap;
      interestingRoomIds.clear();
      getBotRoomIds(mx, preset, mDirect).forEach((id) => interestingRoomIds.add(id));
    };

    const reEvaluate = () => {
      refreshInterestingRoomIds();
      setState(computeBotRoomState(mx, preset));
    };
    reEvaluate();

    const onAccountData = (ev: MatrixEvent) => {
      if (ev.getType() === DIRECT_ACCOUNT_DATA) reEvaluate();
    };
    const onRoom = (room: Room) => {
      // A freshly-created bot DM may not be in m.direct yet — recompute always.
      // Otherwise gate by mxid membership so we ignore unrelated room events.
      if (interestingRoomIds.has(room.roomId) || room.getMember(preset.mxid)) {
        reEvaluate();
      }
    };
    const onMyMembership = (room: Room) => {
      if (interestingRoomIds.has(room.roomId) || room.getMember(preset.mxid)) reEvaluate();
    };
    const onMembers = (ev: MatrixEvent) => {
      const roomId = ev.getRoomId();
      if (roomId && (interestingRoomIds.has(roomId) || ev.getStateKey() === preset.mxid)) {
        reEvaluate();
      }
    };
    const onStateEvent = (ev: MatrixEvent) => {
      if (!isBridgeStateEvent(ev)) return;
      const roomId = ev.getRoomId();
      const room = roomId ? mx.getRoom(roomId) : undefined;
      if (roomId && (interestingRoomIds.has(roomId) || room?.getMember(preset.mxid))) {
        reEvaluate();
      }
    };

    mx.on(ClientEvent.AccountData, onAccountData);
    mx.on(ClientEvent.Room, onRoom);
    mx.on(RoomEvent.MyMembership, onMyMembership);
    mx.on(RoomStateEvent.Members, onMembers);
    mx.on(RoomStateEvent.Events, onStateEvent);

    return () => {
      mx.removeListener(ClientEvent.AccountData, onAccountData);
      mx.removeListener(ClientEvent.Room, onRoom);
      mx.removeListener(RoomEvent.MyMembership, onMyMembership);
      mx.removeListener(RoomStateEvent.Members, onMembers);
      mx.removeListener(RoomStateEvent.Events, onStateEvent);
    };
  }, [mx, preset]);

  return state;
};
