import { Inject, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
import { MESSAGE_SEARCH_PORT, type MessageSearchPort, type SearchDoc, type SearchHit } from './message-search.port';

/**
 * Message search. The permission fence lives HERE: a query only ever runs against the caller's own
 * scope AND the set of threads the caller currently belongs to (resolved live from participant
 * rows, so membership changes are reflected immediately). The engine never sees a thread the caller
 * isn't in. Text is drawn from the message's TEXT part; every conversation kind (chat, mail, sms…)
 * is a message with parts, so all are searchable through one index.
 */
@Injectable()
export class SearchService {
  constructor(
    @Inject(MESSAGE_SEARCH_PORT) private readonly engine: MessageSearchPort,
    private readonly prisma: PrismaService,
    private readonly actors: ActorResolver,
  ) {}

  async search(principal: MessagePrincipal, opts: { query: string; limit?: number }): Promise<SearchHit[]> {
    const q = (opts.query ?? '').trim();
    if (!q || !this.engine.ready()) return [];
    const scope = await this.actors.findScope(principal);
    if (!scope) return [];
    const actor = await this.actors.resolveActor(scope.id, principal);
    const memberships = await this.prisma.iiosThreadParticipant.findMany({ where: { actorId: actor.id }, select: { threadId: true } });
    const threadIds = memberships.map((m) => m.threadId);
    if (threadIds.length === 0) return [];
    return this.engine.search(scope.id, threadIds, q, Math.min(opts.limit ?? 20, 50));
  }

  /** Build + index the search doc for one interaction (called by the projector on message.sent). */
  async indexInteraction(interactionId: string): Promise<void> {
    if (!this.engine.ready()) return;
    const doc = await this.buildDoc(interactionId);
    if (doc) await this.engine.index([doc]);
  }

  /** Remove an interaction from the index (retention / redaction hook). */
  async removeInteraction(interactionId: string): Promise<void> {
    if (this.engine.ready()) await this.engine.remove([interactionId]);
  }

  /** Backfill: (re)index every text message, optionally for one scope. Returns the count indexed. */
  async reindexAll(scopeId?: string, batch = 500): Promise<number> {
    if (!this.engine.ready()) return 0;
    const interactions = await this.prisma.iiosInteraction.findMany({
      where: { ...(scopeId ? { scopeId } : {}), threadId: { not: null }, parts: { some: { kind: 'TEXT' } } },
      select: { id: true },
    });
    let indexed = 0;
    for (let i = 0; i < interactions.length; i += batch) {
      const docs = (await Promise.all(interactions.slice(i, i + batch).map((x) => this.buildDoc(x.id)))).filter((d): d is SearchDoc => d !== null);
      if (docs.length > 0) {
        await this.engine.index(docs);
        indexed += docs.length;
      }
    }
    return indexed;
  }

  private async buildDoc(interactionId: string): Promise<SearchDoc | null> {
    const i = await this.prisma.iiosInteraction.findUnique({
      where: { id: interactionId },
      include: { parts: { where: { kind: 'TEXT' }, take: 1 }, thread: true },
    });
    if (!i || !i.threadId || !i.thread) return null;
    const text = i.parts[0]?.bodyText?.trim();
    if (!text) return null; // nothing searchable
    const meta = (i.thread.metadata as { source?: string } | null) ?? {};
    return {
      id: i.id,
      scopeId: i.scopeId,
      threadId: i.threadId,
      text,
      subject: i.thread.subject ?? null,
      source: meta.source ?? null,
      actorId: i.actorId ?? null,
      at: i.occurredAt.getTime(),
    };
  }
}
