/**
 * Message store helpers - delegates pure domain logic to `domains/chat/domain/`
 * and provides DB-dependent hydration/merge operations at the application level.
 */
import { Message, type MessageRevision } from '../../shared-kernel';
import { DatabaseService } from '../../infrastructure/persistence';
import { getMessageTimestamp, normaliseDeletedMessage } from '../../domains/chat/domain/rules/message.rules';
import type { InventoryItem } from '../../domains/chat/domain/rules/message-sync.rules';
import {
  computeMessageHeadHashFromMessage,
  getMessageRevision,
  shouldApplyIncomingRevision
} from '../../domains/chat/domain/rules/message-integrity.rules';
import { materializeMessageFromRevision, revisionBeatsMessage } from '../../domains/chat/domain/rules/message-revision.builder.rules';

// Re-export domain logic so existing callers keep working
export {
  getMessageTimestamp,
  getLatestTimestamp,
  normaliseDeletedMessage,
  canEditMessage
} from '../../domains/chat/domain/rules/message.rules';
export {
  INVENTORY_LIMIT,
  CHUNK_SIZE,
  SYNC_POLL_FAST_MS,
  SYNC_POLL_SLOW_MS,
  SYNC_TIMEOUT_MS,
  FULL_SYNC_LIMIT,
  chunkArray,
  findMissingIds
} from '../../domains/chat/domain/rules/message-sync.rules';
export type { InventoryItem } from '../../domains/chat/domain/rules/message-sync.rules';

/** Hydrates a single message with its reactions from the database. */
export async function hydrateMessage(
  msg: Message,
  _db: DatabaseService
): Promise<Message> {
  if (msg.isDeleted)
    return normaliseDeletedMessage(msg);

  return msg;
}

/** Hydrates an array of messages with their reactions. */
export async function hydrateMessages(
  messages: Message[],
  _db: DatabaseService
): Promise<Message[]> {
  return messages.map((msg) => msg.isDeleted ? normaliseDeletedMessage(msg) : msg);
}

/** Builds a sync inventory item from a message and its reaction count.
 *
 * Reactions are read from the already-hydrated `msg.reactions` array (the
 * persistence layer joins them in via `getMessages`), and attachment counts
 * only come from the in-memory override. We deliberately avoid per-message
 * DB lookups here so a whole-room inventory stays O(1) DB calls even when
 * the room contains tens of thousands of messages.
 */
export async function buildInventoryItem(
  msg: Message,
  _db: DatabaseService,
  attachmentCountOverride?: number
): Promise<InventoryItem> {
  const revision = getMessageRevision(msg);
  const headHash = msg.headHash ?? await computeMessageHeadHashFromMessage(msg, revision);

  if (msg.isDeleted) {
    return {
      id: msg.id,
      ts: getMessageTimestamp(msg),
      rc: 0,
      ac: 0,
      revision,
      headHash
    };
  }

  return {
    id: msg.id,
    ts: getMessageTimestamp(msg),
    rc: msg.reactions?.length ?? 0,
    ac: attachmentCountOverride ?? 0,
    revision,
    headHash
  };
}

/** Builds a local map of `{timestamp, reactionCount, attachmentCount}` keyed by message ID.
 *
 * As with {@link buildInventoryItem}, reactions come from the already-hydrated
 * `msg.reactions` array and attachment counts only come from the in-memory
 * override map.
 */
export async function buildLocalInventoryMap(
  messages: Message[],
  _db: DatabaseService,
  attachmentCountOverrides?: ReadonlyMap<string, number>
): Promise<Map<string, InventoryItem>> {
  const map = new Map<string, InventoryItem>();

  for (const msg of messages) {
    const item = await buildInventoryItem(
      msg,
      _db,
      attachmentCountOverrides?.get(msg.id)
    );

    map.set(msg.id, item);
  }

  return map;
}

/** Result of merging an incoming message into the local database. */
export interface MergeResult {
  message: Message;
  changed: boolean;
}

/**
 * Merges an incoming message into the local database.
 * Handles message upsert and reaction deduplication, then returns
 * the fully hydrated message alongside a `changed` flag.
 */
function shouldApplyIncomingMessage(incoming: Message, existing: Message | null): boolean {
  const incomingRevision = getMessageRevision(incoming);
  const existingRevision = getMessageRevision(existing ?? undefined);

  if (incoming.headHash) {
    const existingHeadHash = existing?.headHash
      ?? '';

    return shouldApplyIncomingRevision(
      incomingRevision,
      existingRevision,
      incoming.headHash,
      existingHeadHash
    );
  }

  const existingTs = existing ? getMessageTimestamp(existing) : -1;
  const incomingTs = getMessageTimestamp(incoming);
  const isDeletedStateNewer =
    !!existing &&
    incomingTs === existingTs &&
    incoming.isDeleted &&
    !existing.isDeleted;

  return !existing || incomingTs > existingTs || isDeletedStateNewer;
}

export async function mergeIncomingRevision(
  revision: MessageRevision,
  db: DatabaseService
): Promise<MergeResult> {
  const existing = await db.getMessageById(revision.messageId);

  if (!revisionBeatsMessage(revision, existing)) {
    if (!existing) {
      return {
        message: materializeMessageFromRevision(null, revision),
        changed: false
      };
    }

    return {
      message: normaliseDeletedMessage(existing),
      changed: false
    };
  }

  const message = materializeMessageFromRevision(existing, revision);

  await db.saveMessage(message);
  await db.saveMessageRevision(revision);

  if (message.isDeleted) {
    return {
      message: normaliseDeletedMessage(message),
      changed: true
    };
  }

  const reactions = await db.getReactionsForMessage(message.id);

  return {
    message: {
      ...message,
      reactions
    },
    changed: true
  };
}

export async function mergeIncomingMessage(
  incoming: Message,
  db: DatabaseService
): Promise<MergeResult> {
  const existing = await db.getMessageById(incoming.id);
  const isNewer = shouldApplyIncomingMessage(incoming, existing);

  if (isNewer) {
    const persisted = incoming.headHash
      ? incoming
      : {
        ...incoming,
        revision: getMessageRevision(incoming),
        headHash: await computeMessageHeadHashFromMessage(incoming, getMessageRevision(incoming))
      };

    await db.saveMessage(persisted);
  }

  // Persist incoming reactions (deduped by the DB layer)
  const incomingReactions = incoming.isDeleted ? [] : incoming.reactions ?? [];

  for (const reaction of incomingReactions) {
    await db.saveReaction(reaction);
  }

  const changed = isNewer || incomingReactions.length > 0;

  if (changed) {
    const baseMessage = isNewer ? incoming : existing;

    if (!baseMessage) {
      return { message: normaliseDeletedMessage(incoming),
        changed };
    }

    if (baseMessage.isDeleted) {
      return {
        message: normaliseDeletedMessage(baseMessage),
        changed
      };
    }

    const reactions = await db.getReactionsForMessage(incoming.id);

    return {
      message: { ...baseMessage,
        reactions },
      changed
    };
  }

  if (!existing) {
    return { message: normaliseDeletedMessage(incoming),
      changed: false };
  }

  return { message: normaliseDeletedMessage(existing),
    changed: false };
}
