import { useRef, useState, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router';
import { Plus, MessageSquare, RefreshCw, Trash2, Pencil, Check, X, Layers } from 'lucide-react';
import { toast } from '@/components/ui/sonner';
import { DataList, DataRow, EmptyBlock, ErrorBlock, LoadingBlock, MetaItem, RelativeTime } from '@/components/Data';
import { useSelectedChatSession } from '../../channels';
import { errorText } from 'helpers/error-text';
import { useClaudeSessions } from 'state/useClaudeSessions';
import type { SelectedSession } from './ChatDetailPanel';
import { chatListPath, chatNewPath, chatSessionPath, cwdFromSplat } from './chat-routes';
import { PwdSelector } from './PwdSelector';

/**
 * Which agent owns a conversation, said on EVERY row.
 *
 * Only OpenCode rows carried a badge before, which marks one harness and leaves the other inferred from
 * its absence — fine when you already know the list mixes two, useless when you don't. The list is
 * merged from two stores (Claude transcripts, OpenCode's session db) and the harness decides where a
 * reply is dispatched, so it is a property of the row, not a footnote on the odd one out.
 *
 * `harness` is absent on older Claude rows, so anything that is not OpenCode reads as Claude — the same
 * default the server applies.
 */
const HarnessBadge = ({ harness }: { harness?: 'claude' | 'opencode' }) =>
  harness === 'opencode' ? (
    <span className="rounded bg-info/10 px-1.5 py-0.5 font-medium text-info">OpenCode</span>
  ) : (
    <span className="rounded bg-primary/10 px-1.5 py-0.5 font-medium text-primary">Claude</span>
  );

// Reads the /chat conversation list from Claude's own transcript store (source of truth).
// Clicking a session loads its transcript and continues the real Claude session via --resume.
export const SessionList = () => {
  const navigate = useNavigate();
  // Which session is open, and which project group we're browsing, both come from the route. Reading
  // them there rather than from the selection channel means the highlight and the group are correct on
  // a deep link and on back/forward, before any panel has published.
  const { sessionId, '*': splat } = useParams<{ sessionId: string; '*': string }>();
  const [selected, setSelected] = useSelectedChatSession();
  // A group path when we're on one; otherwise the open session's own directory, so /chat/<id> shows
  // that session among its neighbours instead of snapping the list back to the default group. Null =
  // the caller's own home.
  const activeCwd = cwdFromSplat(splat) ?? selected?.cwd ?? null;
  const { sessions, isLoading, error, refetch, deleteSession, renameSession } = useClaudeSessions(activeCwd);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editValue, setEditValue] = useState('');
  const [confirmingId, setConfirmingId] = useState<string | null>(null);

  const scrolledRef = useRef(false);
  const selectedRef = useCallback((node: HTMLDivElement | null) => {
    if (node && !scrolledRef.current) {
      scrolledRef.current = true;
      node.scrollIntoView({ block: 'center' });
    }
  }, []);

  // No group in a session link, deliberately: the id already determines the directory. It used to
  // carry `?cwd=`, which is how a link could name a group the session doesn't belong to.
  const linkTo = chatSessionPath;

  const startRename = (id: string, current: string) => {
    setConfirmingId(null);
    setEditingId(id);
    setEditValue(current);
  };

  // Rename and delete used to swallow their failures whole. `useClient` only raises a dialog for 401,
  // 403 and 5xx, so a 404 — the likely one here, since a transcript can vanish from disk under you —
  // put the row back exactly as it was and said nothing, which reads as "the rename didn't take".
  const commitRename = async () => {
    const id = editingId;
    const title = editValue.trim();
    setEditingId(null);
    if (!id || !title) return;
    try {
      await renameSession(id, title);
    } catch (err) {
      toast.error(`Could not rename the session: ${errorText(err)}`);
    }
  };

  const handleDelete = async (id: string) => {
    setConfirmingId(null);
    try {
      await deleteSession(id);
      if (selected?.id === id) setSelected(null);
      // Back to the group's list, not the default one — deleting the open session shouldn't also move
      // you out of the project you were working in.
      if (sessionId === id) navigate(chatListPath(activeCwd), { replace: true });
    } catch (err) {
      toast.error(`Could not delete the session: ${errorText(err)}`);
    }
  };

  return (
    <div className="flex h-full flex-col overflow-hidden">
      <div className="flex shrink-0 items-center gap-2 border-b border-border bg-background/60 px-3 py-2">
        <PwdSelector
          value={activeCwd}
          onChange={(cwd) => {
            setSelected(null); // sessions belong to a cwd — clear the open one when switching
            navigate(chatListPath(cwd), { replace: true });
          }}
        />
        <div className="ml-auto flex items-center gap-1.5">
          <button
            onClick={() => refetch()}
            className="cursor-pointer rounded p-1 text-muted-foreground transition-colors hover:text-foreground"
            title="Refresh"
            aria-label="Refresh session list"
          >
            <RefreshCw className={`h-3.5 w-3.5 ${isLoading ? 'animate-spin' : ''}`} />
          </button>
          <button
            onClick={() => {
              // A new chat starts in the group the list is showing, and says so in both places: on the
              // selection (which is what the composer actually runs in) and in the URL.
              setSelected({ id: `new:${Date.now()}`, cwd: activeCwd });
              navigate(chatNewPath(activeCwd), { replace: true });
            }}
            // Was duck-teal filled with duck-yellow text. duck-teal is a bright cyan in dark mode and
            // duck-yellow has no dark override at all, so the pair sat around 2:1 contrast either way —
            // brand colours used as a fill they were never legible against.
            className="flex h-7 cursor-pointer items-center gap-1.5 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
          >
            <Plus className="h-3.5 w-3.5" />
            New Chat
          </button>
        </div>
      </div>

      <div className="min-h-0 flex-1">
        {error ? (
          <ErrorBlock
            title="Could not load sessions"
            message={errorText(error)}
            action={
              <button
                onClick={() => refetch()}
                className="cursor-pointer rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground"
              >
                Try again
              </button>
            }
          />
        ) : isLoading && sessions.length === 0 ? (
          // Loading and empty used to be the same grey line of text, so a slow read looked like an
          // account with no history.
          <LoadingBlock label="Reading transcripts…" />
        ) : sessions.length === 0 ? (
          <EmptyBlock
            icon={MessageSquare}
            title="No sessions yet"
            hint="Conversations started in this folder show up here. Start one with New Chat."
          />
        ) : (
          <DataList>
            {sessions.map((session) => {
              const isActive = sessionId === session.id;
              const isEditing = editingId === session.id;
              const isConfirming = confirmingId === session.id;

              if (isEditing) {
                return (
                  <div key={session.id} className="flex min-w-0 items-center gap-2 px-4 py-3">
                    <input
                      autoFocus
                      value={editValue}
                      onChange={(ev) => setEditValue(ev.target.value)}
                      onKeyDown={(ev) => {
                        if (ev.key === 'Enter') commitRename();
                        if (ev.key === 'Escape') setEditingId(null);
                      }}
                      onBlur={commitRename}
                      aria-label="Session title"
                      className="min-w-0 flex-1 border-b border-primary/40 bg-transparent text-sm outline-none"
                    />
                    <button
                      onMouseDown={(ev) => ev.preventDefault()}
                      onClick={commitRename}
                      className="cursor-pointer p-1 text-success hover:opacity-80"
                      title="Save"
                      aria-label="Save title"
                    >
                      <Check className="h-3.5 w-3.5" />
                    </button>
                    <button
                      onMouseDown={(ev) => ev.preventDefault()}
                      onClick={() => setEditingId(null)}
                      className="cursor-pointer p-1 opacity-50 hover:opacity-100"
                      title="Cancel"
                      aria-label="Cancel rename"
                    >
                      <X className="h-3.5 w-3.5" />
                    </button>
                  </div>
                );
              }

              return (
                <div
                  key={session.id}
                  ref={isActive ? selectedRef : undefined}
                  className={`group flex items-center ${isActive ? 'bg-muted' : ''}`}
                >
                  {/* The row is the link and the actions are its siblings — a <button> inside an <a> is
                      not a thing, and nesting them is what breaks cmd-click on half the app's lists. */}
                  <DataRow
                    to={linkTo(session.id)}
                    title={session.title}
                    selected={isActive}
                    className="min-w-0 flex-1"
                    meta={[
                      <RelativeTime key="t" value={session.updatedAt} />,
                      <HarnessBadge key="h" harness={session.harness} />,
                      // Claude's transcripts give a real turn count. OpenCode's session list has no
                      // count field, so the number on those rows is a hardcoded 0 and costs an HTTP
                      // call per row to make real — the badge stands in its place rather than beside a
                      // zero that means "not asked".
                      session.harness === 'opencode'
                        ? null
                        : `${session.messageCount} msg${session.messageCount === 1 ? '' : 's'}`,
                      // One row, several transcripts: this conversation was `/clear`ed and carried on,
                      // and the server has spliced the parts back together. Worth saying out loud —
                      // the message count above covers all of them, and opening the row shows all of
                      // them, so the row is not standing for the single file its id names.
                      session.partCount && session.partCount > 1 ? (
                        <MetaItem key="c" icon={Layers}>
                          {session.partCount} parts
                        </MetaItem>
                      ) : null,
                    ]}
                  />

                  {isConfirming ? (
                    <div className="mr-2 flex shrink-0 items-center gap-1">
                      {/* Say the size of it. Deleting a merged chain removes every part, and a row
                          that looks like one conversation giving no warning that it is three files is
                          exactly the kind of surprise this confirm exists to prevent. */}
                      <span className="text-xs text-destructive">
                        {session.partCount && session.partCount > 1 ? `Delete all ${session.partCount}?` : 'Delete?'}
                      </span>
                      <button
                        onClick={() => handleDelete(session.id)}
                        className="cursor-pointer rounded p-1 text-destructive hover:bg-destructive/10"
                        title="Confirm delete"
                        aria-label={`Confirm deleting ${session.title}`}
                      >
                        <Check className="h-3.5 w-3.5" />
                      </button>
                      <button
                        onClick={() => setConfirmingId(null)}
                        className="cursor-pointer rounded p-1 opacity-50 hover:opacity-100"
                        title="Cancel"
                        aria-label="Cancel delete"
                      >
                        <X className="h-3.5 w-3.5" />
                      </button>
                    </div>
                  ) : (
                    // focus-within, not just hover: keyboard users could never reach these at all.
                    <div className="mr-2 flex shrink-0 items-center opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100">
                      <button
                        onClick={() => startRename(session.id, session.title)}
                        className="cursor-pointer rounded p-1.5 text-muted-foreground transition-colors hover:text-foreground"
                        title="Rename"
                        aria-label={`Rename ${session.title}`}
                      >
                        <Pencil className="h-3.5 w-3.5" />
                      </button>
                      <button
                        onClick={() => setConfirmingId(session.id)}
                        className="cursor-pointer rounded p-1.5 text-muted-foreground transition-colors hover:text-destructive"
                        title="Delete session"
                        aria-label={`Delete ${session.title}`}
                      >
                        <Trash2 className="h-3.5 w-3.5" />
                      </button>
                    </div>
                  )}
                </div>
              );
            })}
          </DataList>
        )}
      </div>
    </div>
  );
};
