import * as React from "react";

import type { ForcedUnreadSource } from "@/features/channels/forcedUnreadStore";
import type { InboxItem } from "@/features/home/lib/inbox";
import {
  getThreadReference,
  isThreadReply,
} from "@/features/messages/lib/threading";

type UseHomeInboxReadStateOptions = {
  /** Inbox items to project read-state across. */
  items: InboxItem[];
  /** NIP-RS read marker resolver for channel-backed items (unix seconds, or null when unknown). */
  getChannelReadAt: (channelId: string) => number | null;
  /** NIP-RS read marker resolver for thread-backed items (unix seconds, or null when unknown). */
  getThreadReadAt: (rootId: string, channelId?: string | null) => number | null;
  /** NIP-RS read marker resolver for per-message items (unix seconds, or null when unknown). */
  getMessageReadAt?: (messageId: string) => number | null;
  /** Invalidation signal for the channel-marker projection. */
  readStateVersion: number;
  /** Local fallback "done" set (used only for items with no channelId). */
  localDoneSet: ReadonlySet<string>;
  /** Per-item local unread override for inbox rows. */
  localUnreadSet: ReadonlySet<string>;
  /** Mark a channel read up to the given ISO timestamp (NIP-RS). */
  markChannelRead: (
    channelId: string,
    readAt: string | null | undefined,
    options?: {
      preserveForcedUnread?: boolean;
      topLevelOnly?: boolean;
    },
  ) => void;
  /** Force a channel's unread indicator without rolling back its NIP-RS marker. */
  markChannelUnread: (channelId: string, source?: ForcedUnreadSource) => void;
  /** Remove only the named owner of a channel's forced-unread indicator. */
  clearChannelUnreadSource: (
    channelId: string,
    source: ForcedUnreadSource,
  ) => void;
  /** Advance the thread read marker to the given unix-seconds timestamp. */
  markThreadRead: (rootId: string, timestamp: number) => void;
  /** Advance a reply's per-message read marker to the given unix-seconds timestamp. */
  markMessageRead: (messageId: string, timestamp: number) => void;
  /** Local fallback: mark a non-channel item done. */
  markDoneLocal: (id: string) => void;
  /** Local inbox row override: mark an item unread alongside its channel emphasis. */
  markUnreadLocal: (id: string) => void;
  /** Local fallback: undo a non-channel item done. */
  undoDoneLocal: (id: string) => void;
  /** Clear the local inbox row unread override. */
  undoUnreadLocal: (id: string) => void;
};

const EMPTY_ITEM_SET: ReadonlySet<string> = new Set();

function getInboxThreadRootId(item: InboxItem): string | null {
  if (!isThreadReply(item.item.tags)) {
    return null;
  }

  const thread = getThreadReference(item.item.tags);
  return thread.rootId ?? thread.parentId;
}

export function getGroupedChannelReadTimestamp(
  item: InboxItem,
): { channelId: string; timestamp: number } | null {
  const channelId = item.item.channelId ?? null;
  if (!channelId) {
    return null;
  }

  let timestamp: number | null = null;
  for (const groupItem of item.groupItems) {
    if (groupItem.channelId !== channelId || isThreadReply(groupItem.tags)) {
      continue;
    }
    timestamp = Math.max(timestamp ?? 0, groupItem.createdAt);
  }

  return timestamp === null ? null : { channelId, timestamp };
}

export function getGroupedInboxItemIds(item: InboxItem): string[] {
  return [
    ...new Set([item.id, item.item.id, ...item.groupItems.map((i) => i.id)]),
  ];
}

export function hasGroupedUnreadOverride(
  item: InboxItem,
  localUnreadSet: ReadonlySet<string>,
): boolean {
  return getGroupedInboxItemIds(item).some((id) => localUnreadSet.has(id));
}

export function hasRemainingChannelUnreadOverride(
  items: InboxItem[],
  localUnreadSet: ReadonlySet<string>,
  channelId: string,
  clearedItemIds: ReadonlySet<string>,
): boolean {
  return items.some(
    (candidate) =>
      candidate.item.channelId === channelId &&
      getGroupedInboxItemIds(candidate).some(
        (id) => localUnreadSet.has(id) && !clearedItemIds.has(id),
      ),
  );
}

export function resolveInboxItemReadAt(
  item: InboxItem,
  options: {
    getChannelReadAt: (channelId: string) => number | null;
    getMessageReadAt?: (messageId: string) => number | null;
    getThreadReadAt: (
      rootId: string,
      channelId?: string | null,
    ) => number | null;
  },
): number | null {
  const channelId = item.item.channelId;
  const threadRootId = getInboxThreadRootId(item);
  if (threadRootId) {
    if (options.getMessageReadAt) {
      return options.getMessageReadAt(item.item.id);
    }
    return options.getThreadReadAt(threadRootId, channelId);
  }

  return channelId ? options.getChannelReadAt(channelId) : null;
}

/**
 * Projects Home inbox read-state from the shared NIP-RS read marker, with
 * the local `useFeedItemState` done-set as a fallback for items that don't
 * belong to a channel (reminders etc.).
 *
 * "Mark as read" on channel-backed items is routed through `markChannelRead`;
 * thread rows advance the same per-message markers as the channel thread
 * panel, plus the aggregate `thread:<root>` marker for compatibility. "Mark
 * unread" keeps its per-item local override and also restores the source
 * channel's forced-unread indicator so channel surfaces agree with Inbox.
 */
export function useHomeInboxReadState({
  items,
  getChannelReadAt,
  getThreadReadAt,
  getMessageReadAt,
  readStateVersion,
  localDoneSet,
  localUnreadSet = EMPTY_ITEM_SET,
  markChannelRead,
  markChannelUnread,
  clearChannelUnreadSource,
  markThreadRead,
  markMessageRead,
  markDoneLocal,
  markUnreadLocal,
  undoDoneLocal,
  undoUnreadLocal,
}: UseHomeInboxReadStateOptions) {
  const itemById = React.useMemo(
    () => new Map(items.map((item) => [item.id, item])),
    [items],
  );

  // biome-ignore lint/correctness/useExhaustiveDependencies: readStateVersion invalidates getChannelReadAt
  const effectiveDoneSet = React.useMemo<ReadonlySet<string>>(() => {
    const result = new Set<string>();
    for (const item of items) {
      if (hasGroupedUnreadOverride(item, localUnreadSet)) {
        continue;
      }

      const threadRootId = getInboxThreadRootId(item);
      const readAt = resolveInboxItemReadAt(item, {
        getChannelReadAt,
        getMessageReadAt,
        getThreadReadAt,
      });
      if (readAt !== null) {
        if (item.latestActivityAt <= readAt) {
          result.add(item.id);
        }
        continue;
      }

      if (threadRootId !== null || item.item.channelId) {
        continue;
      }

      if (localDoneSet.has(item.id)) {
        result.add(item.id);
      }
    }
    return result;
  }, [
    getChannelReadAt,
    getThreadReadAt,
    getMessageReadAt,
    items,
    localDoneSet,
    localUnreadSet,
    readStateVersion,
  ]);

  const markItemRead = React.useCallback(
    (itemId: string) => {
      const item = itemById.get(itemId);
      const localUnreadIds = item ? getGroupedInboxItemIds(item) : [itemId];
      const clearedItemIds = new Set(localUnreadIds);
      for (const id of localUnreadIds) {
        undoUnreadLocal(id);
      }
      const channelId = item?.item.channelId ?? null;
      if (
        channelId &&
        !hasRemainingChannelUnreadOverride(
          items,
          localUnreadSet,
          channelId,
          clearedItemIds,
        )
      ) {
        clearChannelUnreadSource(channelId, "inbox");
      }
      const threadRootId = item ? getInboxThreadRootId(item) : null;
      if (item && threadRootId) {
        const markedReplyIds = new Set<string>();
        for (const reply of [item.item, ...item.groupItems]) {
          if (!isThreadReply(reply.tags) || markedReplyIds.has(reply.id)) {
            continue;
          }
          markedReplyIds.add(reply.id);
          markMessageRead(reply.id, reply.createdAt);
        }
        markThreadRead(threadRootId, item.latestActivityAt);
        const groupedChannelRead = getGroupedChannelReadTimestamp(item);
        if (groupedChannelRead) {
          markChannelRead(
            groupedChannelRead.channelId,
            new Date(groupedChannelRead.timestamp * 1_000).toISOString(),
            { preserveForcedUnread: true, topLevelOnly: true },
          );
        }
        return;
      }

      if (item && channelId) {
        markChannelRead(
          channelId,
          new Date(item.latestActivityAt * 1_000).toISOString(),
          { preserveForcedUnread: true },
        );
        return;
      }
      markDoneLocal(itemId);
    },
    [
      itemById,
      items,
      clearChannelUnreadSource,
      localUnreadSet,
      markChannelRead,
      markDoneLocal,
      markMessageRead,
      markThreadRead,
      undoUnreadLocal,
    ],
  );

  const markItemUnread = React.useCallback(
    (itemId: string) => {
      undoDoneLocal(itemId);
      markUnreadLocal(itemId);
      const item = itemById.get(itemId);
      const channelId = item?.item.channelId ?? null;
      // The Inbox override owns the durable thread dot, while the channel force
      // restores timeline-level emphasis after the user leaves the channel.
      // Opening the channel clears the bolding but leaves the thread dot until
      // the thread itself is opened or marked read.
      if (channelId && item) {
        markChannelUnread(channelId, "inbox");
      }
    },
    [itemById, markChannelUnread, markUnreadLocal, undoDoneLocal],
  );

  return { effectiveDoneSet, markItemRead, markItemUnread };
}
