import { useCallback } from 'react';
import { useLocation, useParams } from 'react-router';
import { Unplug } from 'lucide-react';
import { toast } from '@/components/ui/sonner';
import { EditableTitle } from '@/components/EditableTitle';
import { useSelectedChatSession } from '../../channels';
import { usePublishPageTitle } from '../../page-title';
import { useAuth } from 'hooks/useAuth';
import { errorText } from 'helpers/error-text';
import { useClaudeSessions } from 'state/useClaudeSessions';
import { useChat, EmbeddableChat } from '../Chat';
import type { ChatMessage } from '../Chat/types';

export type SelectedSession = {
  id: string;
  model?: string | null;
  resumeSummary?: string;
  resumeSessionId?: string;
  initialMessages?: ChatMessage[];
  total?: number; // full transcript length — initialMessages is only the tail window
  initialOffset?: number; // absolute index of initialMessages[0]; older remain above when > 0
  /**
   * The directory this session actually runs in, taken from its own transcript. Carried here rather
   * than read back off the URL: the id already determines it, and when the URL was the authority a
   * fresh deep-link had no cwd at all until the resolve landed — so a turn sent in that window ran in
   * the default dir instead of the project. For a new chat it is the group the list is showing.
   */
  cwd?: string | null;
  /** What the list calls this conversation — the whole `/clear` chain's title when it is one. */
  title?: string | null;
  /** Transcripts spliced into `initialMessages`; `> 1` when this is a merged `/clear` chain. */
  partCount?: number;
} | null;

type ChatLocationState = {
  initialMessage?: string;
  prefillInput?: string;
  model?: string;
  cwd?: { root?: string; path: string };
  attachmentIds?: string[];
  images?: { filename: string; dataUrl: string }[];
} | null;

type DetailBarProps = {
  sessionTitle: string | undefined;
  /**
   * The session's own directory. Not decoration — every `/chat/sessions` call is scoped by it, because
   * the server resolves a transcript by searching that cwd's project dir (`findTranscript`). Renaming
   * without it searches the default group and 404s for any conversation living in a project.
   */
  cwd?: string | null;
  /** Transcripts behind this conversation; `> 1` when `/clear` parts have been merged into it. */
  partCount?: number;
  isConnected: boolean;
  isGenerating: boolean;
  onDisconnect?: () => void;
};

function DetailBar({ sessionTitle, cwd, partCount, isConnected, isGenerating, onDisconnect }: DetailBarProps) {
  // The id to rename comes from the URL, not from `resumeSessionId`. They are the same for an ordinary
  // conversation and not for a merged `/clear` chain, where the resume target is the tail transcript
  // while the list, the links and the server all address the chain by its head. Renaming the tail would
  // have written a title nothing displays.
  //
  // Absent on `/chat/new`, which makes the title read-only — there is no transcript to name yet.
  const { sessionId } = useParams<{ sessionId: string }>();

  // The same rename the list's pencil calls, so both surfaces write one place and the invalidation that
  // follows refreshes the row too. A failure toasts rather than reverting silently, matching `SessionList`.
  const { renameSession } = useClaudeSessions(cwd);
  const commitRename = useCallback(
    async (next: string) => {
      if (!sessionId) return;
      try {
        await renameSession(sessionId, next);
      } catch (err) {
        toast.error(errorText(err));
      }
    },
    [renameSession, sessionId],
  );

  return (
    <div className="shrink-0 flex items-center px-4 py-2 border-b border-border bg-background/60">
      <div className="flex-1 min-w-0 px-3 text-center">
        <EditableTitle
          value={sessionTitle ?? 'New chat'}
          onCommit={commitRename}
          readOnly={!sessionId}
          ariaLabel="Session title"
          hint="Click to rename this chat"
          className="block w-full cursor-text truncate text-sm font-medium text-foreground/80 transition-opacity hover:opacity-70"
          inputClassName="w-full border-b border-primary/40 bg-transparent text-center text-sm font-medium text-foreground/80 outline-none"
        />
        {/* This replaced a link back to the previous session. There is nowhere to go now — the previous
            session is scrolled up above you, with a divider where the clear happened. */}
        {partCount != null && partCount > 1 && (
          <div className="truncate text-xs text-muted-foreground">continued across {partCount} sessions</div>
        )}
      </div>
      <div className="flex items-center gap-2 text-xs text-muted-foreground">
        {!isConnected ? (
          <span className="inline-block h-2 w-2 rounded-full bg-destructive" />
        ) : isGenerating ? (
          <span className="inline-block h-2 w-2 rounded-full bg-duck-orange animate-pulse" />
        ) : (
          <span className="inline-block h-2 w-2 rounded-full bg-green-500" />
        )}
        <span>{!isConnected ? 'Disconnected' : isGenerating ? 'Working...' : ''}</span>
        {isConnected && onDisconnect && (
          <button
            type="button"
            onClick={onDisconnect}
            title="End session — kills any running turn and releases it so it can be resumed elsewhere"
            className="ml-1 p-1 rounded hover:bg-muted hover:text-destructive transition-colors cursor-pointer"
          >
            <Unplug className="h-3.5 w-3.5" />
          </button>
        )}
      </div>
    </div>
  );
}

type NewChatProps = {
  /**
   * The harness that owns this conversation, as `harness/model` — e.g. `opencode/big-pickle`.
   *
   * Load-bearing on resume, not cosmetic. The server routes by model: anything starting `claude-code`
   * goes to the Claude sidecar, everything else to OpenCode. Send nothing and it falls back to the
   * user's default, which is how a `ses_…` id ended up being handed to `claude --resume` — a
   * wrong-harness dispatch rather than a degradation.
   */
  model?: string | null;
  resumeSummary?: string;
  resumeSessionId?: string;
  initialMessages?: ChatMessage[];
  total?: number;
  initialOffset?: number;
  sessionCwd?: string | null;
  sessionTitle?: string | null;
  partCount?: number;
};

function NewChat(props: NewChatProps) {
  const { resumeSummary, resumeSessionId, initialMessages, total, initialOffset, sessionCwd } = props;
  const location = useLocation();
  const locationState = location.state as ChatLocationState;
  const { invalidate: invalidateClaudeSessions } = useClaudeSessions();

  // Refresh the /chat list — Claude has just written/appended this session's transcript.
  const onTurnComplete = useCallback(() => {
    invalidateClaudeSessions();
  }, [invalidateClaudeSessions]);

  // context 'chat' tells the backend to run this session from the caller's own home, so
  // its transcript lands in Claude's own store as an isolated project group (source of truth).
  const chat = useChat(undefined, locationState?.model, {
    resumeSummary,
    resumeSessionId,
    initialMessages,
    onTurnComplete,
    context: 'chat',
    // Only the tail is loaded up front — let the chat page older messages upward on scroll.
    paginate:
      resumeSessionId && typeof total === 'number'
        ? { sessionId: resumeSessionId, total, initialOffset: initialOffset ?? 0 }
        : undefined,
  });

  // Run the session in ITS OWN directory. This used to read `?cwd=` straight off the URL, which made
  // the address bar the authority on where an agent executes — so a pasted /chat/<id> ran its first
  // turn in the default dir until the deep-link resolve caught up, and a hand-edited cwd could point a
  // resumed session anywhere. The session's transcript is the only thing that knows, so it wins;
  // absent (a genuinely new chat) → the group the list is on, else the backend default.
  const cwd = sessionCwd ? { path: sessionCwd } : locationState?.cwd;

  const initialMessage = locationState?.initialMessage
    ? {
        text: locationState.initialMessage,
        attachmentIds: locationState.attachmentIds,
        images: locationState.images,
        cwd: locationState.cwd,
      }
    : undefined;

  return (
    <div className="flex flex-col h-full">
      <DetailBar
        // Was hardcoded `undefined`, so every resumed conversation was headed "New chat". The title now
        // comes from the same place the list gets it, which is also what makes the `/clear` numbering
        // agree in both views instead of only in the row.
        sessionTitle={props.sessionTitle ?? undefined}
        cwd={props.sessionCwd}
        partCount={props.partCount}
        isConnected={chat.isConnected}
        isGenerating={chat.isGenerating}
        onDisconnect={chat.disconnectSession}
      />
      <EmbeddableChat
        chat={chat}
        sessionId={undefined}
        // The session's own model wins. `locationState.model` is declared on ChatLocationState and
        // written by nothing, so it was the only source and always undefined on resume; it stays as a
        // fallback for a future caller that navigates with one deliberately.
        initialModel={props.model ?? locationState?.model ?? undefined}
        initialMessage={initialMessage}
        defaultInput={locationState?.prefillInput ?? ''}
        cwd={cwd}
        className="flex-1 min-h-0"
      />
    </div>
  );
}

export const ChatDetailPanel = () => {
  const [selected] = useSelectedChatSession();

  // Name the page after the conversation, whenever the URL names a real one. Gated on the route param
  // rather than on `selected`, so `/chat` and `/chat/new` keep the plain "Chat" — the panel holds a
  // selection in both, and only `/chat/<id>` means a conversation that exists.
  //
  // Not gated on full screen, though that is where it earns its keep: the nav header is hidden then, so
  // the browser tab strip is the only place the name shows, which is exactly what tells two side-by-side
  // windows apart. Tiled, the same value fills the header's centre. One rule, both states.
  const { sessionId } = useParams<{ sessionId: string }>();

  // The list's copy of the title wins over the one handed down at selection time. `selected.title` rides
  // the `chat:selected-session` channel, which is published once when a row is clicked and never updated
  // — so a rename reached the server, refreshed the row, and left this pane and the page title showing
  // the old name, which reads exactly like the rename having failed. Resolved here, once, and passed
  // down: renaming from either surface now retitles both, and the list's own pencil retitles an open pane.
  const { sessions } = useClaudeSessions(selected?.cwd);
  const title = sessions.find((session) => session.id === sessionId)?.title ?? selected?.title ?? null;

  // The id rides along so the shell can tell a rename of this conversation from opening a different one
  // — only the first should override a tab name typed earlier. See usePageTitle.
  usePublishPageTitle(sessionId && title ? { id: sessionId, title } : null);

  if (!selected) {
    return (
      <div className="h-full flex items-center justify-center text-muted-foreground text-sm">
        Select a session to view
      </div>
    );
  }

  return (
    <NewChat
      key={selected.id}
      resumeSummary={selected.resumeSummary}
      resumeSessionId={selected.resumeSessionId}
      initialMessages={selected.initialMessages}
      total={selected.total}
      initialOffset={selected.initialOffset}
      model={selected.model}
      sessionCwd={selected.cwd}
      sessionTitle={title}
      partCount={selected.partCount}
    />
  );
};
