import type { ChatMessage } from '../types';

/**
 * A finished conversation is worth reading as question, summary, answer — the tool calls and running
 * commentary that produced an answer are the thing you scroll past. So a turn folds to those three parts.
 *
 * But it folds because it is HISTORY, not because a newer turn exists. That distinction is the whole
 * design: this used to collapse the previous turn the instant you sent the next message, and watching
 * your own conversation fold up under you as you typed was worse than the scrolling it saved. Everything
 * that happened while you were watching stays open for as long as the tab is; reload, and all of it has
 * become history and folds at once.
 *
 * The cost is a property worth naming, because it was deliberate before and is deliberately given up now:
 * folding is no longer a pure derivation over the message list, so a reload does NOT render identically to
 * a live session. That is the feature. The state it depends on is a single count of how many messages came
 * from the server — it lives in `useChat`, never on the wire and never on disk.
 */

/** Rows that are landmarks about the conversation rather than work done inside a turn. These never fold. */
function isStructural(message: ChatMessage): boolean {
  return message.role === 'divider' || message.role === 'compact';
}

/** The work of a turn: everything that belongs behind the summary row. */
function isWork(message: ChatMessage): boolean {
  return message.role === 'tool' || message.role === 'task';
}

/** Prose I actually produced. An empty assistant block renders as nothing and must not count as an answer. */
function isAnswer(message: ChatMessage): boolean {
  return message.role === 'assistant' && message.text.trim().length > 0;
}

/** Ways a turn can end other than by answering. Each has its own row already and rules out the marker. */
function isFailure(message: ChatMessage): boolean {
  return message.role === 'error' || message.role === 'interrupted' || message.role === 'cutoff';
}

export type FoldCounts = {
  tools: number;
  notes: number;
  tasks: number;
  failed: number;
};

export type ChatRow =
  /**
   * `live` marks a turn from this sitting rather than one loaded from the server. It is what tool rows key
   * their default expansion off, so "expanded" means "you were here when this happened".
   */
  | { kind: 'message'; key: string; message: ChatMessage; live: boolean }
  /** A collapsed run of one turn's work. `messages` is the whole of it, in order, for when it is opened. */
  | { kind: 'fold'; key: string; messages: ChatMessage[]; counts: FoldCounts }
  /**
   * A turn that finished cleanly without saying anything. Rare, and disproportionately confusing: the
   * composer re-enables and nothing appears, which is indistinguishable from a turn that died. Deliberately
   * synthesised as a seam rather than as prose — words in my voice that I did not write are a lie, and the
   * next time it happened you would not know which kind of row you were reading.
   */
  | { kind: 'turn-end'; key: string };

/** Stable enough to survive a prepend of older messages, which shifts every index in the list. */
export function keyOf(message: ChatMessage, fallback: number): string {
  switch (message.role) {
    case 'tool':
      return `t:${message.toolCallId || fallback}`;
    case 'task':
      return `k:${message.taskId || fallback}`;
    case 'assistant':
      return `a:${message.id ?? `${fallback}:${message.text.slice(0, 24)}`}`;
    case 'divider':
      return `d:${message.sessionId}`;
    case 'user':
      return `u:${fallback}:${message.text.slice(0, 24)}`;
    default:
      return `${message.role}:${fallback}`;
  }
}

type TurnParams = {
  segment: ChatMessage[];
  /** Index of `segment[0]` in the full list, so keys stay unique across turns. */
  offset: number;
  /** False for a turn from this sitting: its work stays on screen. */
  fold: boolean;
  /** Whether the turn has finished — the live turn has not, while a response is still streaming. */
  complete: boolean;
  /** Stamped onto every row of this turn. Now simply `!fold` — see `buildRows`. */
  live: boolean;
};

function buildTurn({ segment, offset, fold, complete, live }: TurnParams): ChatRow[] {
  const rows: ChatRow[] = [];
  const push = (message: ChatMessage, i: number) =>
    rows.push({ kind: 'message', key: keyOf(message, offset + i), message, live });

  // Your message is never folded — it is the question the whole turn is an answer to.
  let start = 0;
  if (segment[0]?.role === 'user') {
    push(segment[0], 0);
    start = 1;
  }

  // The exposed tail is the trailing run that isn't work: my final prose, and whatever settled the turn.
  let tailStart = segment.length;
  while (tailStart > start && !isWork(segment[tailStart - 1]!)) tailStart--;

  const head = segment.slice(start, tailStart);
  const tail = segment.slice(tailStart);

  if (!fold) {
    head.forEach((message, i) => push(message, start + i));
  } else {
    // Structural rows split the run rather than disappearing into it: "5 tool calls" hiding a `/clear`
    // would misreport what happened to the conversation, not just to the work.
    let run: ChatMessage[] = [];
    let runAt = start;
    const flush = () => {
      if (!run.length) return;
      const counts: FoldCounts = {
        tools: run.filter((m) => m.role === 'tool').length,
        notes: run.filter(isAnswer).length,
        tasks: run.filter((m) => m.role === 'task').length,
        failed: run.filter((m) => m.role === 'tool' && m.isError === true).length,
      };
      rows.push({ kind: 'fold', key: `f:${keyOf(run[0]!, runAt)}`, messages: run, counts });
      run = [];
    };
    head.forEach((message, i) => {
      if (isStructural(message)) {
        flush();
        push(message, start + i);
        runAt = start + i + 1;
        return;
      }
      if (!run.length) runAt = start + i;
      run.push(message);
    });
    flush();
  }

  // Nothing was said and nothing went wrong — say so, rather than ending on a collapsed tool call.
  if (complete && !tail.some(isAnswer) && !tail.some(isFailure) && (head.length > 0 || tail.length > 0)) {
    rows.push({ kind: 'turn-end', key: `e:${offset}` });
  }

  tail.forEach((message, i) => push(message, tailStart + i));
  return rows;
}

/**
 * `historicalCount` is how many messages at the front of the list came from the server rather than from
 * this sitting. It is what decides folding now: a turn folds because it is history, not because a newer
 * turn exists. Everything that happened while you were watching stays open, however many turns you send
 * after it; reload, and all of it is history and folds together.
 *
 * Omitting it folds nothing, which is the right default for a caller that cannot tell the two apart.
 */
export function buildRows(messages: ChatMessage[], isGenerating: boolean, historicalCount = 0): ChatRow[] {
  if (!messages.length) return [];

  const userAt: number[] = [];
  messages.forEach((m, i) => {
    if (m.role === 'user') userAt.push(i);
  });

  // Every user message is a turn boundary — including the last one, which used to be excluded because the
  // turn after it was by definition the live one. Now the final segment is an ordinary turn that folds or
  // not on the same rule as the rest.
  const bounds = [0, ...userAt.filter((i) => i > 0), messages.length].filter((v, i, a) => i === 0 || v !== a[i - 1]);

  const rows: ChatRow[] = [];
  for (let i = 0; i < bounds.length - 1; i++) {
    const from = bounds[i]!;
    const to = bounds[i + 1]!;
    const isLast = to === messages.length;
    // Fold only a turn whose every message is history. A turn still generating is never folded even when
    // its opening messages came from the server — that is a reload onto a live turn, and hiding the work
    // as it arrives is exactly the thing being undone here.
    const generatingNow = isLast && isGenerating;
    const fold = to <= historicalCount && !generatingNow;
    rows.push(
      ...buildTurn({
        segment: messages.slice(from, to),
        offset: from,
        fold,
        complete: !generatingNow,
        // Unfolded and live are one claim now: if a turn's work is on screen, its tool rows are open. The
        // two came apart only because the old rule force-unfolded a window it could find no boundary in.
        live: !fold,
      }),
    );
  }

  return rows;
}

/** "5 tool calls · 2 messages · 1 failed" — what you gave up by folding, so you can tell if you want it back. */
export function foldLabel(counts: FoldCounts): string {
  const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`;
  const parts: string[] = [];
  if (counts.tools) parts.push(plural(counts.tools, 'tool call'));
  if (counts.notes) parts.push(plural(counts.notes, 'message'));
  if (counts.tasks) parts.push(plural(counts.tasks, 'background task'));
  if (!parts.length) parts.push('nothing to show');
  if (counts.failed) parts.push(`${counts.failed} failed`);
  return parts.join(' · ');
}
