import { useEffect, useRef, useState } from 'react';
import { Brain } from 'lucide-react';
import { cn } from '@/components/ui';
import { Disclosure } from '@/components/ui/disclosure';

/**
 * How long after the last reasoning token the panel folds itself away.
 *
 * Long enough that the collapse reads as a consequence of the thinking ending
 * rather than as a flicker, short enough that the answer is not still fighting
 * a wall of scratch text for the eye by the time it starts streaming.
 */
const COLLAPSE_DELAY_MS = 1_000;

/**
 * Piggy's scratch work, shown while it happens and folded away afterwards.
 *
 * Reachable only when `PIGGY_REASONING_EFFORT` is turned up from its default of
 * `none`; with the default, `reasoning_delta` never fires and the integrator
 * never renders this. That is deliberate — see the note on the setting in
 * apps/piggy/src/config.ts — so treat this panel as the operator's debugging
 * surface first and a chat flourish second.
 *
 * Three behaviours, in the order they matter:
 *  - it opens itself while the thinking streams, because unexplained latency is
 *    the thing a reasoning model is worst at;
 *  - it closes itself a beat after the thinking stops, because the answer is
 *    what the user came for and scratch work left open buries it;
 *  - it stops doing either the moment the user touches the disclosure, because
 *    a panel that re-closes itself under someone who opened it to read is worse
 *    than one that never opened at all.
 */
export function PiggyReasoning({ text, streaming }: { text: string; streaming: boolean }) {
  const [open, setOpen] = useState(streaming);
  const [durationMs, setDurationMs] = useState<number | null>(null);
  const bodyRef = useRef<HTMLDivElement | null>(null);
  /**
   * `performance.now()` at the first reasoning token, not at mount: the
   * integrator may render this panel from the moment the turn starts, and the
   * wait for the first byte belongs to the request, not to the thinking.
   */
  const startedAt = useRef<number | null>(null);
  /**
   * Set by the only gesture that can toggle a `<details>` — a click or an
   * Enter/Space on the summary, which the browser reports as a click too. Once
   * it is set, neither automatic rule fires again for this turn.
   */
  const touched = useRef(false);

  const started = Boolean(text.trim());
  useEffect(() => {
    if (streaming && started && startedAt.current === null) startedAt.current = performance.now();
    if (!streaming && startedAt.current !== null) {
      setDurationMs(performance.now() - startedAt.current);
      startedAt.current = null;
    }
  }, [streaming, started]);

  useEffect(() => {
    if (touched.current) return;
    if (streaming) {
      setOpen(true);
      return;
    }
    const timer = setTimeout(() => {
      // Re-checked at fire time as well as at schedule time: the user may have
      // opened the panel during the delay, and this closure would otherwise
      // shut it under them a second later.
      if (!touched.current) setOpen(false);
    }, COLLAPSE_DELAY_MS);
    return () => clearTimeout(timer);
  }, [streaming]);

  useEffect(() => {
    // Follow the tail while it writes. Without this the capped box shows the
    // opening sentence for the whole turn, which looks like a stalled stream.
    if (streaming && bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
  }, [text, streaming]);

  // Nothing was thought and nothing is being thought: render no chrome at all,
  // rather than an empty box the user can open to find nothing in.
  if (!started && !streaming) return null;

  return (
    <Disclosure
      open={open}
      onOpenChange={setOpen}
      className="mb-2 text-xs text-muted"
      summaryClassName="pr-2 text-xs"
      /*
       * The transcript around this is `role="log" aria-live="polite"`, and a
       * live region announces its whole subtree. Auto-opening the panel
       * therefore put the model's scratch work into a screen reader's ear,
       * token by token, ahead of the answer it was scratch work for. `off`
       * overrides the inherited politeness for this subtree only.
       */
      aria-live="off"
      /*
       * On the `<details>` rather than on the summary, because that is where
       * the primitive's own props land — and a click anywhere in this panel,
       * summary or body, is the user attending to it, which is exactly the
       * signal the automatic open and close must stand down for.
       */
      onClick={() => {
        touched.current = true;
      }}
      summary={
        <span className="flex min-w-0 items-center gap-2">
          <Brain className={cn('size-4 shrink-0', streaming && 'animate-pulse')} aria-hidden />
          {streaming ? 'Thinking' : reasoningLabel(durationMs)}
        </span>
      }
    >
      {/* Withheld until the first token so the gap between "Thinking" and
          anything to read is empty space rather than an empty rail. */}
      {started ? (
        <div
          ref={bodyRef}
          className={cn(
            'ml-1 animate-in fade-in border-l border-border py-1 pl-3 duration-2 ease-enter',
            // Capped only while it writes. An auto-opened panel is one the user
            // did not ask for, so it must not push the answer off screen; a
            // panel they opened themselves is one they mean to read to the end.
            streaming && 'max-h-40 overflow-y-auto',
          )}
        >
          <p className="whitespace-pre-wrap leading-5">{text}</p>
        </div>
      ) : null}
    </Disclosure>
  );
}

/**
 * Deliberately silent about duration when there is none to report.
 *
 * A panel mounted against an already-finished turn — a restored transcript, a
 * remount behind a closed sheet — never saw the clock start, and "Thought for 0
 * seconds" would be a measurement we did not take.
 */
function reasoningLabel(durationMs: number | null): string {
  if (durationMs === null) return 'Reasoning';
  const seconds = Math.max(1, Math.round(durationMs / 1_000));
  return `Thought for ${seconds} ${seconds === 1 ? 'second' : 'seconds'}`;
}
