import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import {
  createFileRoute,
  Link,
  redirect,
  useNavigate,
  useSearch,
} from "@tanstack/react-router";
import { useQuery, useMutation, useClient } from "urql";
import { type ColumnDef } from "@tanstack/react-table";
import {
  Loader2,
  Trash2,
  Brain,
  Search,
  X,
  ArrowLeft,
  RotateCcw,
  RefreshCw,
} from "lucide-react";
import { toast } from "sonner";
import {
  AgentsListQuery,
  TenantMembersListQuery,
  MemoryRecordsQuery,
  MemorySearchQuery,
  MemorySystemConfigQuery,
  DeleteMemoryRecordMutation,
  UpdateMemoryRecordMutation,
  ThreadIdleLearningRunsQuery,
  RollbackThreadIdleLearningRunMutation,
  ResetUserWikiCursorMutation,
  CompileUserWikiNowMutation,
} from "@/lib/graphql-queries";
import {
  MemoryGraph,
  type MemoryGraphHandle,
  type MemoryGraphNode,
} from "@thinkwork/graph";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { useTenant } from "@/context/TenantContext";
import { useBreadcrumbs } from "@/context/BreadcrumbContext";
import { PageSkeleton } from "@/components/PageSkeleton";
import { DataTable } from "@/components/ui/data-table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  Sheet,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetDescription,
} from "@/components/ui/sheet";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";

type MemoryView = "memories" | "graph";

function isMemoryView(v: unknown): v is MemoryView {
  return v === "memories" || v === "graph";
}

export const Route = createFileRoute("/_authed/_tenant/memory/")({
  beforeLoad: ({ search }) => {
    throw redirect({
      to: "/knowledge/memory",
      search: search as any,
      replace: true,
    });
  },
  component: MemoryPage,
  validateSearch: (
    search: Record<string, unknown>,
  ): { agent?: string; view?: MemoryView } => ({
    ...(typeof search.agent === "string" && search.agent
      ? { agent: search.agent }
      : {}),
    ...(isMemoryView(search.view) ? { view: search.view } : {}),
  }),
});

type MemoryPageProps = {
  routeBase?: "/memory" | "/knowledge/memory";
  embedded?: boolean;
  breadcrumbs?: Parameters<typeof useBreadcrumbs>[0];
};

/** Parse <topic name="...">content</topic> tags into structured sections.
 *  Handles both closed and unclosed topic tags. */
function parseMemoryTopics(text: string): { topic: string; content: string }[] {
  // Match closed tags: <topic name="X">content</topic>
  // AND unclosed tags: <topic name="X">content (rest of string or until next <topic)
  const regex =
    /<topic\s+name="([^"]*)">\s*([\s\S]*?)(?:<\/topic>|(?=<topic\s)|$)/g;
  const sections: { topic: string; content: string }[] = [];
  let lastIndex = 0;
  let match;
  while ((match = regex.exec(text)) !== null) {
    const before = text.slice(lastIndex, match.index).trim();
    if (before) sections.push({ topic: "", content: before });
    sections.push({ topic: match[1], content: match[2].trim() });
    lastIndex = regex.lastIndex;
  }
  const after = text.slice(lastIndex).trim();
  if (after) sections.push({ topic: "", content: after });
  if (sections.length === 0) sections.push({ topic: "", content: text });
  return sections;
}

/** Strip topic XML tags for plain text display */
function stripTopicTags(text: string): string {
  return text
    .replace(/<\/?topic[^>]*>/g, "")
    .replace(/\s+/g, " ")
    .trim();
}

/** Render parsed memory sections */
function MemoryContent({ text }: { text: string }) {
  const sections = parseMemoryTopics(text);
  return (
    <div className="space-y-3">
      {sections.map((s, i) => (
        <div key={i}>
          {s.topic && (
            <p className="font-medium text-xs text-muted-foreground uppercase tracking-wider mb-1">
              {s.topic}
            </p>
          )}
          <p className="text-sm whitespace-pre-wrap leading-relaxed">
            {s.content}
          </p>
        </div>
      ))}
    </div>
  );
}

type MemoryRow = {
  memoryRecordId: string;
  userId: string;
  text: string;
  createdAt: string | null;
  updatedAt: string | null;
  strategyId: string | null;
  namespace: string | null;
  strategy: string | null;
  score: number | null;
  agentSlug: string | null;
  agentName: string;
  factType: string | null;
  confidence: number | null;
  eventDate: string | null;
  occurredStart: string | null;
  occurredEnd: string | null;
  mentionedAt: string | null;
  tags: string[] | null;
  accessCount: number;
  proofCount: number | null;
  context: string | null;
  threadId: string | null;
};

type UserScope = {
  userId: string;
  label: string;
  agentIds: string[];
};

type IdleLearningRunRow = {
  id: string;
  threadId: string;
  status: string;
  startedAt?: string | null;
  finishedAt?: string | null;
  error?: string | null;
  canRollback: boolean;
  changedFiles: Array<{ path: string; hindsightStatus?: string | null }>;
};

function agentUserId(agent: any): string | null {
  return agent.humanPairId ?? agent.humanPair?.id ?? null;
}

function agentUserLabel(agent: any): string {
  return agent.humanPair?.name ?? agent.humanPair?.email ?? agent.name;
}

const STRATEGY_COLORS: Record<string, string> = {
  semantic: "bg-blue-500/20 text-blue-400",
  preferences: "bg-purple-500/20 text-purple-400",
  summaries: "bg-yellow-500/20 text-yellow-400",
  episodes: "bg-green-500/20 text-green-400",
  reflections: "bg-orange-500/20 text-orange-400",
};

function StrategyBadge({ strategy }: { strategy: string | null }) {
  if (!strategy) return null;
  const colors = STRATEGY_COLORS[strategy] || "bg-muted text-muted-foreground";
  const label = strategy.charAt(0).toUpperCase() + strategy.slice(1);
  return <Badge className={`${colors} font-normal text-xs`}>{label}</Badge>;
}

function IdleLearningRunsPanel({
  runs,
  fetching,
  rollingBackRunId,
  onRollback,
}: {
  runs: IdleLearningRunRow[];
  fetching: boolean;
  rollingBackRunId: string | null;
  onRollback: (runId: string) => void;
}) {
  if (fetching) {
    return (
      <div className="flex shrink-0 items-center gap-2 rounded-md border border-border px-3 py-2 text-xs text-muted-foreground">
        <Loader2 className="h-3.5 w-3.5 animate-spin" />
        Loading learning runs...
      </div>
    );
  }
  if (runs.length === 0) return null;

  return (
    <div className="shrink-0 rounded-md border border-border">
      <div className="flex items-center justify-between border-b border-border px-3 py-2">
        <div>
          <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
            Idle learning runs
          </p>
          <p className="text-xs text-muted-foreground">
            Latest automatic requester-memory updates for this user.
          </p>
        </div>
      </div>
      <div className="divide-y divide-border">
        {runs.map((run) => (
          <div
            key={run.id}
            className="flex flex-wrap items-center gap-3 px-3 py-2 text-xs"
          >
            <Badge variant="outline" className="font-normal">
              {run.status}
            </Badge>
            <span className="text-muted-foreground">
              {run.finishedAt || run.startedAt
                ? new Date(run.finishedAt ?? run.startedAt!).toLocaleString(
                    "en-US",
                    {
                      month: "short",
                      day: "numeric",
                      hour: "numeric",
                      minute: "2-digit",
                    },
                  )
                : "Not started"}
            </span>
            <span className="min-w-0 flex-1 truncate text-muted-foreground">
              {run.changedFiles.length > 0
                ? run.changedFiles.map((file) => file.path).join(", ")
                : run.error || "No memory file changes"}
            </span>
            {run.threadId && (
              <Link
                to="/threads/$threadId"
                params={{ threadId: run.threadId }}
                className="text-sky-400 hover:text-sky-300 hover:underline"
              >
                Thread
              </Link>
            )}
            {run.canRollback && (
              <AlertDialog>
                <AlertDialogTrigger asChild>
                  <Button
                    variant="outline"
                    size="sm"
                    className="h-7 px-2 text-xs"
                    disabled={rollingBackRunId === run.id}
                  >
                    {rollingBackRunId === run.id ? (
                      <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
                    ) : (
                      <RotateCcw className="mr-1.5 h-3.5 w-3.5" />
                    )}
                    Roll back
                  </Button>
                </AlertDialogTrigger>
                <AlertDialogContent>
                  <AlertDialogHeader>
                    <AlertDialogTitle>
                      Roll back memory changes?
                    </AlertDialogTitle>
                    <AlertDialogDescription>
                      This restores the requester memory files from the
                      snapshots captured before this idle-learning run and
                      re-syncs the markdown memory document.
                    </AlertDialogDescription>
                  </AlertDialogHeader>
                  <AlertDialogFooter>
                    <AlertDialogCancel>Cancel</AlertDialogCancel>
                    <AlertDialogAction onClick={() => onRollback(run.id)}>
                      Roll back
                    </AlertDialogAction>
                  </AlertDialogFooter>
                </AlertDialogContent>
              </AlertDialog>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

export function MemoryPage({
  routeBase = "/memory",
  embedded = false,
  breadcrumbs = [{ label: "Memories" }],
}: MemoryPageProps = {}) {
  const { tenantId } = useTenant();
  const { agent, view: viewParam } = useSearch({ strict: false }) as {
    agent?: string;
    view?: MemoryView;
  };
  const navigate = useNavigate();
  const selectedAgentId = agent ?? "all";
  const view: MemoryView = viewParam ?? "memories";
  const [searchQuery, setSearchQuery] = useState("");
  const [activeSearch, setActiveSearch] = useState("");
  const graphRef = useRef<MemoryGraphHandle>(null);

  const updateFilters = useCallback(
    (next: { agent?: string; view?: MemoryView }) => {
      navigate({
        to: routeBase,
        search: {
          ...(next.agent && next.agent !== "all" ? { agent: next.agent } : {}),
          ...(next.view && next.view !== "memories" ? { view: next.view } : {}),
        },
        replace: true,
      });
    },
    [navigate, routeBase],
  );
  const setSelectedAgentId = useCallback(
    (nextAgent: string) => updateFilters({ agent: nextAgent, view }),
    [updateFilters, view],
  );
  const setView = useCallback(
    (nextView: MemoryView) =>
      updateFilters({ agent: selectedAgentId, view: nextView }),
    [updateFilters, selectedAgentId],
  );

  // Graph node detail sheet
  const [graphNode, setGraphNode] = useState<MemoryGraphNode | null>(null);
  const [graphNodeEdges, setGraphNodeEdges] = useState<
    {
      label: string;
      targetLabel: string;
      targetType: string;
      targetId: string;
    }[]
  >([]);
  const [graphSheetOpen, setGraphSheetOpen] = useState(false);
  const [graphNodeHistory, setGraphNodeHistory] = useState<
    {
      node: MemoryGraphNode;
      edges: {
        label: string;
        targetLabel: string;
        targetType: string;
        targetId: string;
      }[];
    }[]
  >([]);

  // Detect which memory backends are wired up at runtime. The Knowledge
  // Graph view is only meaningful when Hindsight is deployed, so hide the
  // toggle entirely when it's disabled — managed AgentCore Memory alone
  // has no entity graph to render.
  const [memorySystemConfigResult] = useQuery({
    query: MemorySystemConfigQuery,
  });
  // Don't coalesce until the query resolves — otherwise on refresh with
  // ?view=graph the effect below sees `false` during load and strips the
  // param from the URL before the real value arrives.
  const hindsightConfigLoaded = memorySystemConfigResult.data !== undefined;
  const hindsightEnabled =
    memorySystemConfigResult.data?.memorySystemConfig?.hindsightEnabled ??
    false;

  // If Hindsight is disabled mid-session (e.g. after a deploy), force the
  // view back to memories so we never render an empty graph.
  useEffect(() => {
    if (hindsightConfigLoaded && !hindsightEnabled && view === "graph") {
      setView("memories");
    }
  }, [hindsightConfigLoaded, hindsightEnabled, view, setView]);

  const [agentsResult] = useQuery({
    query: AgentsListQuery,
    variables: { tenantId: tenantId ?? "" },
    pause: !tenantId,
  });
  const [membersResult] = useQuery({
    query: TenantMembersListQuery,
    variables: { tenantId: tenantId ?? "" },
    pause: !tenantId,
  });

  const agents = useMemo(
    () =>
      [...(agentsResult.data?.agents ?? [])].sort((a, b) =>
        a.name.localeCompare(b.name),
      ),
    [agentsResult.data],
  );
  const memberScopes = useMemo<UserScope[]>(() => {
    const members = membersResult.data?.tenantMembers ?? [];
    return members
      .filter((m) => m.principalType.toLowerCase() === "user" && m.user)
      .map((m) => ({
        userId: m.user!.id,
        label: m.user!.name ?? m.user!.email,
        agentIds: [],
      }))
      .sort((a, b) => a.label.localeCompare(b.label));
  }, [membersResult.data]);
  const userScopes = useMemo<UserScope[]>(() => {
    const map = new Map<string, UserScope>();
    for (const scope of memberScopes) {
      map.set(scope.userId, { ...scope, agentIds: [...scope.agentIds] });
    }
    for (const a of agents) {
      const userId = agentUserId(a);
      if (!userId) continue;
      const existing = map.get(userId);
      if (existing) {
        existing.agentIds.push(a.id);
        continue;
      }
      map.set(userId, {
        userId,
        label: agentUserLabel(a),
        agentIds: [a.id],
      });
    }
    return [...map.values()].sort((a, b) => a.label.localeCompare(b.label));
  }, [agents, memberScopes]);
  const userLabels = useMemo(() => {
    const map: Record<string, string> = {};
    for (const scope of userScopes) map[scope.userId] = scope.label;
    return map;
  }, [userScopes]);
  const userLabelsBySlug = useMemo(() => {
    const map: Record<string, string> = {};
    for (const scope of userScopes) map[`user_${scope.userId}`] = scope.label;
    return map;
  }, [userScopes]);

  const isAllAgents = selectedAgentId === "all";
  const selectedScope = userScopes.find(
    (scope) =>
      scope.userId === selectedAgentId ||
      scope.agentIds.includes(selectedAgentId),
  );
  const selectedScopeId = isAllAgents
    ? "all"
    : (selectedScope?.userId ?? selectedAgentId);
  const effectiveUserId = isAllAgents
    ? userScopes[0]?.userId
    : selectedScope?.userId;
  const effectiveUserIds = useMemo(
    () => (isAllAgents ? userScopes.map((scope) => scope.userId) : []),
    [isAllAgents, userScopes],
  );

  const namespace = "all";

  // Single-user path uses urql normally. All-users fans out per user
  // below (mirroring MemoryGraph) to avoid the resolver's "all" branch,
  // which requires a tenant claim in the JWT that Google OAuth users
  // don't always have populated yet.
  const [memoryResult, refetchMemory] = useQuery({
    query: MemoryRecordsQuery,
    variables: { userId: effectiveUserId ?? "", namespace },
    pause: !!activeSearch || isAllAgents || !effectiveUserId,
  });
  const [idleRunsResult, refetchIdleRuns] = useQuery({
    query: ThreadIdleLearningRunsQuery,
    variables: { userId: effectiveUserId ?? "", limit: 8 },
    pause: isAllAgents || !effectiveUserId,
  });

  // Multi-user fan-out
  const client = useClient();
  const [multiRecords, setMultiRecords] = useState<any[] | null>(null);
  const [multiFetching, setMultiFetching] = useState(false);

  const fetchAllAgentRecords = useCallback(async () => {
    if (!isAllAgents || userScopes.length === 0 || activeSearch) return;
    setMultiFetching(true);
    try {
      const perAgent = await Promise.all(
        userScopes.map(async (scope) => {
          try {
            const res = await client
              .query(MemoryRecordsQuery, { userId: scope.userId, namespace })
              .toPromise();
            return ((res.data?.memoryRecords ?? []) as any[]).map((r) => ({
              ...r,
              __userId: scope.userId,
              __userLabel: scope.label,
            }));
          } catch {
            return [];
          }
        }),
      );
      setMultiRecords(perAgent.flat());
    } finally {
      setMultiFetching(false);
    }
  }, [isAllAgents, userScopes, activeSearch, client, namespace]);

  useEffect(() => {
    if (isAllAgents && !activeSearch) {
      fetchAllAgentRecords();
    } else {
      setMultiRecords(null);
    }
  }, [isAllAgents, activeSearch, fetchAllAgentRecords]);

  // Search mode
  const searchUserId = isAllAgents
    ? (userScopes[0]?.userId ?? "")
    : (effectiveUserId ?? "");
  const [searchResult] = useQuery({
    query: MemorySearchQuery,
    variables: {
      userId: searchUserId,
      query: activeSearch,
      limit: 50,
    },
    pause: !activeSearch || !searchUserId,
  });

  const [, deleteMemory] = useMutation(DeleteMemoryRecordMutation);
  const [, updateMemory] = useMutation(UpdateMemoryRecordMutation);
  const [, rollbackIdleRun] = useMutation(
    RollbackThreadIdleLearningRunMutation,
  );
  const [, resetUserWikiCursor] = useMutation(ResetUserWikiCursorMutation);
  const [, compileUserWikiNow] = useMutation(CompileUserWikiNowMutation);
  const [selectedRecord, setSelectedRecord] = useState<MemoryRow | null>(null);
  const [sheetOpen, setSheetOpen] = useState(false);
  const [editing, setEditing] = useState(false);
  const [editValue, setEditValue] = useState("");
  const [saving, setSaving] = useState(false);
  const [deleting, setDeleting] = useState(false);
  const [rollingBackRunId, setRollingBackRunId] = useState<string | null>(null);
  const [processingMemory, setProcessingMemory] = useState(false);

  useBreadcrumbs(breadcrumbs);

  const mapRecord = (r: any): MemoryRow => ({
    memoryRecordId: r.memoryRecordId,
    userId: r.__userId ?? effectiveUserId ?? searchUserId ?? selectedAgentId,
    text: r.content?.text ?? "",
    createdAt: r.createdAt ?? null,
    updatedAt: r.updatedAt ?? null,
    strategyId: r.strategyId ?? null,
    namespace: r.namespace ?? null,
    strategy:
      r.strategy ?? inferStrategy(r.strategyId ?? "", r.namespace ?? ""),
    score: r.score ?? null,
    agentSlug: r.userSlug ?? r.agentSlug ?? r.namespace ?? null,
    agentName:
      r.__userLabel ??
      userLabelsBySlug[r.userSlug ?? r.agentSlug ?? r.namespace ?? ""] ??
      r.userSlug ??
      r.agentSlug ??
      "",
    factType: r.factType ?? null,
    confidence: r.confidence ?? null,
    eventDate: r.eventDate ?? null,
    occurredStart: r.occurredStart ?? null,
    occurredEnd: r.occurredEnd ?? null,
    mentionedAt: r.mentionedAt ?? null,
    tags: r.tags ?? null,
    accessCount: r.accessCount ?? 0,
    proofCount: r.proofCount ?? null,
    context: r.context ?? null,
    threadId: r.threadId ?? null,
  });

  const rawRecords: any[] = activeSearch
    ? (searchResult.data?.memorySearch?.records ?? [])
    : isAllAgents
      ? (multiRecords ?? [])
      : (memoryResult.data?.memoryRecords ?? []);

  const rows: MemoryRow[] = rawRecords
    .map(mapRecord)
    .sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));

  const columns: ColumnDef<MemoryRow>[] = [
    {
      accessorKey: "createdAt",
      header: "Date",
      size: 140,
      cell: ({ row }) =>
        row.original.createdAt
          ? new Date(row.original.createdAt).toLocaleDateString("en-US", {
              month: "short",
              day: "numeric",
              hour: "numeric",
              minute: "2-digit",
            })
          : "\u2014",
    },
    {
      accessorKey: "agentName",
      header: "User",
      size: 100,
      cell: ({ row }) => (
        <span className="text-muted-foreground text-xs">
          {row.original.agentName}
        </span>
      ),
    },
    {
      accessorKey: "factType",
      header: "Type",
      size: 90,
      cell: ({ row }) => <StrategyBadge strategy={row.original.strategy} />,
    },
    {
      accessorKey: "text",
      header: "Memory",
      cell: ({ row }) => (
        <span className="truncate block">
          {stripTopicTags(row.original.text)}
        </span>
      ),
    },
  ];

  const handleRowClick = useCallback((row: MemoryRow) => {
    setSelectedRecord(row);
    setEditValue(row.text);
    setEditing(false);
    setSheetOpen(true);
  }, []);

  const handleSearch = () => {
    setActiveSearch(searchQuery.trim());
  };

  const handleSave = async () => {
    if (!selectedRecord) return;
    setSaving(true);
    try {
      await updateMemory({
        userId: selectedRecord.userId,
        memoryRecordId: selectedRecord.memoryRecordId,
        content: editValue,
      });
      setSelectedRecord({ ...selectedRecord, text: editValue });
      setEditing(false);
      if (isAllAgents) fetchAllAgentRecords();
      else refetchMemory({ requestPolicy: "network-only" });
    } finally {
      setSaving(false);
    }
  };

  const handleDelete = async () => {
    if (!selectedRecord) return;
    setDeleting(true);
    try {
      await deleteMemory({
        userId: selectedRecord.userId,
        memoryRecordId: selectedRecord.memoryRecordId,
      });
      setSheetOpen(false);
      setSelectedRecord(null);
      if (isAllAgents) fetchAllAgentRecords();
      else refetchMemory({ requestPolicy: "network-only" });
    } finally {
      setDeleting(false);
    }
  };

  const handleRollbackIdleRun = async (runId: string) => {
    if (!effectiveUserId) return;
    setRollingBackRunId(runId);
    try {
      await rollbackIdleRun({ userId: effectiveUserId, runId });
      refetchIdleRuns({ requestPolicy: "network-only" });
      if (isAllAgents) fetchAllAgentRecords();
      else refetchMemory({ requestPolicy: "network-only" });
    } finally {
      setRollingBackRunId(null);
    }
  };

  const handleProcessMemory = async () => {
    if (!tenantId || !effectiveUserId || processingMemory) return;

    const label = selectedScope?.label ?? "user";
    setProcessingMemory(true);
    try {
      const resetResult = await resetUserWikiCursor({
        tenantId,
        userId: effectiveUserId,
        force: true,
        includeBrain: true,
      });
      if (resetResult.error) {
        toast.error(`Couldn't reset ${label}'s compiled memory.`);
        return;
      }

      const compileResult = await compileUserWikiNow({
        tenantId,
        userId: effectiveUserId,
        forceNew: true,
      });
      if (compileResult.error) {
        toast.error(`Couldn't start processing memory for ${label}.`);
        return;
      }

      const jobId = compileResult.data?.compileWikiNow?.id;
      toast.success(
        jobId
          ? `Processing memory for ${label}.`
          : `Memory processing requested for ${label}.`,
      );
      refetchIdleRuns({ requestPolicy: "network-only" });
      if (isAllAgents) fetchAllAgentRecords();
      else refetchMemory({ requestPolicy: "network-only" });
      graphRef.current?.refetch();
    } finally {
      setProcessingMemory(false);
    }
  };

  if (agentsResult.fetching && !agentsResult.data) return <PageSkeleton />;

  const isLoading = activeSearch
    ? searchResult.fetching && !searchResult.data
    : isAllAgents
      ? multiFetching && multiRecords === null
      : memoryResult.fetching && !memoryResult.data;

  return (
    <div
      className={
        embedded
          ? "flex h-full min-w-0 flex-col"
          : "flex flex-col -m-6 h-[calc(100%+48px)] min-w-0"
      }
    >
      <div
        className={
          embedded
            ? "relative z-10 flex shrink-0 flex-wrap items-center justify-between gap-3 pb-3"
            : "relative z-10 flex shrink-0 flex-wrap items-center justify-between gap-3 px-4 pb-3"
        }
      >
        <div className="relative w-80 max-w-full">
          <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
          <Input
            placeholder="Search memories..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && handleSearch()}
            className="pl-9"
          />
          {searchQuery && (
            <button
              onClick={() => {
                setSearchQuery("");
                setActiveSearch("");
              }}
              className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
            >
              <X className="h-3.5 w-3.5" />
            </button>
          )}
        </div>
        <div className="flex shrink-0 items-center gap-2">
          {!isAllAgents && effectiveUserId && tenantId && (
            <AlertDialog>
              <AlertDialogTrigger asChild>
                <Button
                  variant="outline"
                  size="sm"
                  disabled={processingMemory}
                  className="gap-2"
                >
                  {processingMemory ? (
                    <Loader2 className="h-4 w-4 animate-spin" />
                  ) : (
                    <RefreshCw className="h-4 w-4" />
                  )}
                  Process memory
                </Button>
              </AlertDialogTrigger>
              <AlertDialogContent>
                <AlertDialogHeader>
                  <AlertDialogTitle>Process user memory?</AlertDialogTitle>
                  <AlertDialogDescription>
                    This archives the selected user&apos;s compiled Wiki and
                    Brain pages, clears the compile cursor, and starts a fresh
                    compile from their memory records.
                  </AlertDialogDescription>
                </AlertDialogHeader>
                <AlertDialogFooter>
                  <AlertDialogCancel>Cancel</AlertDialogCancel>
                  <AlertDialogAction
                    onClick={handleProcessMemory}
                    disabled={processingMemory}
                  >
                    {processingMemory ? "Processing..." : "Process memory"}
                  </AlertDialogAction>
                </AlertDialogFooter>
              </AlertDialogContent>
            </AlertDialog>
          )}
          <ToggleGroup
            type="single"
            value={view}
            onValueChange={(v) => v && setView(v as MemoryView)}
            variant="outline"
          >
            <ToggleGroupItem value="memories" className="px-3 text-xs">
              Table
            </ToggleGroupItem>
            {hindsightEnabled && (
              <ToggleGroupItem value="graph" className="px-3 text-xs">
                Graph
              </ToggleGroupItem>
            )}
          </ToggleGroup>
          <Select value={selectedScopeId} onValueChange={setSelectedAgentId}>
            <SelectTrigger className="w-44">
              <SelectValue placeholder="Select user" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="all">All Users</SelectItem>
              {userScopes.map((scope) => (
                <SelectItem key={scope.userId} value={scope.userId}>
                  {scope.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
      </div>

      <div className={embedded ? "min-h-0 flex-1" : "min-h-0 flex-1 px-4"}>
        {view === "graph" ? (
          <div className="h-full relative border border-border rounded-lg overflow-hidden">
            <MemoryGraph
              ref={graphRef}
              userId={isAllAgents ? undefined : effectiveUserId}
              userIds={isAllAgents ? effectiveUserIds : undefined}
              agentNames={userLabels}
              searchQuery={searchQuery || undefined}
              onNodeClick={(node, edges) => {
                setGraphNode(node);
                setGraphNodeEdges(edges);
                setGraphNodeHistory([]);
                setGraphSheetOpen(true);
              }}
            />
          </div>
        ) : (
          <div className="flex h-full min-h-0 flex-col gap-3">
            {!isAllAgents && (
              <IdleLearningRunsPanel
                runs={
                  (idleRunsResult.data?.threadIdleLearningRuns ??
                    []) as IdleLearningRunRow[]
                }
                fetching={idleRunsResult.fetching && !idleRunsResult.data}
                rollingBackRunId={rollingBackRunId}
                onRollback={handleRollbackIdleRun}
              />
            )}
            {isLoading ? (
              <div className="flex items-center justify-center gap-2 py-12 text-sm text-muted-foreground">
                <Loader2 className="h-4 w-4 animate-spin" /> Loading memories...
              </div>
            ) : rows.length === 0 ? (
              <div className="flex flex-col items-center gap-3 py-16 text-center">
                <Brain className="h-12 w-12 text-muted-foreground/40" />
                <p className="text-sm text-muted-foreground">
                  {activeSearch
                    ? "No memories match your search."
                    : "No memories yet."}
                </p>
              </div>
            ) : (
              <div className="min-h-0 flex-1">
                <DataTable
                  columns={columns}
                  data={rows}
                  onRowClick={handleRowClick}
                  scrollable
                  pageSize={25}
                  tableClassName="table-fixed"
                />
              </div>
            )}
          </div>
        )}
      </div>

      {/* Detail sheet */}
      <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
        <SheetContent className="sm:max-w-lg flex flex-col">
          <SheetHeader className="p-6 pb-0">
            <div className="flex items-center justify-between">
              <div>
                <SheetTitle>Memory Detail</SheetTitle>
                <SheetDescription>
                  {selectedRecord?.createdAt
                    ? `Created ${new Date(
                        selectedRecord.createdAt,
                      ).toLocaleDateString("en-US", {
                        month: "long",
                        day: "numeric",
                        year: "numeric",
                        hour: "numeric",
                        minute: "2-digit",
                      })}`
                    : "Memory record"}
                </SheetDescription>
              </div>
              {!editing && hindsightEnabled && (
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => setEditing(true)}
                >
                  Edit
                </Button>
              )}
            </div>
          </SheetHeader>
          <div className="flex-1 overflow-y-auto px-6 pt-4">
            {editing ? (
              <>
                <Textarea
                  value={editValue}
                  onChange={(e) => setEditValue(e.target.value)}
                  className="min-h-[200px] font-mono text-sm resize-none"
                />
                <div className="flex items-center gap-2 pt-4">
                  <AlertDialog>
                    <AlertDialogTrigger asChild>
                      <Button variant="outline" size="sm" disabled={deleting}>
                        {deleting ? (
                          <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
                        ) : (
                          <Trash2 className="h-3.5 w-3.5 mr-1.5" />
                        )}
                        Delete
                      </Button>
                    </AlertDialogTrigger>
                    <AlertDialogContent>
                      <AlertDialogHeader>
                        <AlertDialogTitle>Delete memory?</AlertDialogTitle>
                        <AlertDialogDescription>
                          This memory will be permanently removed.
                        </AlertDialogDescription>
                      </AlertDialogHeader>
                      <AlertDialogFooter>
                        <AlertDialogCancel>Cancel</AlertDialogCancel>
                        <AlertDialogAction onClick={handleDelete}>
                          Delete
                        </AlertDialogAction>
                      </AlertDialogFooter>
                    </AlertDialogContent>
                  </AlertDialog>
                  <div className="flex-1" />
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => {
                      setEditValue(selectedRecord?.text ?? "");
                      setEditing(false);
                    }}
                  >
                    Cancel
                  </Button>
                  <Button
                    size="sm"
                    onClick={handleSave}
                    disabled={saving || editValue === selectedRecord?.text}
                  >
                    {saving && (
                      <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
                    )}
                    Save
                  </Button>
                </div>
              </>
            ) : (
              <div className="space-y-4">
                {selectedRecord?.factType && (
                  <div className="flex items-center gap-2">
                    <p className="text-xs text-muted-foreground uppercase tracking-wider font-medium">
                      {selectedRecord.factType}
                    </p>
                    <StrategyBadge
                      strategy={selectedRecord?.strategy ?? null}
                    />
                  </div>
                )}
                <MemoryContent text={selectedRecord?.text ?? ""} />

                {selectedRecord?.threadId && (
                  <Link
                    to="/threads/$threadId"
                    params={{ threadId: selectedRecord.threadId }}
                    className="inline-flex items-center gap-1 text-xs text-sky-400 hover:text-sky-300 hover:underline"
                  >
                    View source thread →
                  </Link>
                )}

                {!hindsightEnabled && (
                  <p className="text-xs text-muted-foreground">
                    AgentCore memory records are immutable in this deployment.
                    To change a fact, create a new memory instead.
                  </p>
                )}

                <div className="border-t border-muted pt-4 space-y-3">
                  <div className="grid grid-cols-2 gap-3 text-xs">
                    {selectedRecord?.agentName && (
                      <div>
                        <p className="text-muted-foreground uppercase tracking-wider font-medium">
                          Agent
                        </p>
                        <p className="mt-0.5">{selectedRecord.agentName}</p>
                      </div>
                    )}
                    {selectedRecord?.confidence != null && (
                      <div>
                        <p className="text-muted-foreground uppercase tracking-wider font-medium">
                          Confidence
                        </p>
                        <p className="mt-0.5">
                          {(selectedRecord.confidence * 100).toFixed(0)}%
                        </p>
                      </div>
                    )}
                    {selectedRecord?.accessCount != null &&
                      selectedRecord.accessCount > 0 && (
                        <div>
                          <p className="text-muted-foreground uppercase tracking-wider font-medium">
                            Access Count
                          </p>
                          <p className="mt-0.5">{selectedRecord.accessCount}</p>
                        </div>
                      )}
                    {selectedRecord?.proofCount != null && (
                      <div>
                        <p className="text-muted-foreground uppercase tracking-wider font-medium">
                          Proof Count
                        </p>
                        <p className="mt-0.5">{selectedRecord.proofCount}</p>
                      </div>
                    )}
                    {selectedRecord?.context && (
                      <div>
                        <p className="text-muted-foreground uppercase tracking-wider font-medium">
                          Context
                        </p>
                        <p className="mt-0.5 truncate">
                          {selectedRecord.context}
                        </p>
                      </div>
                    )}
                  </div>

                  {/* Bi-temporal dates */}
                  <div className="grid grid-cols-2 gap-3 text-xs">
                    {selectedRecord?.eventDate && (
                      <div>
                        <p className="text-muted-foreground uppercase tracking-wider font-medium">
                          Event Date
                        </p>
                        <p className="mt-0.5">
                          {new Date(
                            selectedRecord.eventDate,
                          ).toLocaleDateString("en-US", {
                            month: "short",
                            day: "numeric",
                            year: "numeric",
                          })}
                        </p>
                      </div>
                    )}
                    {selectedRecord?.mentionedAt && (
                      <div>
                        <p className="text-muted-foreground uppercase tracking-wider font-medium">
                          Mentioned At
                        </p>
                        <p className="mt-0.5">
                          {new Date(
                            selectedRecord.mentionedAt,
                          ).toLocaleDateString("en-US", {
                            month: "short",
                            day: "numeric",
                            year: "numeric",
                          })}
                        </p>
                      </div>
                    )}
                    {selectedRecord?.occurredStart && (
                      <div>
                        <p className="text-muted-foreground uppercase tracking-wider font-medium">
                          Occurred Start
                        </p>
                        <p className="mt-0.5">
                          {new Date(
                            selectedRecord.occurredStart,
                          ).toLocaleDateString("en-US", {
                            month: "short",
                            day: "numeric",
                            year: "numeric",
                          })}
                        </p>
                      </div>
                    )}
                    {selectedRecord?.occurredEnd && (
                      <div>
                        <p className="text-muted-foreground uppercase tracking-wider font-medium">
                          Occurred End
                        </p>
                        <p className="mt-0.5">
                          {new Date(
                            selectedRecord.occurredEnd,
                          ).toLocaleDateString("en-US", {
                            month: "short",
                            day: "numeric",
                            year: "numeric",
                          })}
                        </p>
                      </div>
                    )}
                  </div>

                  {/* Tags */}
                  {selectedRecord?.tags && selectedRecord.tags.length > 0 && (
                    <div className="text-xs">
                      <p className="text-muted-foreground uppercase tracking-wider font-medium mb-1">
                        Tags
                      </p>
                      <div className="flex flex-wrap gap-1">
                        {selectedRecord.tags.map((t) => (
                          <Badge
                            key={t}
                            variant="outline"
                            className="font-normal text-xs"
                          >
                            {t}
                          </Badge>
                        ))}
                      </div>
                    </div>
                  )}
                </div>
              </div>
            )}
          </div>
        </SheetContent>
      </Sheet>

      {/* Graph node detail sheet */}
      <Sheet open={graphSheetOpen} onOpenChange={setGraphSheetOpen}>
        <SheetContent className="sm:max-w-lg flex flex-col">
          <SheetHeader className="p-6 pb-0">
            <SheetTitle className="flex items-center gap-2">
              {graphNodeHistory.length > 0 && (
                <button
                  onClick={() => {
                    const prev = graphNodeHistory[graphNodeHistory.length - 1];
                    setGraphNodeHistory((h) => h.slice(0, -1));
                    setGraphNode(prev.node);
                    setGraphNodeEdges(prev.edges);
                  }}
                  className="text-muted-foreground hover:text-foreground -ml-1 mr-1"
                >
                  <ArrowLeft className="h-4 w-4" />
                </button>
              )}
              {graphNode?.nodeType === "memory" ? "Memory" : graphNode?.label}
              <Badge
                className={`font-normal text-xs ${
                  graphNode?.nodeType === "memory"
                    ? "bg-pink-500/20 text-pink-400"
                    : "bg-sky-500/20 text-sky-400"
                }`}
              >
                {graphNode?.nodeType === "memory"
                  ? (graphNode?.strategy ?? "memory")
                  : (graphNode?.entityType ?? "entity")}
              </Badge>
            </SheetTitle>
            <SheetDescription>
              {graphNode?.nodeType === "memory"
                ? `Memory node — ${graphNodeEdges.length} connection${graphNodeEdges.length !== 1 ? "s" : ""}`
                : `Entity — ${graphNodeEdges.length} mention${graphNodeEdges.length !== 1 ? "s" : ""}`}
            </SheetDescription>
          </SheetHeader>

          <div className="flex-1 overflow-y-auto px-6 pt-4 space-y-4">
            {graphNode?.nodeType === "memory" && (
              <MemoryContent text={graphNode.label} />
            )}

            {graphNode?.latestThreadId && (
              <Link
                to="/threads/$threadId"
                params={{ threadId: graphNode.latestThreadId }}
                className="inline-flex items-center gap-1 text-xs text-sky-400 hover:text-sky-300 hover:underline"
              >
                View source thread →
              </Link>
            )}

            {graphNodeEdges.length > 0 && (
              <div>
                <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
                  {graphNode?.nodeType === "memory"
                    ? "Mentions"
                    : "Mentioned by"}
                </h4>
                <div className="space-y-2">
                  {graphNodeEdges.map((edge, i) => (
                    <div
                      key={i}
                      className="flex items-start gap-2 text-sm rounded-md bg-muted/30 px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
                      onClick={() => {
                        const result = graphRef.current?.getNodeWithEdges(
                          edge.targetId,
                        );
                        if (result && graphNode) {
                          setGraphNodeHistory((h) => [
                            ...h,
                            { node: graphNode, edges: graphNodeEdges },
                          ]);
                          setGraphNode(result.node);
                          setGraphNodeEdges(result.edges);
                        }
                      }}
                    >
                      <Badge
                        variant="outline"
                        className={`shrink-0 text-[10px] mt-0.5 ${
                          edge.targetType === "memory"
                            ? "border-pink-500/30 text-pink-400"
                            : "border-sky-500/30 text-sky-400"
                        }`}
                      >
                        {edge.targetType === "memory" ? "Memory" : "Entity"}
                      </Badge>
                      <div className="min-w-0">
                        <p className="font-medium text-foreground truncate">
                          {stripTopicTags(edge.targetLabel)}
                        </p>
                        {edge.label && (
                          <p className="text-xs text-muted-foreground">
                            {edge.label}
                          </p>
                        )}
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            )}

            {graphNodeEdges.length === 0 && (
              <p className="text-sm text-muted-foreground">
                No connections found for this node.
              </p>
            )}
          </div>
        </SheetContent>
      </Sheet>
    </div>
  );
}

function inferStrategy(strategyId: string, namespace: string): string {
  if (strategyId.includes("semantic")) return "semantic";
  if (strategyId.includes("summary") || strategyId.includes("Summar"))
    return "summaries";
  if (strategyId.includes("Preference") || strategyId.includes("preference"))
    return "preferences";
  if (strategyId.includes("Episode") || strategyId.includes("episode"))
    return "episodes";
  if (namespace.startsWith("assistant_")) return "semantic";
  if (namespace.startsWith("preferences_")) return "preferences";
  if (namespace.startsWith("session_")) return "summaries";
  if (namespace.startsWith("episodes_")) return "episodes";
  return "semantic";
}
