import type { UIMessage } from "@ai-sdk/react";

/** Cheap content signature for one message: changes iff something VISIBLE in the
 *  row changed. Streaming is APPEND-ONLY (text parts only grow, parts are only
 *  appended, a tool/text part flips state once), so a per-part [type, text
 *  length, state, error/output presence] tuple + the persisted metadata
 *  (error/finishReason) is a sufficient change signal without comparing full
 *  strings on every delta. WARNING — load-bearing for the MessageItem memo:
 *  if a future part kind's VISIBLE content can change WITHOUT changing [type,
 *  text length, state, error/output presence] (e.g. a tool that streams
 *  `preliminary` output, or a client-side regenerate that edits a finalized
 *  row in place), extend this signature or the memo will freeze a stale row. */
export function messageSignature(message: UIMessage): string {
  const parts = message.parts
    .map((p) => {
      const any = p as {
        type: string;
        text?: string;
        state?: string;
        errorText?: string;
        output?: unknown;
      };
      return [
        any.type,
        any.text?.length ?? 0,
        any.state ?? "",
        any.errorText ? 1 : 0,
        any.output !== undefined ? 1 : 0,
      ].join(":");
    })
    .join("|");
  const meta = message.metadata as
    | { error?: string; finishReason?: string; usage?: { reasoningTokens?: number } }
    | undefined;
  // `usage.reasoningTokens` is neither append-only nor part-bound: the authoritative
  // turn total arrives on the final `finish-step` AFTER the reasoning text length and
  // state are already frozen. Without it in the signature the row's signature would be
  // unchanged at that point and the re-render skipped, so the "Thinking · N tokens"
  // header (reasoningTokensForPart) would keep the live estimate instead of snapping
  // to the exact figure.
  return `${message.id}#${message.role}#${parts}#${meta?.error ?? ""}#${
    meta?.finishReason ?? ""
  }#${meta?.usage?.reasoningTokens ?? ""}`;
}
