import { BaseRepository } from "@edr/api-common";
import { SupportAuthorRole } from "@edr/types";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";

import { SupportConversation } from "./entities/support-conversation.entity";
import { SupportMessage } from "./entities/support-message.entity";

@Injectable()
export class SupportMessageRepository extends BaseRepository<SupportMessage> {
  constructor(
    @InjectRepository(SupportMessage)
    repo: Repository<SupportMessage>,
  ) {
    super(repo);
  }

  /**
   * One page of a thread, walking backwards from newest.
   *
   * Returns **newest-first** and takes one row more than asked, so the caller
   * can tell "there is another page" from "this page happened to be full"
   * without a second COUNT. The caller trims the probe row and flips the page
   * to oldest-first for rendering.
   *
   * Rides IDX_SUPPORT_MSG_CONV_CREATED (conversation_id, created_at); the id in
   * the keyset is a tiebreak only and doesn't need its own index.
   */
  async listByConversation(
    conversationId: string,
    limit: number,
    beforeId?: string,
  ): Promise<SupportMessage[]> {
    const qb = this.repository
      .createQueryBuilder("m")
      .where("m.conversation_id = :conversationId", { conversationId })
      // createQueryBuilder bypasses TypeORM's soft-delete filter, unlike find().
      .andWhere("m.deleted_at IS NULL");

    if (beforeId) {
      // Row-value comparison: strictly older than the cursor row in
      // (created_at, id) order.
      //
      // The cursor's timestamp is read back from the row itself rather than
      // passed in. `created_at` is timestamptz(6) but a JS Date only holds
      // milliseconds, so a timestamp that made the round-trip through the API
      // would arrive truncated — and `.254100 < .254000` is false, so every row
      // sharing the cursor's millisecond but earlier within it would be skipped
      // on every page, permanently. Postgres compares the stored values at full
      // precision instead.
      qb.andWhere(
        `(m.created_at, m.id) < (
           SELECT c.created_at, c.id
           FROM freight.support_messages c
           WHERE c.id = :cursorId
         )`,
        { cursorId: beforeId },
      );
    }

    return qb
      .orderBy("m.created_at", "DESC")
      .addOrderBy("m.id", "DESC")
      .take(limit + 1)
      .getMany();
  }

  /**
   * Unread message counts per conversation *for one side*: messages authored by
   * the other role that are newer than the side's read cursor. Returns a map of
   * conversationId → count (conversations with 0 unread are absent).
   */
  async unreadCountsByConversation(
    conversationIds: string[],
    mySide: SupportAuthorRole,
  ): Promise<Map<string, number>> {
    if (conversationIds.length === 0) return new Map();
    const rows = await this.baseUnreadQuery(mySide)
      .select("m.conversation_id", "conversationId")
      .addSelect("COUNT(*)", "count")
      .andWhere("m.conversation_id IN (:...ids)", { ids: conversationIds })
      .groupBy("m.conversation_id")
      .getRawMany<{ conversationId: string; count: string }>();
    return new Map(rows.map((r) => [r.conversationId, Number(r.count)]));
  }

  /** Number of distinct conversations with at least one unread message for the side. */
  async countUnreadConversations(
    mySide: SupportAuthorRole,
    companyId?: string,
  ): Promise<number> {
    const qb = this.baseUnreadQuery(mySide).select(
      "COUNT(DISTINCT m.conversation_id)",
      "count",
    );
    if (companyId) {
      qb.andWhere("c.company_id = :companyId", { companyId });
    }
    const row = await qb.getRawOne<{ count: string }>();
    return Number(row?.count ?? 0);
  }

  /**
   * Base query for "unread for `mySide`": join the conversation, keep only
   * messages from the opposite role that are newer than the side's read cursor.
   */
  private baseUnreadQuery(mySide: SupportAuthorRole) {
    const otherRole =
      mySide === SupportAuthorRole.CUSTOMER
        ? SupportAuthorRole.AGENT
        : SupportAuthorRole.CUSTOMER;
    const cursorCol =
      mySide === SupportAuthorRole.CUSTOMER
        ? "c.customer_last_read_at"
        : "c.agent_last_read_at";
    return this.repository
      .createQueryBuilder("m")
      .innerJoin(SupportConversation, "c", "c.id = m.conversation_id")
      .where("m.deleted_at IS NULL")
      .andWhere("m.author_role = :otherRole", { otherRole })
      .andWhere(`(${cursorCol} IS NULL OR m.created_at > ${cursorCol})`);
  }
}
