import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router";
import { Masthead } from "../components/Masthead";
import { useDirective, useDirectives } from "../hooks/useDirectives";
import { useAuth } from "../contexts/AuthContext";
import { DocumentEditor } from "../components/directives/DocumentEditor";
import {
  type DirectiveSummary,
  type DirectiveStatus,
  type DirectiveDocument,
  type DirectiveDocumentStatus,
  type DirectiveStep,
  type Task,
  type DocumentTasksResponse,
  listDirectiveDocuments,
  createDirectiveDocument,
  getDirectiveDocument,
  updateDirectiveDocument,
  listDirectiveDocumentTasks,
  createDirectiveTask,
  startDirective,
  pauseDirective,
  updateDirective,
  deleteDirective,
  createDirectivePR,
  advanceDirective,
  cleanupDirective,
  pickUpOrders,
} from "../lib/api";
import { DirectiveContextMenu } from "../components/directives/DirectiveContextMenu";
import { DocumentTaskStream } from "../components/directives/DocumentTaskStream";

// Status dot color, matching the existing tabular UI's badge palette so the
// document mode feels like a sibling of the existing list, not a foreign UI.
const STATUS_DOT: Record<DirectiveStatus, string> = {
  draft: "bg-[#556677]",
  active: "bg-green-400",
  idle: "bg-yellow-400",
  paused: "bg-orange-400",
  inactive: "bg-[#75aafc]",
  archived: "bg-[#3a4a6a]",
};

// Per-document status palette. Active/draft documents use the same bright
// green-ish accent as a running directive; shipped/archived use a muted blue.
const DOC_STATUS_DOT: Record<DirectiveDocumentStatus, string> = {
  draft: "bg-[#556677]",
  active: "bg-green-400",
  shipped: "bg-[#75aafc]",
  archived: "bg-[#3a4a6a]",
};

// =============================================================================
// Sidebar grouping — group directives by lifecycle stage so the file tree
// reads like a folder per status. We collapse the noisy ones (Archived) by
// default and keep Active / Idle expanded.
// =============================================================================

// Sidebar is now a flat list ordered by status precedence — see
// `sortedDirectives` in DocumentSidebar. Status is shown as a colored dot
// on the right of each row, no per-status grouping.

// Slugify a document title for the displayed `.md` filename, falling back to
// the directive title and finally the document id slice when the title is
// empty. Mirrors the file-naming fix from step 1 (use the user-readable label
// rather than just an id slice). Accepts either a DirectiveSummary or a full
// DirectiveWithSteps — only `title` is read.
function fileLabel(
  doc: DirectiveDocument,
  directive: { title: string },
): string {
  const docTitle = doc.title.trim();
  if (docTitle.length > 0) return docTitle;
  const dirTitle = directive.title.trim();
  if (dirTitle.length > 0) return dirTitle;
  return doc.id.slice(0, 8);
}

// =============================================================================
// Sidebar icons (inline SVG, no new deps)
// =============================================================================

function FolderIcon({ open = false }: { open?: boolean }) {
  return (
    <svg
      viewBox="0 0 16 16"
      width={12}
      height={12}
      className="shrink-0"
      aria-hidden
    >
      {open ? (
        <path
          d="M1.5 3.5a1 1 0 0 1 1-1h3.382a1 1 0 0 1 .894.553l.448.894a1 1 0 0 0 .894.553H13.5a1 1 0 0 1 1 1V6H1.5V3.5z M1 6.5h13.382a.5.5 0 0 1 .49.598l-.9 5A.5.5 0 0 1 13.482 12.5H2.518a.5.5 0 0 1-.49-.402l-.9-5A.5.5 0 0 1 1.62 6.5H1z"
          fill="#75aafc"
          opacity="0.85"
        />
      ) : (
        <path
          d="M1.5 4a1 1 0 0 1 1-1h3.382a1 1 0 0 1 .894.553l.448.894a1 1 0 0 0 .894.553H13.5a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V4z"
          fill="#75aafc"
          opacity="0.65"
        />
      )}
    </svg>
  );
}

function FileIcon() {
  return (
    <svg
      viewBox="0 0 16 16"
      width={12}
      height={12}
      className="shrink-0"
      aria-hidden
    >
      <path
        d="M3 1.5h6.293a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 13.293 5.5H13V14a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V1.5z"
        fill="none"
        stroke="#9bc3ff"
        strokeWidth="1"
      />
      <path
        d="M9.5 1.5v3h3"
        fill="none"
        stroke="#9bc3ff"
        strokeWidth="1"
      />
    </svg>
  );
}

function Caret({ open }: { open: boolean }) {
  return (
    <svg
      viewBox="0 0 8 8"
      width={8}
      height={8}
      className={`shrink-0 transition-transform ${open ? "rotate-90" : ""}`}
      aria-hidden
    >
      <path d="M2 1l4 3-4 3z" fill="#7788aa" />
    </svg>
  );
}

// =============================================================================
// SidebarSelection — exactly one of taskId/documentId is non-null. taskId is
// reserved for a future "task selection" feature (we expose it in the URL
// already so the param shape is stable). documentId picks one of the
// directive's documents.
// =============================================================================

interface SidebarSelection {
  directiveId: string;
  taskId: string | null;
  documentId: string | null;
}

// =============================================================================
// Per-directive folder — renders as a collapsible folder containing the
// directive's documents. Loads documents lazily on first open (mirroring the
// pattern from step 1's DirectiveFolder, which fetched the full directive
// only when expanded).
// =============================================================================

interface DirectiveFolderProps {
  directive: DirectiveSummary;
  open: boolean;
  onToggle: () => void;
  /** Called when the user clicks the folder header itself (after toggle). */
  onHeaderClick: () => void;
  selection: SidebarSelection | null;
  onSelectDocument: (directiveId: string, doc: DirectiveDocument) => void;
  onCreateDocument: (directive: DirectiveSummary) => Promise<void>;
  /** Open the inline "+ New ephemeral task" form for this directive. */
  onCreateEphemeralTask: (directive: DirectiveSummary) => void;
  /** Right-click handler — opens DirectiveContextMenu with start/pause/PR/etc. */
  onContextMenu: (e: React.MouseEvent, directive: DirectiveSummary) => void;
  /** Click handler for task/step rows — navigates to the live transcript. */
  onSelectTask: (directiveId: string, taskId: string) => void;
  /**
   * Document refresh trigger — bumped externally so the folder refetches its
   * document list after a create/update happens elsewhere. Primarily used so
   * a freshly-created document shows up immediately.
   */
  refreshNonce: number;
}

function DirectiveFolder({
  directive,
  open,
  onToggle,
  onHeaderClick,
  selection,
  onSelectDocument,
  onCreateDocument,
  onCreateEphemeralTask,
  onContextMenu,
  onSelectTask,
  refreshNonce,
}: DirectiveFolderProps) {
  const selectedTaskIdForFolder =
    selection && selection.directiveId === directive.id
      ? selection.taskId
      : null;
  const dotColor = STATUS_DOT[directive.status] ?? STATUS_DOT.draft;
  const orchestratorRunning = !!directive.orchestratorTaskId;

  // Documents fetched lazily on open. We deliberately scope the fetch to the
  // open-state so closed folders don't pay the network cost on initial render.
  const [docs, setDocs] = useState<DirectiveDocument[] | null>(null);
  const [docsLoading, setDocsLoading] = useState(false);
  const [docsError, setDocsError] = useState<string | null>(null);

  // shipped/ subfolder open state — independent of the directive folder.
  const [shippedOpen, setShippedOpen] = useState(false);

  // Whether a "+ New document" call is in flight (disables the button).
  const [creating, setCreating] = useState(false);

  const refresh = useCallback(async () => {
    setDocsLoading(true);
    setDocsError(null);
    try {
      const list = await listDirectiveDocuments(directive.id);
      setDocs(list);
    } catch (e) {
      setDocsError(e instanceof Error ? e.message : "Failed to load documents");
    } finally {
      setDocsLoading(false);
    }
  }, [directive.id]);

  // Fetch on open; refetch when refreshNonce bumps and the folder is open.
  useEffect(() => {
    if (!open) return;
    void refresh();
  }, [open, refresh, refreshNonce]);

  // Split the documents into the two visual groups. Memoised so we don't
  // recompute on every render.
  const { activeDocs, shippedDocs } = useMemo(() => {
    const active: DirectiveDocument[] = [];
    const shipped: DirectiveDocument[] = [];
    for (const d of docs ?? []) {
      if (d.status === "shipped" || d.status === "archived") {
        shipped.push(d);
      } else {
        active.push(d);
      }
    }
    // Stable order: by createdAt ascending so the first row is the oldest doc.
    active.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    shipped.sort((a, b) => (b.shippedAt ?? "").localeCompare(a.shippedAt ?? ""));
    return { activeDocs: active, shippedDocs: shipped };
  }, [docs]);

  const handleCreate = useCallback(async () => {
    if (creating) return;
    setCreating(true);
    try {
      await onCreateDocument(directive);
      // Refresh after creating so the new doc appears in the list.
      await refresh();
    } finally {
      setCreating(false);
    }
  }, [creating, onCreateDocument, directive, refresh]);

  // Selection helpers — used to highlight the currently-selected doc row.
  const selectedDocumentId =
    selection && selection.directiveId === directive.id
      ? selection.documentId
      : null;

  return (
    <div className="select-none">
      {/* Directive folder header. Status is shown as a colored dot on the
          RIGHT (per the user's spec — flat list, no per-status grouping).
          Right-click opens the context menu (start / pause / archive /
          delete / create-PR / update-PR / etc.). */}
      <button
        type="button"
        onClick={() => {
          onToggle();
          onHeaderClick();
        }}
        onContextMenu={(e) => onContextMenu(e, directive)}
        title={`${directive.title} — ${directive.status}`}
        className="w-full flex items-center gap-1.5 pl-3 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)]"
      >
        <Caret open={open} />
        <FolderIcon open={open} />
        <span className="truncate flex-1 text-left">
          {directive.title.trim().length > 0
            ? directive.title
            : directive.id.slice(0, 8)}
          /
        </span>
        {orchestratorRunning && (
          <span
            className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-yellow-400 animate-pulse"
            title="Orchestrator running"
            aria-label="Orchestrator running"
          />
        )}
        {/* Status indicator on the RIGHT side of the row. */}
        <span
          className={`inline-block w-2 h-2 rounded-full shrink-0 ${dotColor}`}
          aria-label={`status: ${directive.status}`}
          title={`status: ${directive.status}`}
        />
      </button>

      {/* Folder body — rendered only when open */}
      {open && (
        <div className="py-0.5">
          {docsLoading && !docs && (
            <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677]">
              Loading documents…
            </div>
          )}
          {docsError && (
            <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-red-400">
              {docsError}
            </div>
          )}

          {/* Active group */}
          {docs && (
            <>
              {/* + New document affordance — sits at the top of the active list
                  so the user can always reach it without scrolling past
                  existing docs. */}
              <button
                type="button"
                onClick={handleCreate}
                disabled={creating}
                className="w-full flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] text-emerald-400 hover:bg-[rgba(74,222,128,0.06)] disabled:opacity-50"
                title="Create a new document under this directive"
              >
                <span className="text-[12px] leading-none">+</span>
                <span>New document</span>
              </button>

              {/* + New ephemeral task — sibling affordance for spawning a
                  one-off task under this directive that's NOT part of the
                  DAG. Useful for sidebar scratch work, debugging, etc. */}
              <button
                type="button"
                onClick={() => onCreateEphemeralTask(directive)}
                className="w-full flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] text-[#c084fc] hover:bg-[rgba(192,132,252,0.06)]"
                title="Spawn a one-off ephemeral task under this directive"
              >
                <span className="text-[12px] leading-none">+</span>
                <span>New ephemeral task</span>
              </button>

              {activeDocs.length === 0 && !docsLoading && (
                <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677] italic">
                  no active documents
                </div>
              )}

              {activeDocs.map((doc) => (
                // Each active document gets its own tasks/ subfolder
                // immediately below it. Active docs default-open the
                // folder so the user sees their live work without an
                // extra click.
                <div key={doc.id}>
                  <DocumentRow
                    doc={doc}
                    directive={directive}
                    selected={doc.id === selectedDocumentId}
                    onSelect={() => onSelectDocument(directive.id, doc)}
                  />
                  <DocumentTasksFolder
                    documentId={doc.id}
                    directiveId={directive.id}
                    depth="normal"
                    defaultOpen={doc.status === "active"}
                    refreshNonce={refreshNonce}
                    selectedTaskId={selectedTaskIdForFolder}
                    onSelectTask={onSelectTask}
                  />
                </div>
              ))}

              {/* shipped/ subfolder — only rendered when at least one shipped
                  or archived doc exists. Hidden entirely otherwise so empty
                  directives stay tidy. */}
              {shippedDocs.length > 0 && (
                <div>
                  <button
                    type="button"
                    onClick={() => setShippedOpen((v) => !v)}
                    className="w-full flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)]"
                  >
                    <Caret open={shippedOpen} />
                    <FolderIcon open={shippedOpen} />
                    <span>shipped/</span>
                    <span className="ml-auto text-[10px] text-[#556677]">
                      {shippedDocs.length}
                    </span>
                  </button>
                  {shippedOpen &&
                    shippedDocs.map((doc) => (
                      // Shipped docs render the doc row + its frozen
                      // tasks/ subfolder. The tasks/ folder defaults
                      // closed (history) so it doesn't dominate the
                      // sidebar; users can click to inspect what work
                      // produced this shipped contract.
                      <div key={doc.id}>
                        <DocumentRow
                          doc={doc}
                          directive={directive}
                          selected={doc.id === selectedDocumentId}
                          onSelect={() => onSelectDocument(directive.id, doc)}
                          indent="deep"
                        />
                        <DocumentTasksFolder
                          documentId={doc.id}
                          directiveId={directive.id}
                          depth="deep"
                          defaultOpen={false}
                          refreshNonce={refreshNonce}
                          selectedTaskId={selectedTaskIdForFolder}
                          onSelectTask={onSelectTask}
                        />
                      </div>
                    ))}
                </div>
              )}
            </>
          )}
        </div>
      )}
    </div>
  );
}

// =============================================================================
// DocumentRow — one row inside a directive folder. The indent depth differs
// between active rows (one level deep) and shipped rows (two levels deep).
// =============================================================================

interface DocumentRowProps {
  doc: DirectiveDocument;
  directive: DirectiveSummary;
  selected: boolean;
  onSelect: () => void;
  indent?: "normal" | "deep";
}

function DocumentRow({
  doc,
  directive,
  selected,
  onSelect,
  indent = "normal",
}: DocumentRowProps) {
  const dot = DOC_STATUS_DOT[doc.status] ?? DOC_STATUS_DOT.draft;
  const padLeft = indent === "deep" ? "pl-[88px]" : "pl-14";
  const name = `${fileLabel(doc, directive)}.md`;

  return (
    <button
      type="button"
      onClick={onSelect}
      title={name}
      className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${
        selected
          ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]"
          : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent"
      }`}
    >
      <FileIcon />
      <span
        className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
        aria-hidden
        title={doc.status}
      />
      <span className="truncate flex-1">{name}</span>
      {/* Status chip — only shown for non-active states so the row stays
          uncluttered for the common case. */}
      {doc.status !== "active" && (
        <span className="text-[9px] uppercase tracking-wide text-[#556677]">
          {doc.status}
        </span>
      )}
      {/* PR badge for shipped docs. The link short-circuits the row's
          onClick so clicking the PR doesn't also re-select the doc. */}
      {doc.prUrl && (doc.status === "shipped" || doc.status === "archived") && (
        <a
          href={doc.prUrl}
          target="_blank"
          rel="noreferrer noopener"
          onClick={(e) => e.stopPropagation()}
          className="text-[9px] text-[#75aafc] hover:text-white border border-[#2a3a5a] rounded px-1"
          title={doc.prUrl}
        >
          PR
        </a>
      )}
    </button>
  );
}

// =============================================================================
// Per-document tasks/ subfolder — fetches the steps + ephemeral tasks for a
// single document and renders a collapsible `tasks/` row beneath the
// document. Lazy: fetch only fires once the user opens the folder, and we
// also keep the folder closed by default for shipped docs (where it's
// historical) and open by default for active docs (where it's live work).
// =============================================================================

interface DocumentTasksFolderProps {
  documentId: string;
  /** Parent directive id — needed so a clicked task row can navigate to
   *  /directives/<directiveId>?task=<taskId>. */
  directiveId: string;
  /** Visual indent depth — mirrors the parent DocumentRow's indent so the
   *  tasks/ row sits one level deeper than its parent doc. */
  depth: "normal" | "deep";
  /** Whether to fetch+open by default. Active docs default to open so the
   *  user sees their live tasks immediately; shipped docs default to closed
   *  (historical), and the user can click to expand. */
  defaultOpen: boolean;
  /** Bumped externally so the folder refetches its task list after a save
   *  or status change elsewhere. Same nonce used for the directive folder. */
  refreshNonce: number;
  /** Currently-selected task id (drives row highlight). */
  selectedTaskId: string | null;
  /** Click handler for step/task rows — navigates to the live transcript. */
  onSelectTask: (directiveId: string, taskId: string) => void;
}

function DocumentTasksFolder({
  documentId,
  directiveId,
  depth,
  defaultOpen,
  refreshNonce,
  selectedTaskId,
  onSelectTask,
}: DocumentTasksFolderProps) {
  const [open, setOpen] = useState(defaultOpen);
  const [data, setData] = useState<DocumentTasksResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Inner row indent is one level deeper than the folder header. Folder
  // header uses pl-[88px] (deep) or pl-14 (normal); tasks rows go one
  // step beyond that.
  const headerPadLeft = depth === "deep" ? "pl-[88px]" : "pl-14";
  const rowPadLeft = depth === "deep" ? "pl-[112px]" : "pl-[72px]";

  const refresh = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await listDirectiveDocumentTasks(documentId);
      setData(res);
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to load tasks");
    } finally {
      setLoading(false);
    }
  }, [documentId]);

  // Fetch when the folder is open (initial open or refresh). We don't
  // pre-fetch on closed folders so we don't waste bandwidth on the long
  // tail of historical shipped docs the user never expands.
  useEffect(() => {
    if (!open) return;
    void refresh();
  }, [open, refresh, refreshNonce]);

  const total = (data?.steps.length ?? 0) + (data?.tasks.length ?? 0);

  // Don't render the folder at all if we've fetched and the document has
  // no tasks. This is the cleanest visual: a draft document just shows up
  // as a single row with no children. The empty-folder check is gated on
  // a successful fetch so we don't flash "no tasks/" rows during loading.
  if (data && total === 0 && !loading && !error) {
    return null;
  }

  return (
    <div>
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className={`w-full flex items-center gap-1.5 ${headerPadLeft} pr-3 py-1 font-mono text-[11px] text-[#7788aa] hover:bg-[rgba(117,170,252,0.06)]`}
      >
        <Caret open={open} />
        <FolderIcon open={open} />
        <span>tasks/</span>
        {total > 0 && (
          <span className="ml-auto text-[10px] text-[#556677]">{total}</span>
        )}
      </button>
      {open && (
        <div className="py-0.5">
          {loading && !data && (
            <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-[#556677]`}>
              Loading tasks…
            </div>
          )}
          {error && (
            <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-red-400`}>
              {error}
            </div>
          )}
          {data?.steps.map((step) => (
            <StepRow
              key={`step-${step.id}`}
              step={step}
              directiveId={directiveId}
              selected={!!selectedTaskId && step.taskId === selectedTaskId}
              padLeft={rowPadLeft}
              onSelect={onSelectTask}
            />
          ))}
          {data?.tasks.map((task) => (
            <TaskRow
              key={`task-${task.id}`}
              task={task}
              directiveId={directiveId}
              selected={task.id === selectedTaskId}
              padLeft={rowPadLeft}
              onSelect={onSelectTask}
            />
          ))}
        </div>
      )}
    </div>
  );
}

// Step status → coloured dot, mirroring directive status palette so the
// sidebar reads consistently.
const STEP_STATUS_DOT: Record<string, string> = {
  pending: "bg-[#556677]",
  ready: "bg-[#9bc3ff]",
  running: "bg-yellow-400",
  completed: "bg-green-400",
  failed: "bg-red-400",
  skipped: "bg-[#3a4a6a]",
};

// Task status → coloured dot. Statuses come from the Task model; the small
// set we expect in directive context is enough — anything else falls back
// to the muted "draft" colour.
const TASK_STATUS_DOT: Record<string, string> = {
  pending: "bg-[#556677]",
  starting: "bg-yellow-400",
  running: "bg-yellow-400",
  completed: "bg-green-400",
  failed: "bg-red-400",
  cancelled: "bg-[#3a4a6a]",
  interrupted: "bg-orange-400",
};

interface StepRowProps {
  step: DirectiveStep;
  directiveId: string;
  selected: boolean;
  padLeft: string;
  onSelect: (directiveId: string, taskId: string) => void;
}

function StepRow({
  step,
  directiveId,
  selected,
  padLeft,
  onSelect,
}: StepRowProps) {
  const dot = STEP_STATUS_DOT[step.status] ?? "bg-[#556677]";
  // Steps without an underlying task can't be opened — the executor
  // hasn't started yet so there's no transcript to show. Render them
  // disabled so the user can see them in the list but knows they're
  // inert. Same for steps stuck in pending/skipped.
  const taskId = step.taskId;
  const clickable = !!taskId;
  return (
    <button
      type="button"
      disabled={!clickable}
      onClick={() => clickable && onSelect(directiveId, taskId!)}
      title={
        clickable
          ? `${step.name} (${step.status})`
          : `${step.name} — no task spawned yet (${step.status})`
      }
      className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${
        selected
          ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]"
          : clickable
          ? "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent"
          : "text-[#556677] border-l-2 border-transparent cursor-not-allowed"
      }`}
    >
      <FileIcon />
      <span
        className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
        aria-hidden
        title={step.status}
      />
      <span className="truncate flex-1">{step.name}</span>
      <span className="text-[9px] uppercase tracking-wide text-[#556677]">
        step
      </span>
    </button>
  );
}

interface TaskRowProps {
  task: Task;
  directiveId: string;
  selected: boolean;
  padLeft: string;
  onSelect: (directiveId: string, taskId: string) => void;
}

function TaskRow({
  task,
  directiveId,
  selected,
  padLeft,
  onSelect,
}: TaskRowProps) {
  const dot = TASK_STATUS_DOT[task.status] ?? "bg-[#556677]";
  // Supervisor tasks get a small "sup" tag so the user can spot
  // contract orchestrators in the list.
  const isSup = task.isSupervisor;
  return (
    <button
      type="button"
      onClick={() => onSelect(directiveId, task.id)}
      title={`${task.name} (${task.status})`}
      className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${
        selected
          ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]"
          : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent"
      }`}
    >
      <FileIcon />
      <span
        className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
        aria-hidden
        title={task.status}
      />
      <span className="truncate flex-1">{task.name}</span>
      <span className="text-[9px] uppercase tracking-wide text-[#556677]">
        {isSup ? "sup" : "task"}
      </span>
    </button>
  );
}

// =============================================================================
// Sidebar
// =============================================================================

interface SidebarProps {
  directives: DirectiveSummary[];
  loading: boolean;
  selection: SidebarSelection | null;
  onSelectDocument: (directiveId: string, doc: DirectiveDocument) => void;
  onSelectDirective: (directiveId: string) => void;
  onCreateDocument: (directive: DirectiveSummary) => Promise<void>;
  onCreateContract: () => void;
  onCreateEphemeralTask: (directive: DirectiveSummary) => void;
  onContextMenu: (e: React.MouseEvent, directive: DirectiveSummary) => void;
  onSelectTask: (directiveId: string, taskId: string) => void;
  refreshNonce: number;
}

function DocumentSidebar({
  directives,
  loading,
  selection,
  onSelectDocument,
  onSelectDirective,
  onCreateDocument,
  onCreateContract,
  onCreateEphemeralTask,
  onContextMenu,
  onSelectTask,
  refreshNonce,
}: SidebarProps) {
  // Flat sort: active first, then idle, paused, draft, inactive, archived.
  // Status is surfaced as a colored dot on the RIGHT of each contract row
  // (see DirectiveFolder header) — the user explicitly asked NOT to nest
  // contracts inside per-status folders.
  const sortedDirectives: DirectiveSummary[] = useMemo(() => {
    const order: Record<DirectiveStatus, number> = {
      active: 0,
      paused: 1,
      idle: 2,
      draft: 3,
      inactive: 4,
      archived: 5,
    };
    return [...directives].sort((a, b) => {
      const oa = order[a.status] ?? 99;
      const ob = order[b.status] ?? 99;
      if (oa !== ob) return oa - ob;
      return a.title.localeCompare(b.title, undefined, { sensitivity: "base" });
    });
  }, [directives]);

  // Per-directive open state. We auto-open the directive containing the
  // current selection so the user can see what they're editing.
  const [openDirectives, setOpenDirectives] = useState<Record<string, boolean>>({});

  useEffect(() => {
    if (!selection) return;
    setOpenDirectives((prev) =>
      prev[selection.directiveId] ? prev : { ...prev, [selection.directiveId]: true },
    );
  }, [selection?.directiveId]);

  const toggleDirective = (id: string) =>
    setOpenDirectives((prev) => ({ ...prev, [id]: !prev[id] }));

  return (
    <div className="flex flex-col h-full">
      {/* Sidebar header */}
      <div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]">
        <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide">
          Contracts
        </span>
        <div className="flex items-center gap-2">
          <span className="text-[10px] font-mono text-[#556677]">
            {directives.length}
          </span>
          <button
            type="button"
            onClick={onCreateContract}
            className="text-[11px] font-mono text-emerald-300 hover:text-white border border-emerald-700/60 hover:border-emerald-400 rounded px-1.5 py-0.5 leading-none"
            title="Create a new contract (directive)"
          >
            + New
          </button>
        </div>
      </div>

      {/* Top-level "directives/" folder */}
      <div className="flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-mono text-[#9bc3ff]">
        <FolderIcon open />
        <span>directives/</span>
      </div>

      {/* Body — flat list, status is a colored dot on the right of each row. */}
      <div className="flex-1 overflow-y-auto pb-4">
        {loading && directives.length === 0 ? (
          <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
            Loading...
          </div>
        ) : directives.length === 0 ? (
          <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
            No contracts yet
          </div>
        ) : (
          <div className="py-0.5">
            {sortedDirectives.map((d) => (
              <DirectiveFolder
                key={d.id}
                directive={d}
                open={!!openDirectives[d.id]}
                onToggle={() => toggleDirective(d.id)}
                onHeaderClick={() => onSelectDirective(d.id)}
                selection={selection}
                onSelectDocument={onSelectDocument}
                onCreateDocument={onCreateDocument}
                onCreateEphemeralTask={onCreateEphemeralTask}
                onContextMenu={onContextMenu}
                onSelectTask={onSelectTask}
                refreshNonce={refreshNonce}
              />
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// =============================================================================
// Editor shell — wraps DocumentEditor and handles the "no document selected"
// and loading states. Two modes:
//   1) documentId selected → fetch the DirectiveDocument and edit doc.body via
//      updateDirectiveDocument (the call that auto-reactivates a shipped doc).
//   2) no documentId (legacy fallback, kept for the "select a directive but
//      not a document" transitional case) → edit directive.goal as before.
// =============================================================================

interface EditorShellProps {
  selection: SidebarSelection | null;
  hasDirectives: boolean;
  listLoading: boolean;
  /** Bumped after a successful document save so the sidebar refetches. */
  onDocumentChanged: () => void;
}

function EditorShell({
  selection,
  hasDirectives,
  listLoading,
  onDocumentChanged,
}: EditorShellProps) {
  const directiveId = selection?.directiveId;
  const documentId = selection?.documentId ?? null;

  // We deliberately don't pull `updateGoal` here — in the multi-document
  // world, edits flow through updateDirectiveDocument (which auto-reactivates
  // a shipped doc when its body changes). The legacy directive.goal is
  // unused on this surface.
  const {
    directive,
    loading: directiveLoading,
    cleanup,
    createPR,
    pickUpOrders,
  } = useDirective(directiveId);

  // Document fetch — only when documentId is selected. Refetched whenever the
  // id changes; not polled (the document stream is too low-traffic to warrant
  // background refresh in this iteration).
  const [doc, setDoc] = useState<DirectiveDocument | null>(null);
  const [docLoading, setDocLoading] = useState(false);
  const [docError, setDocError] = useState<string | null>(null);

  useEffect(() => {
    if (!documentId) {
      setDoc(null);
      setDocLoading(false);
      setDocError(null);
      return;
    }
    let cancelled = false;
    setDocLoading(true);
    setDocError(null);
    getDirectiveDocument(documentId)
      .then((d) => {
        if (cancelled) return;
        setDoc(d);
      })
      .catch((e) => {
        if (cancelled) return;
        setDocError(e instanceof Error ? e.message : "Failed to load document");
      })
      .finally(() => {
        if (cancelled) return;
        setDocLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, [documentId]);

  // Save callback for the document path. The backend re-stamps a shipped doc
  // back to active when its body changes, so we just optimistically update
  // local state with the server's response.
  const onUpdateDocumentBody = useCallback(
    async (body: string) => {
      if (!documentId) return;
      const updated = await updateDirectiveDocument(documentId, { body });
      setDoc(updated);
      // Tell the sidebar to refetch the directive's document list so the
      // status chip flips from `shipped` back to `active` (and any title
      // changes propagate). Cheap — folders only refetch when open.
      onDocumentChanged();
    },
    [documentId, onDocumentChanged],
  );

  // ---- Empty / error / loading states ------------------------------------
  if (!directiveId) {
    return (
      <div className="flex-1 flex items-center justify-center h-full">
        <p className="text-[#556677] font-mono text-[12px]">
          {listLoading
            ? "Loading documents..."
            : hasDirectives
            ? "Select a document from the sidebar"
            : "No directives yet — create one from the legacy UI"}
        </p>
      </div>
    );
  }

  if (directiveLoading && !directive) {
    return (
      <div className="flex-1 flex items-center justify-center h-full">
        <p className="text-[#556677] font-mono text-[12px]">Loading directive...</p>
      </div>
    );
  }

  if (!directive) {
    return (
      <div className="flex-1 flex items-center justify-center h-full">
        <p className="text-[#7788aa] font-mono text-[12px]">Directive not found</p>
      </div>
    );
  }

  // --- Task path: task row clicked in the sidebar ------------------------
  // Renders the live transcript via DocumentTaskStream. Selection wins over
  // the document path when both are somehow present (defensive).
  if (selection?.taskId) {
    const taskId = selection.taskId;
    // Resolve a human label for the task: orchestrator/completion are
    // labelled by role; step tasks borrow the step name; everything else
    // is an ephemeral and just shows the task id slice. Look-up uses the
    // already-fetched directive (with steps).
    const stepWithTask = directive.steps.find((s) => s.taskId === taskId);
    const label =
      taskId === directive.orchestratorTaskId
        ? "orchestrator"
        : taskId === directive.completionTaskId
        ? "completion"
        : stepWithTask?.name ?? taskId.slice(0, 8);
    const isStepBound =
      taskId === directive.orchestratorTaskId ||
      taskId === directive.completionTaskId ||
      !!stepWithTask;
    return (
      <div className="flex-1 flex flex-col h-full overflow-hidden">
        <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
          <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
            <FileIcon />
            <span>directives /</span>
            <span className="text-[#9bc3ff]">
              {directive.title.trim().length > 0
                ? directive.title
                : directive.id.slice(0, 8)}
            </span>
            <span>/</span>
            <span className="text-white">{label}</span>
          </div>
        </div>
        <DocumentTaskStream
          taskId={taskId}
          label={label}
          ephemeral={!isStepBound}
        />
      </div>
    );
  }

  // --- Document path: documentId selected --------------------------------
  if (documentId) {
    if (docLoading && !doc) {
      return (
        <div className="flex-1 flex items-center justify-center h-full">
          <p className="text-[#556677] font-mono text-[12px]">Loading document...</p>
        </div>
      );
    }
    if (docError) {
      return (
        <div className="flex-1 flex items-center justify-center h-full">
          <p className="text-red-400 font-mono text-[12px]">{docError}</p>
        </div>
      );
    }
    if (!doc) {
      return (
        <div className="flex-1 flex items-center justify-center h-full">
          <p className="text-[#7788aa] font-mono text-[12px]">Document not found</p>
        </div>
      );
    }

    // Synthesise a directive-shaped object whose `goal` is the document body.
    // DocumentEditor was originally written against DirectiveWithSteps, so we
    // can keep its shape by overriding `goal` with `doc.body` and `title`
    // with the document's filename label. The steps panel still draws from
    // the real directive (passed through StepsBlockContextProvider).
    const docTitle = `${fileLabel(doc, directive)}.md`;
    const directiveAsDocument = {
      ...directive,
      goal: doc.body,
      title: docTitle,
    };

    return (
      <div className="flex-1 flex flex-col h-full overflow-hidden">
        {/* Breadcrumb — directives / <directive title> / <document title>.md */}
        <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
          <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
            <FileIcon />
            <span>directives /</span>
            <span className="text-[#9bc3ff]">
              {directive.title.trim().length > 0
                ? directive.title
                : directive.id.slice(0, 8)}
            </span>
            <span>/</span>
            <span className="text-white">{docTitle}</span>
            {doc.status === "shipped" && (
              <span className="ml-2 text-[#75aafc] normal-case">shipped</span>
            )}
            {doc.status === "archived" && (
              <span className="ml-2 text-[#7788aa] normal-case">archived</span>
            )}
            {doc.status === "draft" && (
              <span className="ml-2 text-[#556677] normal-case">draft</span>
            )}
            {!!directive.orchestratorTaskId && (
              <span className="ml-auto inline-flex items-center gap-1 text-yellow-400">
                <span className="inline-block w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse" />
                orchestrator running
              </span>
            )}
          </div>
        </div>

        <DocumentEditor
          // Keying by document id ensures the Lexical editor remounts cleanly
          // when the user switches documents, so the previous doc's body
          // doesn't bleed into the new one.
          key={doc.id}
          directive={directiveAsDocument}
          onUpdateGoal={onUpdateDocumentBody}
          onCleanup={async () => {
            await cleanup();
          }}
          onCreatePR={async () => {
            await createPR();
          }}
          onPickUpOrders={async () => {
            await pickUpOrders();
          }}
        />
      </div>
    );
  }

  // --- Legacy fallback: directive selected but no document chosen --------
  // We only ever land here transiently while the page resolves the default
  // document selection, so we render a thin "loading" placeholder rather
  // than the full goal editor (which would be confusing alongside the new
  // multi-document model).
  return (
    <div className="flex-1 flex flex-col h-full overflow-hidden">
      <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
        <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
          <FileIcon />
          <span>directives /</span>
          <span className="text-[#9bc3ff]">
            {directive.title.trim().length > 0
              ? directive.title
              : directive.id.slice(0, 8)}
          </span>
        </div>
      </div>
      <div className="flex-1 flex items-center justify-center">
        <p className="text-[#556677] font-mono text-[12px]">
          Select a document, or click "+ New document" to create one.
        </p>
      </div>
    </div>
  );
}

// =============================================================================
// Page
// =============================================================================

export default function DocumentDirectivesPage() {
  const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
  const navigate = useNavigate();
  const { id: routeDirectiveId } = useParams<{ id: string }>();
  const [searchParams, setSearchParams] = useSearchParams();
  const { directives, loading: listLoading } = useDirectives();

  // refreshNonce — bumped to tell open directive folders to refetch their
  // document lists (after a create or save).
  const [refreshNonce, setRefreshNonce] = useState(0);
  const bumpRefresh = useCallback(() => setRefreshNonce((n) => n + 1), []);

  // Derive the SidebarSelection from the URL. The route param is the
  // directive id; ?document=:id and ?task=:id pick a specific child. Exactly
  // one of taskId/documentId can be set; if both happen to be present in the
  // URL (which shouldn't happen via our nav code) we prefer ?task= since
  // task selection is the more disruptive action.
  const selection: SidebarSelection | null = useMemo(() => {
    if (!routeDirectiveId) return null;
    const taskId = searchParams.get("task");
    const documentId = searchParams.get("document");
    if (taskId) return { directiveId: routeDirectiveId, taskId, documentId: null };
    if (documentId) return { directiveId: routeDirectiveId, taskId: null, documentId };
    return { directiveId: routeDirectiveId, taskId: null, documentId: null };
  }, [routeDirectiveId, searchParams]);

  useEffect(() => {
    if (!authLoading && isAuthConfigured && !isAuthenticated) {
      navigate("/login");
    }
  }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);

  // ------------------------------------------------------------------
  // Default-selection: when the user clicks a directive's folder header (or
  // lands on /directives/:id without ?document=) we pick the first active or
  // draft document and update the URL to point at it. This avoids the
  // "directive selected, but nothing in the editor" intermediate state.
  // ------------------------------------------------------------------
  const lastResolvedRef = useRef<string | null>(null);
  useEffect(() => {
    if (!routeDirectiveId) {
      lastResolvedRef.current = null;
      return;
    }
    // Only auto-resolve when no document/task has been picked yet, AND we
    // haven't already resolved this directive in a prior tick (otherwise
    // navigating away from the doc would instantly re-pick the same one).
    if (selection?.documentId || selection?.taskId) {
      lastResolvedRef.current = routeDirectiveId;
      return;
    }
    if (lastResolvedRef.current === routeDirectiveId) return;
    lastResolvedRef.current = routeDirectiveId;

    let cancelled = false;
    listDirectiveDocuments(routeDirectiveId)
      .then((list) => {
        if (cancelled) return;
        // Prefer the first 'active' doc; fall back to the first 'draft'.
        const firstActive = list.find((d) => d.status === "active");
        const firstDraft = list.find((d) => d.status === "draft");
        const pick = firstActive ?? firstDraft;
        if (pick) {
          setSearchParams(
            (prev) => {
              const next = new URLSearchParams(prev);
              next.set("document", pick.id);
              next.delete("task");
              return next;
            },
            { replace: true },
          );
        }
      })
      .catch(() => {
        // Swallow — the editor pane will show "Document not found" and the
        // user can click "+ New document" to recover.
      });
    return () => {
      cancelled = true;
    };
  }, [routeDirectiveId, selection?.documentId, selection?.taskId, setSearchParams]);

  const handleSelectDocument = useCallback(
    (directiveId: string, doc: DirectiveDocument) => {
      navigate(`/directives/${directiveId}?document=${doc.id}`);
    },
    [navigate],
  );

  // When the user clicks a directive folder header (not a document row), we
  // jump to /directives/:id without ?document= — the default-selection
  // effect above will then pick the first active doc.
  const handleSelectDirective = useCallback(
    (directiveId: string) => {
      if (routeDirectiveId === directiveId) return;
      navigate(`/directives/${directiveId}`);
    },
    [navigate, routeDirectiveId],
  );

  const handleCreateDocument = useCallback(
    async (directive: DirectiveSummary) => {
      const created = await createDirectiveDocument(directive.id, {
        title: "",
        body: "",
      });
      bumpRefresh();
      // Navigate to the new doc so it's selected immediately.
      navigate(`/directives/${directive.id}?document=${created.id}`);
    },
    [bumpRefresh, navigate],
  );

  // Click on a task or step row → open the live transcript pane via
  // ?task=<id>. EditorShell switches to DocumentTaskStream when this is set.
  const handleSelectTask = useCallback(
    (directiveId: string, taskId: string) => {
      navigate(`/directives/${directiveId}?task=${taskId}`);
    },
    [navigate],
  );

  // Modal state for the two new creation surfaces in the sidebar:
  //   * + New contract  → opens NewContractModal, calls useDirectives.create
  //   * + New ephemeral task (per directive) → opens NewEphemeralTaskModal
  const { create: createDirective } = useDirectives();
  const [showNewContract, setShowNewContract] = useState(false);
  const [newEphemeralFor, setNewEphemeralFor] = useState<DirectiveSummary | null>(null);

  const handleSubmitNewContract = useCallback(
    async (title: string, goal: string, repositoryUrl: string) => {
      const d = await createDirective({
        title,
        goal,
        repositoryUrl: repositoryUrl.length > 0 ? repositoryUrl : undefined,
      });
      setShowNewContract(false);
      navigate(`/directives/${d.id}`);
    },
    [createDirective, navigate],
  );

  const handleSubmitNewEphemeral = useCallback(
    async (name: string, plan: string) => {
      if (!newEphemeralFor) return;
      const task = await createDirectiveTask(newEphemeralFor.id, { name, plan });
      const target = newEphemeralFor.id;
      setNewEphemeralFor(null);
      bumpRefresh();
      navigate(`/directives/${target}?task=${task.id}`);
    },
    [newEphemeralFor, bumpRefresh, navigate],
  );

  // Right-click context menu state. Right-clicking any directive header
  // opens the menu; menu actions (start/pause/archive/delete/PR/etc.) hit
  // the directives API and trigger a sidebar refresh on success.
  const { refresh: refreshDirectiveList } = useDirectives();
  const [contextMenu, setContextMenu] = useState<{
    x: number;
    y: number;
    directive: DirectiveSummary;
  } | null>(null);

  const handleContextMenu = useCallback(
    (e: React.MouseEvent, directive: DirectiveSummary) => {
      e.preventDefault();
      e.stopPropagation();
      setContextMenu({ x: e.clientX, y: e.clientY, directive });
    },
    [],
  );

  const closeContextMenu = useCallback(() => setContextMenu(null), []);

  const runAction = useCallback(
    async (action: () => Promise<unknown>, errMsg: string) => {
      try {
        await action();
        await refreshDirectiveList();
        bumpRefresh();
      } catch (err) {
        // eslint-disable-next-line no-console
        console.error(`[makima] ${errMsg}`, err);
        alert(
          err instanceof Error ? `${errMsg}: ${err.message}` : errMsg,
        );
      }
    },
    [refreshDirectiveList, bumpRefresh],
  );

  if (authLoading) {
    return (
      <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
        <Masthead showNav />
        <main className="flex-1 flex items-center justify-center">
          <p className="text-[#7788aa] font-mono text-sm">Loading...</p>
        </main>
      </div>
    );
  }

  return (
    // h-screen + overflow-hidden so the page itself never scrolls; the
    // sidebar and editor pane each manage their own scroll via flex-1
    // children with overflow-y-auto. Previously we set
    // height: calc(100vh - 80px) on <main>, which assumed an 80px masthead
    // and quietly clipped content when the masthead was taller (or pushed
    // the page below the viewport on shorter screens, which made the
    // whole page scroll instead of the sidebar/editor independently).
    <div className="relative z-10 h-screen flex flex-col bg-[#0a1628] overflow-hidden">
      <Masthead showNav />
      <main className="flex-1 flex min-h-0 overflow-hidden">
        {/* Left: file-tree sidebar — independent scroll. */}
        <div className="w-[260px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col bg-[#091428]">
          <DocumentSidebar
            directives={directives}
            loading={listLoading}
            selection={selection}
            onSelectDocument={handleSelectDocument}
            onSelectDirective={handleSelectDirective}
            onCreateDocument={handleCreateDocument}
            onCreateContract={() => setShowNewContract(true)}
            onCreateEphemeralTask={(d) => setNewEphemeralFor(d)}
            onContextMenu={handleContextMenu}
            onSelectTask={handleSelectTask}
            refreshNonce={refreshNonce}
          />
        </div>

        {/* Right: Lexical editor */}
        <EditorShell
          selection={selection}
          hasDirectives={directives.length > 0}
          listLoading={listLoading}
          onDocumentChanged={bumpRefresh}
        />
      </main>

      {showNewContract && (
        <NewContractModal
          onClose={() => setShowNewContract(false)}
          onSubmit={handleSubmitNewContract}
        />
      )}
      {newEphemeralFor && (
        <NewEphemeralTaskModal
          directive={newEphemeralFor}
          onClose={() => setNewEphemeralFor(null)}
          onSubmit={handleSubmitNewEphemeral}
        />
      )}

      {contextMenu && (
        <DirectiveContextMenu
          x={contextMenu.x}
          y={contextMenu.y}
          directive={contextMenu.directive}
          onClose={closeContextMenu}
          onStart={() =>
            runAction(
              () => startDirective(contextMenu.directive.id),
              "Failed to start contract",
            )
          }
          onPause={() =>
            runAction(
              () => pauseDirective(contextMenu.directive.id),
              "Failed to pause contract",
            )
          }
          onArchive={() =>
            runAction(
              () =>
                updateDirective(contextMenu.directive.id, {
                  status: "archived",
                }),
              "Failed to archive contract",
            )
          }
          onDelete={async () => {
            if (
              !window.confirm(
                `Delete "${contextMenu.directive.title}"? This cannot be undone.`,
              )
            ) {
              return;
            }
            await runAction(
              () => deleteDirective(contextMenu.directive.id),
              "Failed to delete contract",
            );
            // If the deleted contract was selected, clear the URL.
            if (selection?.directiveId === contextMenu.directive.id) {
              navigate("/directives");
            }
          }}
          onGoToPR={() => {
            if (contextMenu.directive.prUrl) {
              window.open(contextMenu.directive.prUrl, "_blank", "noreferrer");
            }
          }}
          onCreatePR={() =>
            runAction(
              () => createDirectivePR(contextMenu.directive.id),
              contextMenu.directive.prUrl
                ? "Failed to update PR"
                : "Failed to create PR",
            )
          }
          onAdvance={() =>
            runAction(
              () => advanceDirective(contextMenu.directive.id),
              "Failed to advance DAG",
            )
          }
          onCleanup={() =>
            runAction(
              () => cleanupDirective(contextMenu.directive.id),
              "Failed to clean up contract",
            )
          }
          onPickUpOrders={() =>
            runAction(
              () => pickUpOrders(contextMenu.directive.id),
              "Failed to pick up orders",
            )
          }
        />
      )}
    </div>
  );
}

/**
 * Modal for creating a new directive (= "contract" in the doc-mode UI).
 * Title + goal are required; repository_url is optional. On submit calls
 * useDirectives.create and navigates the user into the new directive.
 */
function NewContractModal({
  onClose,
  onSubmit,
}: {
  onClose: () => void;
  onSubmit: (title: string, goal: string, repositoryUrl: string) => Promise<void>;
}) {
  const [title, setTitle] = useState("");
  const [goal, setGoal] = useState("");
  const [repo, setRepo] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [err, setErr] = useState<string | null>(null);
  const titleRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    titleRef.current?.focus();
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    const t = title.trim();
    const g = goal.trim();
    if (!t || !g || submitting) return;
    setSubmitting(true);
    setErr(null);
    try {
      await onSubmit(t, g, repo.trim());
    } catch (caught) {
      setErr(caught instanceof Error ? caught.message : String(caught));
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
      onClick={onClose}
    >
      <form
        onSubmit={submit}
        onClick={(e) => e.stopPropagation()}
        className="w-[520px] max-w-[90vw] bg-[#0a1628] border border-[rgba(117,170,252,0.4)] shadow-xl flex flex-col"
      >
        <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.25)]">
          <p className="text-[10px] font-mono text-[#556677] uppercase tracking-wide">
            New contract
          </p>
        </div>
        <div className="px-4 py-4 space-y-3">
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Title
            </label>
            <input
              ref={titleRef}
              type="text"
              value={title}
              onChange={(e) => setTitle(e.target.value)}
              placeholder="e.g. Migrate auth to Supabase"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]"
            />
          </div>
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Goal
            </label>
            <textarea
              value={goal}
              onChange={(e) => setGoal(e.target.value)}
              onKeyDown={(e) => {
                if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
                  void submit(e as unknown as React.FormEvent);
                }
              }}
              rows={4}
              placeholder="Describe what the contract should achieve"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566] resize-none"
            />
          </div>
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Repository URL (optional)
            </label>
            <input
              type="text"
              value={repo}
              onChange={(e) => setRepo(e.target.value)}
              placeholder="e.g. https://github.com/owner/repo"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]"
            />
          </div>
          {err && (
            <p className="text-[11px] font-mono text-red-400">{err}</p>
          )}
        </div>
        <div className="px-4 py-3 border-t border-dashed border-[rgba(117,170,252,0.25)] flex items-center justify-end gap-2">
          <button
            type="button"
            onClick={onClose}
            className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-[#7788aa] border border-[#2a3a5a] hover:text-white"
          >
            Cancel
          </button>
          <button
            type="submit"
            disabled={!title.trim() || !goal.trim() || submitting}
            className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-emerald-300 border border-emerald-700/60 hover:border-emerald-400 disabled:opacity-40 disabled:cursor-not-allowed"
          >
            {submitting ? "Creating…" : "Create contract"}
          </button>
        </div>
      </form>
    </div>
  );
}

/**
 * Modal for spawning an ephemeral task under a directive. Mirrors the
 * existing right-click "+ New task" flow.
 */
function NewEphemeralTaskModal({
  directive,
  onClose,
  onSubmit,
}: {
  directive: DirectiveSummary;
  onClose: () => void;
  onSubmit: (name: string, plan: string) => Promise<void>;
}) {
  const [name, setName] = useState("");
  const [plan, setPlan] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [err, setErr] = useState<string | null>(null);
  const nameRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    nameRef.current?.focus();
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    const n = name.trim();
    const p = plan.trim();
    if (!n || !p || submitting) return;
    setSubmitting(true);
    setErr(null);
    try {
      await onSubmit(n, p);
    } catch (caught) {
      setErr(caught instanceof Error ? caught.message : String(caught));
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
      onClick={onClose}
    >
      <form
        onSubmit={submit}
        onClick={(e) => e.stopPropagation()}
        className="w-[480px] max-w-[90vw] bg-[#0a1628] border border-[rgba(117,170,252,0.4)] shadow-xl flex flex-col"
      >
        <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.25)]">
          <p className="text-[10px] font-mono text-[#556677] uppercase tracking-wide">
            New ephemeral task in
          </p>
          <p className="text-[12px] font-mono text-white truncate">
            {directive.title}
          </p>
        </div>
        <div className="px-4 py-4 space-y-3">
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Name
            </label>
            <input
              ref={nameRef}
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="e.g. Investigate flaky test"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]"
            />
          </div>
          <div className="space-y-1">
            <label className="text-[10px] font-mono text-[#7788aa] uppercase tracking-wide">
              Plan / instructions
            </label>
            <textarea
              value={plan}
              onChange={(e) => setPlan(e.target.value)}
              onKeyDown={(e) => {
                if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
                  void submit(e as unknown as React.FormEvent);
                }
              }}
              rows={5}
              placeholder="What should the task do?"
              className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566] resize-none"
            />
          </div>
          {err && (
            <p className="text-[11px] font-mono text-red-400">{err}</p>
          )}
        </div>
        <div className="px-4 py-3 border-t border-dashed border-[rgba(117,170,252,0.25)] flex items-center justify-end gap-2">
          <button
            type="button"
            onClick={onClose}
            className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-[#7788aa] border border-[#2a3a5a] hover:text-white"
          >
            Cancel
          </button>
          <button
            type="submit"
            disabled={!name.trim() || !plan.trim() || submitting}
            className="px-3 py-1.5 text-[11px] font-mono uppercase tracking-wide text-[#c084fc] border border-[#c084fc]/40 hover:border-[#c084fc] disabled:opacity-40 disabled:cursor-not-allowed"
          >
            {submitting ? "Spawning…" : "Spawn task"}
          </button>
        </div>
      </form>
    </div>
  );
}
