import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
import { Membership, StateEvent } from '../../../types/matrix/room';
import { getStateEvents, isRoom } from '../../utils/room';
import type { BotPreset } from './catalog';

const ACTIVE_MEMBERSHIPS = new Set<string>([Membership.Join, Membership.Invite]);

export const isBridgeStateEvent = (ev: MatrixEvent): boolean => {
  const type = ev.getType();
  return type === StateEvent.RoomBridge || type === StateEvent.RoomBridgeUnstable;
};

// A room is a "portal" (Telegram/Signal/etc. mirror chat) if it carries an
// m.bridge / uk.half-shot.bridge state event whose state_key is NOT our
// control bot. Bridge BOTS write m.bridge into their OWN control DMs too,
// keyed by the bot mxid — those are management rooms, not portals, and the
// Robots tab must own them. The plain `isBridgedRoom` helper in utils/room
// can't make this distinction (it has no concept of "our bot"); it stays
// correct for DmCallButton's call-button suppression but is too coarse for
// the bot ownership question. Single source of truth — both useBotRoom's
// classifyRoom and BotWidgetDriver.isSafeBotWidgetRoom delegate here.
export const isPortalRoomForOtherBridge = (room: Room, controlBotMxid: string): boolean => {
  const events = [
    ...getStateEvents(room, StateEvent.RoomBridge),
    ...getStateEvents(room, StateEvent.RoomBridgeUnstable),
  ];
  return events.some((ev) => ev.getStateKey() !== controlBotMxid);
};

export const isBotControlRoom = (mx: MatrixClient, room: Room, preset: BotPreset): boolean => {
  if (!isRoom(room) || isPortalRoomForOtherBridge(room, preset.mxid)) return false;

  const myUserId = mx.getUserId();
  if (!myUserId) return false;

  const myMembership = room.getMyMembership();
  if (!ACTIVE_MEMBERSHIPS.has(myMembership)) return false;

  const botMember = room.getMember(preset.mxid);
  if (
    !botMember ||
    botMember.membership === undefined ||
    !ACTIVE_MEMBERSHIPS.has(botMember.membership)
  ) {
    return false;
  }

  // A control DM is exactly the two of us, nobody else. Decide that from the room
  // SUMMARY counts — NOT room.getMembers(): under Matrix lazy member-loading
  // getMembers() returns only the members synced so far (heroes + recent senders
  // + me), so a larger group whose loaded subset happens to be {me, bot} gets
  // misclassified as the control DM. That bites a chat which began as a 1:1 with
  // the bot (and still carries the sticky m.direct tag) but later gained people:
  // it would wrongly drop out of the Direct tab (useDirectRooms excludes control
  // DMs) and resurface only as a bot DM. getJoinedMemberCount()/
  // getInvitedMemberCount() come from the sync summary and stay accurate without
  // loading every member — the same source useAutoDirectSync trusts for its 1:1 test.
  return room.getJoinedMemberCount() + room.getInvitedMemberCount() === 2;
};

export const isCatalogBotControlRoom = (
  mx: MatrixClient,
  room: Room,
  presets: readonly BotPreset[]
): boolean => presets.some((preset) => isBotControlRoom(mx, room, preset));

// How long a bridge-ghost invite stays hidden from the Direct invite list
// while we expect the bridge's double puppet to auto-accept it. Long enough
// to cover a busy initial chat sync; short enough that a broken double
// puppet leaves the user a manual Accept button instead of a black hole.
export const BRIDGE_GHOST_INVITE_GRACE_MS = 2 * 60 * 1000;

// «Is this pending invite from a bridge ghost puppet of a catalog bot?»
// mautrix ghost mxids follow the `<network>_<remote id>` localpart
// convention and the catalog bot ids ARE the network slugs (telegram /
// whatsapp / discord — config.json is the trusted source), so the prefix
// match needs no client-side knowledge beyond the operator config. Invite
// rooms carry only stripped state — m.bridge is NOT in it — which is why
// this matches on the inviter mxid instead of `isBridgedRoom`.
//
// Used to suppress the invite-row flicker during bridge sync: the bridge's
// double puppet auto-accepts portal invites within seconds, and rendering
// Accept/Decline for something that resolves itself reads as UI noise. Only
// FRESH invites (younger than the grace window) are hidden — if auto-accept
// is broken, the invite resurfaces with manual controls.
export const isFreshBridgeGhostInvite = (
  mx: MatrixClient,
  room: Room,
  presets: readonly BotPreset[]
): boolean => {
  const myUserId = mx.getUserId();
  if (!myUserId) return false;
  const memberEvent = room.getMember(myUserId)?.events.member;
  const inviter = memberEvent?.getSender();
  if (!inviter) return false;
  const ts = memberEvent?.getTs() ?? 0;
  if (Date.now() - ts > BRIDGE_GHOST_INVITE_GRACE_MS) return false;
  return presets.some((preset) => {
    const botServer = preset.mxid.slice(preset.mxid.indexOf(':'));
    return inviter.startsWith(`@${preset.id}_`) && inviter.endsWith(botServer);
  });
};

// Returns the BotPreset that owns this room, or undefined if the room is
// not a bot control DM. Used by the RoomMenu's «Show widget» item to
// know which `/bots/:id` route to navigate to. Wraps the same matcher as
// `isCatalogBotControlRoom`.
export const findBotPresetForRoom = (
  mx: MatrixClient,
  room: Room,
  presets: readonly BotPreset[]
): BotPreset | undefined => presets.find((preset) => isBotControlRoom(mx, room, preset));
