/**
 * What Piggy is doing, and what it has touched.
 *
 * Two views of the same question at two scopes, so they are two tabs rather
 * than two stacked panels: THIS CHAT is what the conversation on screen has
 * read and changed, and ACTIVITY is B5's ledger of every run the workspace has
 * made and what it has cost. Stacking them would put the second half of a
 * scrolling rail permanently below the fold on a laptop; a tab keeps both one
 * press away and neither of them half-visible.
 *
 * Which one opens is decided by whether there is a conversation to describe. An
 * empty transcript has no evidence, so a rail that opened on it would greet
 * every new arrival with a blank column; once a question has been asked, the
 * chat's own evidence is the more specific answer and takes the tab. A press
 * fixes the choice — after that the reader has said what they want to see and
 * the transcript does not get to overrule them.
 */
import { useMemo, useState } from 'react';
import { CheckCircle2, CircleAlert, CircleSlash, FileText, Loader2, TriangleAlert } from 'lucide-react';
import type { PiggyProposedChange } from '@pig/core';
import type { ApprovalStep, TranscriptMessage } from '@/lib/piggy-chat';
import { compactNumber } from '@/lib/api';
import { piggyToolLabel } from '@/lib/piggy-tool-labels';
import { PiggyActivityPanel, spendMoney, spendTitle } from '@/components/piggy/activity-panel';
import { Badge, EmptyState, Section, Stat, cn } from '@/components/ui';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

export function PiggyWorkspaceRail({
  messages,
  className,
}: {
  messages: TranscriptMessage[];
  className?: string;
}) {
  const [chosen, setChosen] = useState<string | null>(null);
  const started = messages.length > 0;
  const tab = chosen ?? (started ? 'chat' : 'activity');

  return (
    <Tabs
      value={tab}
      onValueChange={setChosen}
      // `min-w-0` on every level: this is a flex child, and a flex item's
      // default `min-width: auto` lets a long run label — a title that is a
      // whole UUID — push the rail wider than the column it lives in and spill
      // over the transcript's edge. Measured at 1600: 641px of content in a
      // 319px rail.
      className={cn('flex min-h-0 min-w-0 flex-col overflow-hidden', className)}
    >
      <div className="shrink-0 border-b border-border p-2">
        {/* No palette overrides and no height override: PIG's own treatment and
            the 44px floor are in the primitive now. The rail's triggers were
            36px, which is the one place in Piggy the touch rule was actually
            being broken. */}
        <TabsList className="grid w-full grid-cols-2">
          <TabsTrigger value="chat">This chat</TabsTrigger>
          <TabsTrigger value="activity">Activity</TabsTrigger>
        </TabsList>
      </div>
      {/* `mt-0` undoes the primitive's default gap: the tab strip already has a
          border under it, and a second gap below that reads as a dropped panel. */}
      <TabsContent
        value="chat"
        className="mt-0 min-h-0 w-full min-w-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain"
      >
        <ConversationEvidence messages={messages} />
      </TabsContent>
      <TabsContent
        value="activity"
        className="mt-0 min-h-0 w-full min-w-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain"
      >
        <PiggyActivityPanel />
      </TabsContent>
    </Tabs>
  );
}

// ------------------------------------------------------------------ this chat

interface ConversationSummary {
  turns: number;
  tools: { name: string; runs: number; failures: number }[];
  approvals: ApprovalStep[];
  inputTokens: number;
  outputTokens: number;
  /** Null when no turn reported usage — which is not the same as free. */
  costMicroCents: number | null;
}

function summarise(messages: TranscriptMessage[]): ConversationSummary {
  const tools = new Map<string, { name: string; runs: number; failures: number }>();
  const approvals: ApprovalStep[] = [];
  let turns = 0;
  let inputTokens = 0;
  let outputTokens = 0;
  let costMicroCents: number | null = null;

  for (const message of messages) {
    if (message.role !== 'assistant') continue;
    turns += 1;
    inputTokens += message.inputTokens ?? 0;
    outputTokens += message.outputTokens ?? 0;
    // Left null until a figure exists, so a conversation whose provider
    // reported no usage reads as unknown rather than as costing nothing.
    if (message.costMicroCents != null) costMicroCents = (costMicroCents ?? 0) + message.costMicroCents;
    for (const tool of message.tools ?? []) {
      const entry = tools.get(tool.name) ?? { name: tool.name, runs: 0, failures: 0 };
      entry.runs += 1;
      if (tool.state === 'failed') entry.failures += 1;
      tools.set(tool.name, entry);
    }
    approvals.push(...(message.approvals ?? []));
  }

  return {
    turns,
    tools: [...tools.values()].sort((a, b) => b.runs - a.runs),
    approvals,
    inputTokens,
    outputTokens,
    costMicroCents,
  };
}

function ConversationEvidence({ messages }: { messages: TranscriptMessage[] }) {
  const summary = useMemo(() => summarise(messages), [messages]);

  if (!summary.turns) {
    return (
      <EmptyState
        title="Nothing asked yet"
        description="Every record Piggy reads and every change it proposes is listed here, so an answer can be checked against the rows behind it."
      />
    );
  }

  return (
    <div className="flex flex-col gap-6 p-3">
      <div className="grid grid-cols-2 gap-2">
        <Stat size="sm" surface="inset" label="Answers" value={String(summary.turns)} />
        <Stat
          size="sm"
          surface="inset"
          label="Tool calls"
          value={String(summary.tools.reduce((total, tool) => total + tool.runs, 0))}
        />
        <Stat
          size="sm"
          surface="inset"
          label="Tokens"
          value={`${compactNumber(summary.inputTokens)} / ${compactNumber(summary.outputTokens)}`}
          hint="in / out"
        />
        {/* The exact figure hangs off a wrapper because the tile itself takes no
            `title`: sub-cent spend is rounded for reading and must still be
            recoverable to the micro-cent, which is the number an operator
            reconciles against. */}
        <div className="min-w-0" title={spendTitle(summary.costMicroCents)}>
          <Stat size="sm" surface="inset" label="Spend" value={spendMoney(summary.costMicroCents)} />
        </div>
      </div>

      {summary.approvals.length ? (
        <Section title="Changes" tone="micro" level={3}>
          <ul className="flex flex-col gap-1.5">
            {summary.approvals.map((approval) => (
              <li key={approval.change.id}>
                <ChangeRow approval={approval} />
              </li>
            ))}
          </ul>
        </Section>
      ) : null}

      {/* Not "records read": the same list carries the write tools a turn
          proposed, and calling `pig_log_activity` a record read would be a small
          lie on the one panel whose job is the audit trail. */}
      {summary.tools.length ? (
        <Section title="Tools used" tone="micro" level={3}>
          <ul className="flex flex-col gap-1">
            {summary.tools.map((tool) => (
              <li
                key={tool.name}
                className="flex items-center gap-2 rounded-lg bg-surface-2 px-2.5 py-1.5 text-xs"
              >
                <FileText aria-hidden className="size-3.5 shrink-0 text-muted" />
                <span className="min-w-0 flex-1 truncate text-fg" title={tool.name}>
                  {piggyToolLabel(tool.name)}
                </span>
                {tool.failures ? (
                  <Badge tone="danger">{tool.failures} failed</Badge>
                ) : null}
                <span className="nums shrink-0 text-muted">×{tool.runs}</span>
              </li>
            ))}
          </ul>
          <p className="mt-2 text-xs leading-5 text-muted">
            Open a step in the transcript to see what each of these returned.
          </p>
        </Section>
      ) : null}
    </div>
  );
}

/**
 * A proposed change, at rail width.
 *
 * The card in the transcript is the place a change is read and answered; this
 * is the index to it, so it carries the summary, what became of it, and nothing
 * that would invite a decision from a column too narrow to show the diff.
 */
function ChangeRow({ approval }: { approval: ApprovalStep }) {
  const state = CHANGE_STATES[approval.state];
  const Icon = state.icon;
  return (
    <div className="flex items-start gap-2 rounded-lg border border-border px-2.5 py-2">
      <Icon aria-hidden className={cn('mt-0.5 size-3.5 shrink-0', state.className)} />
      <div className="min-w-0 flex-1">
        <p className="text-xs leading-5 text-fg">{approval.change.summary}</p>
        <p className="mt-0.5 text-xs leading-5 text-muted">
          {state.label}
          {recordLabel(approval.change) ? ` · ${recordLabel(approval.change)}` : ''}
        </p>
      </div>
    </div>
  );
}

/**
 * The same table the approval card is drawn from, and for the same reason:
 * colour marks what needs a person, not what happened. This is an index of
 * changes, so a column in which every applied write is green leaves the one row
 * still waiting for an answer indistinguishable from the four above it.
 */
const CHANGE_STATES: Record<
  ApprovalStep['state'],
  { label: string; icon: typeof CheckCircle2; className: string }
> = {
  // A circle for "act on this", a triangle for "this went wrong" — the same two
  // shapes `components/status.tsx` spends, so the scan works without hue.
  pending: { label: 'Waiting for you', icon: CircleAlert, className: 'text-warning' },
  submitting: { label: 'Sending your decision', icon: Loader2, className: 'animate-spin text-muted' },
  applied: { label: 'Applied', icon: CheckCircle2, className: 'text-muted' },
  rejected: { label: 'Rejected', icon: CircleSlash, className: 'text-muted' },
  failed: { label: 'Not applied', icon: TriangleAlert, className: 'text-danger' },
};

function recordLabel(change: PiggyProposedChange): string | null {
  if (!change.record) return null;
  return change.record.label ?? change.record.type.replaceAll('_', ' ');
}

