import {
  IEventWithRoomId,
  IResultContext,
  ISearchRequestBody,
  ISearchResponse,
  ISearchResult,
  MatrixClient,
  MatrixEvent,
  Room,
  SearchOrderBy,
} from 'matrix-js-sdk';
import { useCallback } from 'react';
import { useMatrixClient } from '../../hooks/useMatrixClient';

export type ResultItem = {
  rank: number;
  event: IEventWithRoomId;
  context: IResultContext;
};

export type ResultGroup = {
  roomId: string;
  items: ResultItem[];
};

export type SearchResult = {
  nextToken?: string;
  highlights: string[];
  groups: ResultGroup[];
};

const EMPTY_CONTEXT: IResultContext = {
  events_before: [],
  events_after: [],
  profile_info: {},
};

const groupSearchResult = (results: ISearchResult[]): ResultGroup[] => {
  const groups: ResultGroup[] = [];

  results.forEach((item) => {
    const roomId = item.result.room_id;
    const resultItem: ResultItem = {
      rank: item.rank,
      event: item.result,
      context: item.context,
    };

    const lastAddedGroup: ResultGroup | undefined = groups[groups.length - 1];
    if (lastAddedGroup && roomId === lastAddedGroup.roomId) {
      lastAddedGroup.items.push(resultItem);
      return;
    }
    groups.push({
      roomId,
      items: [resultItem],
    });
  });

  return groups;
};

const parseSearchResult = (result: ISearchResponse): SearchResult => {
  const roomEvents = result.search_categories.room_events;

  return {
    nextToken: roomEvents?.next_batch,
    highlights: roomEvents?.highlights ?? [],
    groups: groupSearchResult(roomEvents?.results ?? []),
  };
};

const eventToSearchEvent = (event: MatrixEvent, roomId: string): IEventWithRoomId | undefined => {
  const eventId = event.getId();
  if (!eventId) return undefined;

  const content = event.getClearContent() ?? event.getContent();
  return {
    event_id: eventId,
    type: event.getWireType() === 'm.room.encrypted' ? 'm.room.message' : event.getType(),
    sender: event.getSender() ?? '',
    origin_server_ts: event.getTs(),
    content,
    room_id: roomId,
    unsigned: event.getUnsigned(),
  };
};

const getSearchableBody = (event: MatrixEvent): string | undefined => {
  if (event.isRedacted()) return undefined;

  // After decryption, clear content is available even if wire type was encrypted
  const content = event.getClearContent() ?? event.getContent();
  if (!content || typeof content !== 'object') return undefined;

  // Skip still-encrypted payloads
  if (event.isEncrypted() && !event.isDecryptionFailure() && !event.getClearContent()) {
    return undefined;
  }

  const msgType = content.msgtype;
  if (msgType && msgType !== 'm.text' && msgType !== 'm.notice' && msgType !== 'm.emote') {
    // Still allow filename / body on media
  }

  const body = typeof content.body === 'string' ? content.body : undefined;
  const formatted =
    typeof content.formatted_body === 'string' ? content.formatted_body : undefined;
  return [body, formatted].filter(Boolean).join('\n') || undefined;
};

const highlightsFromTerm = (term: string): string[] =>
  term
    .split(/\s+/)
    .map((part) => part.trim())
    .filter((part) => part.length > 1);

/**
 * Search decrypted timeline events already loaded for a room.
 * Server-side search cannot match E2EE message bodies.
 */
const searchLocalRoomTimeline = (room: Room, term: string): ResultItem[] => {
  const needle = term.trim().toLowerCase();
  if (!needle) return [];

  const events = room.getLiveTimeline().getEvents();
  const matches: ResultItem[] = [];

  for (let i = events.length - 1; i >= 0; i -= 1) {
    const event = events[i];
    const haystack = getSearchableBody(event);
    if (!haystack || !haystack.toLowerCase().includes(needle)) continue;

    const searchEvent = eventToSearchEvent(event, room.roomId);
    if (!searchEvent) continue;

    matches.push({
      rank: 1,
      event: searchEvent,
      context: EMPTY_CONTEXT,
    });
  }

  return matches;
};

const isEncryptedRoom = (mx: MatrixClient, roomId: string): boolean => {
  const room = mx.getRoom(roomId);
  return !!room?.hasEncryptionStateEvent();
};

const searchLocalRooms = (mx: MatrixClient, roomIds: string[], term: string): SearchResult => {
  const groups: ResultGroup[] = [];

  roomIds.forEach((roomId) => {
    const room = mx.getRoom(roomId);
    if (!room) return;
    const items = searchLocalRoomTimeline(room, term);
    if (items.length > 0) {
      groups.push({ roomId, items });
    }
  });

  return {
    highlights: highlightsFromTerm(term),
    groups,
  };
};

export type MessageSearchParams = {
  term?: string;
  order?: string;
  rooms?: string[];
  senders?: string[];
};

export const useMessageSearch = (params: MessageSearchParams) => {
  const mx = useMatrixClient();
  const { term, order, rooms, senders } = params;

  const searchMessages = useCallback(
    async (nextBatch?: string) => {
      if (!term) {
        return {
          highlights: [],
          groups: [],
        };
      }

      const scopedRooms = rooms?.filter(Boolean) ?? [];
      const encryptedScoped =
        scopedRooms.length > 0 && scopedRooms.every((roomId) => isEncryptedRoom(mx, roomId));

      // Encrypted-only scope: server cannot see bodies — search loaded timeline locally
      if (encryptedScoped && !nextBatch) {
        return searchLocalRooms(mx, scopedRooms, term);
      }

      const limit = 20;
      const requestBody: ISearchRequestBody = {
        search_categories: {
          room_events: {
            event_context: {
              before_limit: 0,
              after_limit: 0,
              include_profile: false,
            },
            filter: {
              limit,
              rooms,
              senders,
            },
            include_state: false,
            order_by: order as SearchOrderBy.Recent,
            search_term: term,
          },
        },
      };

      try {
        const r = await mx.search({
          body: requestBody,
          next_batch: nextBatch === '' ? undefined : nextBatch,
        });
        const parsed = parseSearchResult(r);

        if (
          parsed.groups.length === 0 &&
          !nextBatch &&
          scopedRooms.length > 0 &&
          scopedRooms.some((roomId) => isEncryptedRoom(mx, roomId))
        ) {
          return searchLocalRooms(
            mx,
            scopedRooms.filter((id) => isEncryptedRoom(mx, id)),
            term
          );
        }

        return parsed;
      } catch {
        if (!nextBatch && scopedRooms.length > 0) {
          return searchLocalRooms(mx, scopedRooms, term);
        }
        throw new Error('Message search failed');
      }
    },
    [mx, term, order, rooms, senders]
  );

  return searchMessages;
};
