// Per-thread notification modes (P4-1). Stored in the
// `io.lotus.thread_notifications` account data event. The functions in this
// module are PURE — they never touch React or matrix-js-sdk objects so they
// can be unit-tested in isolation and reused by the pipeline/UI agents.

export enum ThreadNotificationMode {
  Default = 'default',
  All = 'all',
  MentionsOnly = 'mentions',
  Mute = 'mute',
}

export type ThreadDefaultBehavior = 'all' | 'participating';

export type ThreadNotifyDecision = 'loud' | 'notify' | 'none';

export type ThreadNotificationEntry = {
  mode: Exclude<ThreadNotificationMode, ThreadNotificationMode.Default>;
  ts: number;
};

export type ThreadNotificationsContent = {
  default?: ThreadDefaultBehavior;
  rooms?: Record<string, Record<string, ThreadNotificationEntry>>;
};

// DEFAULT behavior when the user has not chosen a global default. Fixed to
// 'participating': notify only if the current user participated in the thread
// or the reply mentions them.
export const THREAD_NOTIFICATIONS_FALLBACK_BEHAVIOR: ThreadDefaultBehavior = 'participating';

// Entries older than this are pruned on write to keep account data bounded.
const PRUNE_MAX_AGE_MS = 180 * 24 * 60 * 60 * 1000;

// Maximum stored entries per room; oldest are evicted first.
const PRUNE_MAX_ENTRIES_PER_ROOM = 200;

const STORED_MODES: ReadonlySet<string> = new Set([
  ThreadNotificationMode.All,
  ThreadNotificationMode.MentionsOnly,
  ThreadNotificationMode.Mute,
]);

const isStoredMode = (
  value: unknown,
): value is Exclude<ThreadNotificationMode, ThreadNotificationMode.Default> =>
  typeof value === 'string' && STORED_MODES.has(value);

const readEntry = (
  content: ThreadNotificationsContent | undefined,
  roomId: string,
  threadRootId: string,
): ThreadNotificationEntry | undefined => {
  const entry = content?.rooms?.[roomId]?.[threadRootId];
  if (!entry || typeof entry !== 'object') return undefined;
  if (!isStoredMode(entry.mode) || typeof entry.ts !== 'number') return undefined;
  return entry;
};

/**
 * Resolve the stored notification mode for a thread. Absent or malformed
 * content resolves to `ThreadNotificationMode.Default`.
 */
export function getThreadNotificationMode(
  content: ThreadNotificationsContent | undefined,
  roomId: string,
  threadRootId: string,
): ThreadNotificationMode {
  const entry = readEntry(content, roomId, threadRootId);
  return entry ? entry.mode : ThreadNotificationMode.Default;
}

/**
 * All thread root ids explicitly muted within a room. Malformed content yields
 * an empty set.
 */
export function getMutedThreads(
  content: ThreadNotificationsContent | undefined,
  roomId: string,
): Set<string> {
  const muted = new Set<string>();
  const roomEntries = content?.rooms?.[roomId];
  if (!roomEntries || typeof roomEntries !== 'object') return muted;
  Object.keys(roomEntries).forEach((threadRootId) => {
    const entry = roomEntries[threadRootId];
    if (entry && isStoredMode(entry.mode) && entry.mode === ThreadNotificationMode.Mute) {
      muted.add(threadRootId);
    }
  });
  return muted;
}

/**
 * Decide whether a thread reply should notify.
 *
 * NOTE: the `notify` input reflects the base matrix push rule outcome and is
 * accepted for forward-compatibility, but is intentionally IGNORED in v1: the
 * per-thread mode fully determines the decision, so honoring `notify` would let
 * server push rules silently override an explicit "All" thread override. Kept
 * in the signature so the pipeline can start plumbing it without a later break.
 */
export function shouldNotifyThreadReply(input: {
  mode: ThreadNotificationMode;
  defaultBehavior: ThreadDefaultBehavior;
  participated: boolean;
  highlight: boolean;
  notify: boolean;
  roomMuted: boolean;
  /**
   * The room is set to "Mentions & Keywords only" (room push rule, not the
   * global default). When the thread mode is Default, this makes only
   * highlights notify — honoring the room preference instead of the
   * all/participating default (which otherwise over-notifies). An explicit
   * per-thread All/MentionsOnly/Mute override still wins.
   */
  roomMentionsOnly: boolean;
}): ThreadNotifyDecision {
  const { mode, defaultBehavior, participated, highlight, roomMuted, roomMentionsOnly } = input;

  if (roomMuted) return 'none';
  if (mode === ThreadNotificationMode.Mute) return 'none';

  if (mode === ThreadNotificationMode.All) {
    return highlight ? 'loud' : 'notify';
  }

  if (mode === ThreadNotificationMode.MentionsOnly) {
    return highlight ? 'loud' : 'none';
  }

  // ThreadNotificationMode.Default — highlights always notify loudly.
  if (highlight) return 'loud';
  // Room is "Mentions & Keywords only": a Default thread inherits that, so a
  // non-highlight reply does not notify.
  if (roomMentionsOnly) return 'none';
  if (defaultBehavior === 'all') return 'notify';
  // defaultBehavior === 'participating'
  return participated ? 'notify' : 'none';
}

/**
 * Return a NEW content object with stale/oversized data removed. Never mutates
 * the input.
 *
 * (1) drop rooms not in `joinedRoomIds`
 * (2) drop entries older than 180 days (`ts < now - PRUNE_MAX_AGE_MS`)
 * (3) cap each room at 200 entries, evicting the oldest `ts` first
 * (4) drop rooms left with no entries
 */
export function pruneThreadNotifications(
  content: ThreadNotificationsContent,
  joinedRoomIds: Set<string>,
  now: number,
): ThreadNotificationsContent {
  const minTs = now - PRUNE_MAX_AGE_MS;
  const pruned: ThreadNotificationsContent = {};
  if (content.default !== undefined) {
    pruned.default = content.default;
  }

  const rooms = content.rooms;
  if (!rooms || typeof rooms !== 'object') {
    return pruned;
  }

  const prunedRooms: Record<string, Record<string, ThreadNotificationEntry>> = {};

  Object.keys(rooms).forEach((roomId) => {
    if (!joinedRoomIds.has(roomId)) return;
    const roomEntries = rooms[roomId];
    if (!roomEntries || typeof roomEntries !== 'object') return;

    // Keep only well-formed, non-expired entries.
    const kept: Array<[string, ThreadNotificationEntry]> = [];
    Object.keys(roomEntries).forEach((threadRootId) => {
      const entry = roomEntries[threadRootId];
      if (!entry || !isStoredMode(entry.mode) || typeof entry.ts !== 'number') return;
      if (entry.ts < minTs) return;
      kept.push([threadRootId, { mode: entry.mode, ts: entry.ts }]);
    });

    if (kept.length === 0) return;

    // Cap per room, evicting oldest first (ascending ts sort, keep the tail).
    let capped = kept;
    if (kept.length > PRUNE_MAX_ENTRIES_PER_ROOM) {
      capped = [...kept]
        .sort((a, b) => a[1].ts - b[1].ts)
        .slice(kept.length - PRUNE_MAX_ENTRIES_PER_ROOM);
    }

    const nextRoom: Record<string, ThreadNotificationEntry> = {};
    capped.forEach(([threadRootId, entry]) => {
      nextRoom[threadRootId] = entry;
    });
    prunedRooms[roomId] = nextRoom;
  });

  if (Object.keys(prunedRooms).length > 0) {
    pruned.rooms = prunedRooms;
  }

  return pruned;
}
