import { and, desc, eq, gt, inArray, isNull, ne, or, sql } from "drizzle-orm";
import type Redis from "ioredis";
import { schema, withTenant } from "@slackwsh/data";
import { asEntityId, requireWorkspaceMembership, type EntityIdInput } from "./channels";
import { publishRoomEvent } from "./events-bus";

export interface SearchResult {
  message: typeof schema.messages.$inferSelect;
  channelName: string | null;
  authorName: string;
}

export interface MentionActivityResult {
  message: typeof schema.messages.$inferSelect;
  channelName: string | null;
  authorName: string;
  mentionType: string;
  readAt: string | null;
}

export interface UnreadMessageResult {
  message: typeof schema.messages.$inferSelect;
  channelName: string | null;
  channelType: string;
  authorName: string;
}

interface ParsedQuery {
  text: string;
  fromUsername?: string;
  inChannelName?: string;
}

/**
 * Message search (Feature 6.1: `from:` `in:` `has:` `before:` `after:`
 * `is:` modifiers — `from:` and `in:` implemented here, the rest deferred).
 * Postgres full-text via the GIN index in sql/search.sql, not Typesense
 * (ADR-007's primary path — not wired up yet, see PHASE3_STATUS.md).
 *
 * Scoped to channels the searching user is actually a member of via the
 * channel_members join — never trust a client-supplied channel name alone
 * to mean "searchable," membership is what's searchable.
 */
export function parseSearchQuery(raw: string): ParsedQuery {
  let text = raw;
  let fromUsername: string | undefined;
  let inChannelName: string | undefined;

  text = text.replace(/\bfrom:(\S+)/i, (_, v) => {
    fromUsername = v;
    return "";
  });
  text = text.replace(/\bin:(\S+)/i, (_, v) => {
    inChannelName = v.replace(/^#/, "");
    return "";
  });

  return { text: text.trim(), fromUsername, inChannelName };
}

export async function searchMessages(
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  rawQuery: string,
  limit = 30,
): Promise<SearchResult[]> {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  const parsed = parseSearchQuery(rawQuery);

  return withTenant({ workspaceId: wsId, userId }, async (tx) => {
    const conditions = [
      eq(schema.channelMembers.userId, userId),
      eq(schema.messages.workspaceId, wsId),
      sql`${schema.messages.deletedAt} is null`,
    ];

    if (parsed.fromUsername) {
      const [author] = await tx.select({ id: schema.users.id }).from(schema.users).where(eq(schema.users.username, parsed.fromUsername)).limit(1);
      // An unresolvable from: filters to nothing rather than being ignored —
      // silently dropping it would surface unrelated results for a typo'd
      // handle, which reads as "the modifier didn't work" either way, but
      // returning zero is the less misleading failure mode.
      // 0 is never a valid entity id (asEntityId requires positive ints).
      conditions.push(eq(schema.messages.authorId, author?.id ?? 0));
    }

    if (parsed.inChannelName) {
      const [channel] = await tx
        .select({ id: schema.channels.id })
        .from(schema.channels)
        .where(and(eq(schema.channels.workspaceId, wsId), eq(schema.channels.name, parsed.inChannelName)))
        .limit(1);
      conditions.push(eq(schema.messages.channelId, channel?.id ?? 0));
    }

    if (parsed.text) {
      conditions.push(sql`to_tsvector('simple', ${schema.messages.text}) @@ plainto_tsquery('simple', ${parsed.text})`);
    }

    const rows = await tx
      .select({ message: schema.messages, channelName: schema.channels.name, authorName: schema.users.name })
      .from(schema.messages)
      .innerJoin(schema.channelMembers, eq(schema.channelMembers.channelId, schema.messages.channelId))
      .innerJoin(schema.channels, eq(schema.channels.id, schema.messages.channelId))
      .innerJoin(schema.users, eq(schema.users.id, schema.messages.authorId))
      .where(and(...conditions))
      .orderBy(desc(schema.messages.seq))
      .limit(limit);

    return rows;
  });
}

export async function listMentionActivity(
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  limit = 50,
): Promise<MentionActivityResult[]> {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);

  return withTenant({ workspaceId: wsId, userId }, async (tx) => {
    const rows = await tx
      .select({
        message: schema.messages,
        channelName: schema.channels.name,
        authorName: schema.users.name,
        mentionType: schema.messageMentions.targetType,
        readAt: schema.activityReads.readAt,
      })
      .from(schema.messageMentions)
      .innerJoin(schema.messages, eq(schema.messages.id, schema.messageMentions.messageId))
      .innerJoin(schema.channels, eq(schema.channels.id, schema.messages.channelId))
      .innerJoin(schema.channelMembers, eq(schema.channelMembers.channelId, schema.messages.channelId))
      .innerJoin(schema.users, eq(schema.users.id, schema.messages.authorId))
      .leftJoin(
        schema.activityReads,
        and(
          eq(schema.activityReads.messageId, schema.messages.id),
          eq(schema.activityReads.userId, userId),
          eq(schema.activityReads.workspaceId, wsId),
        ),
      )
      .where(
        and(
          eq(schema.messages.workspaceId, wsId),
          eq(schema.channelMembers.userId, userId),
          isNull(schema.messages.deletedAt),
          or(
            and(eq(schema.messageMentions.targetType, "user"), eq(schema.messageMentions.targetId, userId)),
            inArray(schema.messageMentions.targetType, ["here", "channel", "everyone"]),
          ),
        ),
      )
      .orderBy(desc(schema.messages.createdAt))
      .limit(limit * 2);

    // One message may contain both @user and @here. Activity is message-based,
    // so render/count it once and prefer the direct-mention label when present.
    const byMessage = new Map<number, MentionActivityResult>();
    for (const row of rows) {
      const normalized = { ...row, readAt: row.readAt?.toISOString() ?? null };
      const existing = byMessage.get(row.message.id);
      if (!existing || (row.mentionType === "user" && existing.mentionType !== "user")) {
        byMessage.set(row.message.id, normalized);
      }
    }
    return [...byMessage.values()].slice(0, limit);
  });
}

/** Persist Activity/Mentions read state for every device owned by the user. */
export async function setActivityReadState(
  redis: Redis,
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  messageIds: EntityIdInput[],
  read: boolean,
) {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  const requested = [...new Set(messageIds.map(asEntityId))];
  const accessibleIds = await withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    const rows = await tx
      .select({ id: schema.messages.id })
      .from(schema.messages)
      .innerJoin(
        schema.channelMembers,
        and(eq(schema.channelMembers.channelId, schema.messages.channelId), eq(schema.channelMembers.userId, userId)),
      )
      .where(and(eq(schema.messages.workspaceId, wsId), inArray(schema.messages.id, requested)));
    const ids = [...new Set(rows.map((row) => row.id))];
    if (ids.length === 0) return ids;
    if (read) {
      await tx
        .insert(schema.activityReads)
        .values(ids.map((messageId) => ({ workspaceId: wsId, userId, messageId, readAt: new Date() })))
        .onConflictDoUpdate({
          target: [schema.activityReads.userId, schema.activityReads.messageId],
          set: { readAt: new Date() },
        });
    } else {
      await tx
        .delete(schema.activityReads)
        .where(
          and(
            eq(schema.activityReads.workspaceId, wsId),
            eq(schema.activityReads.userId, userId),
            inArray(schema.activityReads.messageId, ids),
          ),
        );
    }
    return ids;
  });

  if (accessibleIds.length > 0) {
    await publishRoomEvent(redis, {
      room: `u:${userId}`,
      event: { type: "activity:read-updated", payload: { workspaceId: wsId, messageIds: accessibleIds, read } },
    });
  }
  return accessibleIds;
}

/** Top-level unread messages across every room the requesting user can read. */
export async function listUnreadMessages(
  workspaceId: EntityIdInput,
  actorUserId: EntityIdInput,
  limit = 100,
): Promise<UnreadMessageResult[]> {
  const wsId = asEntityId(workspaceId);
  const userId = asEntityId(actorUserId);
  return withTenant({ workspaceId: wsId, userId }, async (tx) => {
    await requireWorkspaceMembership(tx, wsId, userId);
    return tx
      .select({
        message: schema.messages,
        channelName: schema.channels.name,
        channelType: schema.channels.type,
        authorName: schema.users.name,
      })
      .from(schema.channelMembers)
      .innerJoin(schema.channels, eq(schema.channels.id, schema.channelMembers.channelId))
      .innerJoin(
        schema.messages,
        and(
          eq(schema.messages.channelId, schema.channelMembers.channelId),
          gt(schema.messages.seq, schema.channelMembers.lastReadSeq),
        ),
      )
      .innerJoin(schema.users, eq(schema.users.id, schema.messages.authorId))
      .where(
        and(
          eq(schema.channelMembers.userId, userId),
          eq(schema.channels.workspaceId, wsId),
          eq(schema.messages.workspaceId, wsId),
          or(isNull(schema.messages.parentId), eq(schema.messages.isBroadcast, true)),
          isNull(schema.messages.deletedAt),
          ne(schema.messages.authorId, userId),
        ),
      )
      .orderBy(desc(schema.messages.createdAt))
      .limit(Math.max(1, Math.min(200, limit)));
  });
}
