<script lang="ts">
  import { Cherry, ChevronDown, ChevronRight, CloudOff, EllipsisVertical, GitBranch, GitMerge, LoaderCircle, RotateCcw, StickyNote, Tag, X } from "@lucide/svelte";
  import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";

  interface GraphSegment {
    fromCol: number;
    toCol: number;
    color: string;
    branches: string[];
  }

  interface GraphRow {
    dotCol: number;
    dotColor: string;
    branchLabels: string[];
    top: GraphSegment[];
    bottom: GraphSegment[];
  }

  interface VisibleCommitEntry {
    commit: GitCommit;
    graphCommit: GitCommit;
  }

  type BranchVisibilityMode = "focus" | "local" | "all" | "custom";
  type CommitRefKind = "local" | "remote" | "head";

  interface CommitBranchDecoration {
    label: string;
    kind: CommitRefKind;
    current: boolean;
    trackedRemote: string;
    localOnly: boolean;
    representedBranches: string[];
  }

  interface CommitRefSummary {
    branches: CommitBranchDecoration[];
    tags: string[];
    other: string[];
    primaryBranch: CommitBranchDecoration | null;
    primaryTag: string;
    overflowCount: number;
  }

  const GRAPH_COLORS = [
    "#69a7ff", "#5bd18a", "#d8a74a", "#ba82ff",
    "#ff7c9f", "#48c7d8", "#c5cf54", "#e18c55",
  ];
  const GRAPH_LANE = 18;
  const GRAPH_REF_ARM = 16;
  const GRAPH_VISIBILITY_STORAGE_PREFIX = "gitlite.graphVisibility.v1:";
  const commitDateFormatter = new Intl.DateTimeFormat(undefined, {
    dateStyle: "medium",
    timeStyle: "short",
  });

  interface Props {
    commits: GitCommit[];
    localBranchNames: string[];
    localBranchUpstreams: Record<string, string>;
    remoteBranchNames: string[];
    activeBranch: string;
    activeUpstream: string;
    activeAhead: number;
    activeBehind: number;
    repositoryKey: string;
    hasRepository: boolean;
    isBusy: boolean;
    hasMore: boolean;
    isLoadingMore: boolean;
    loadMoreError: string;
    expandedCommitHashes: Set<string>;
    selectedCommitHash: string;
    onLoadMore: () => void | Promise<void>;
    onRestoreCommit: (commit: GitCommit) => void;
    onPreviewCommitFile: (commit: GitCommit, file: GitCommitFile) => void;
    onToggleCommitFiles: (hash: string) => void;
    onCreateBranchFromCommit: (commit: GitCommit) => void;
    onCherryPickCommit: (commit: GitCommit) => void;
    onRevertCommit: (commit: GitCommit) => void;
    onOpenCommitNote: (commit: GitCommit) => void;
    onLoadCommitNote: (commit: GitCommit) => Promise<string | null>;
    onSelectCommit: (commit: GitCommit) => void;
  }

  let {
    commits = [],
    localBranchNames = [],
    localBranchUpstreams = {},
    remoteBranchNames = [],
    activeBranch = "",
    activeUpstream = "",
    activeAhead = 0,
    activeBehind = 0,
    repositoryKey = "",
    hasRepository = false,
    isBusy = false,
    hasMore = false,
    isLoadingMore = false,
    loadMoreError = "",
    expandedCommitHashes = new Set(),
    selectedCommitHash = "",
    onLoadMore = () => {},
    onRestoreCommit = () => {},
    onPreviewCommitFile = () => {},
    onToggleCommitFiles = () => {},
    onCreateBranchFromCommit = () => {},
    onCherryPickCommit = () => {},
    onRevertCommit = () => {},
    onOpenCommitNote = () => {},
    onLoadCommitNote = async () => null,
    onSelectCommit = () => {},
  }: Props = $props();

  let branchVisibilityMode = $state<BranchVisibilityMode>("focus");
  let customVisibleBranches = $state<Set<string>>(new Set());
  let loadedVisibilityRepository = $state("");
  let branchDialogOpen = $state(false);
  let localBranchGroupOpen = $state(true);
  let remoteBranchGroupOpen = $state(false);
  let expandedRefsCommitHash = $state("");
  let panelElement = $state<HTMLElement | null>(null);
  let contextCommit = $state<GitCommit | null>(null);
  let contextMenuX = $state(0);
  let contextMenuY = $state(0);
  let notePreviews = $state<Record<string, string>>({});
  let notePreviewLoading = $state<Set<string>>(new Set());
  let notePreviewErrors = $state<Set<string>>(new Set());

  $effect(() => {
    repositoryKey;
    notePreviews = {};
    notePreviewLoading = new Set();
    notePreviewErrors = new Set();
  });

  function observeHistoryEnd(node: HTMLElement) {
    const root = node.closest<HTMLElement>(".history-list");
    const observer = new IntersectionObserver((entries) => {
      if (entries.some((entry) => entry.isIntersecting) && hasMore && !isLoadingMore && !loadMoreError && !isBusy) {
        void onLoadMore();
      }
    }, { root, rootMargin: "240px 0px" });
    observer.observe(node);
    return { destroy: () => observer.disconnect() };
  }

  function laneColor(col: number): string {
    return GRAPH_COLORS[((col % GRAPH_COLORS.length) + GRAPH_COLORS.length) % GRAPH_COLORS.length];
  }

  function graphColX(col: number): number {
    return col * GRAPH_LANE + GRAPH_LANE / 2;
  }

  function graphPath(seg: GraphSegment, fromY: number, toY: number): string {
    const x1 = graphColX(seg.fromCol);
    const x2 = graphColX(seg.toCol);
    if (x1 === x2) return `M ${x1} ${fromY} L ${x2} ${toY}`;
    const midY = (fromY + toY) / 2;
    return `M ${x1} ${fromY} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${toY}`;
  }

  function computeGraph(items: GitCommit[], branchMembership = new Map<string, string[]>()): { rows: GraphRow[]; columns: number } {
    const rows: GraphRow[] = [];
    let lanes: (string | null)[] = [];
    let laneBranches: string[][] = [];
    let maxColumns = 1;

    for (const commit of items) {
      const before = lanes.slice();
      const beforeBranches = laneBranches.map((branches) => branches.slice());

      let col = before.indexOf(commit.hash);
      if (col === -1) {
        col = before.indexOf(null);
        if (col === -1) col = before.length;
      }

      const after = before.slice();
      while (after.length <= col) after.push(null);
      const afterBranches = beforeBranches.map((branches) => branches.slice());
      while (afterBranches.length <= col) afterBranches.push([]);

      for (let k = 0; k < after.length; k++) {
        if (after[k] === commit.hash) {
          after[k] = null;
          afterBranches[k] = [];
        }
      }

      const currentBranches = branchMembership.get(commit.hash) ?? localBranchRefs(commit);
      const commitBranches = uniqueStrings([...(beforeBranches[col] ?? []), ...currentBranches]);
      const firstParent = commit.parents[0] ?? null;
      after[col] = firstParent;
      afterBranches[col] = firstParent
        ? (branchMembership.get(firstParent) ?? commitBranches).slice()
        : [];

      const fromCommit = new Set<number>([col]);
      for (let p = 1; p < commit.parents.length; p++) {
        let slot = after.indexOf(null);
        if (slot === -1) { slot = after.length; after.push(null); afterBranches.push([]); }
        after[slot] = commit.parents[p];
        afterBranches[slot] = (branchMembership.get(commit.parents[p]) ?? commitBranches).slice();
        fromCommit.add(slot);
      }

      const top: GraphSegment[] = [];
      for (let k = 0; k < before.length; k++) {
        const target = before[k];
        if (target == null) continue;
        top.push({ fromCol: k, toCol: target === commit.hash ? col : k, color: laneColor(k), branches: beforeBranches[k] ?? [] });
      }

      const bottom: GraphSegment[] = [];
      for (let k = 0; k < after.length; k++) {
        if (after[k] == null) continue;
        bottom.push({
          fromCol: fromCommit.has(k) ? col : k,
          toCol: k,
          color: laneColor(k),
          branches: fromCommit.has(k) ? commitBranches : afterBranches[k] ?? [],
        });
      }

      rows.push({ dotCol: col, dotColor: laneColor(col), branchLabels: currentBranches, top, bottom });

      lanes = after.slice();
      laneBranches = afterBranches.map((branches) => branches.slice());
      while (lanes.length > 0 && lanes[lanes.length - 1] == null) {
        lanes.pop();
        laneBranches.pop();
      }
      maxColumns = Math.max(maxColumns, before.length, after.length, col + 1);
    }

    return { rows, columns: maxColumns };
  }

  function statusLabel(kind: FileStatusKind): string {
    return kind;
  }

  function displayCommitFile(file: GitCommitFile): string {
    return file.old_path ? `${file.old_path} -> ${file.path}` : file.path;
  }

  function baseName(path: string): string {
    return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
  }

  function commitFileName(file: GitCommitFile): string {
    return file.old_path
      ? `${baseName(file.old_path)} -> ${baseName(file.path)}`
      : baseName(file.path);
  }

  function authorInitials(name: string): string {
    const parts = name.trim().split(/\s+/).filter(Boolean);
    if (parts.length === 0) return "?";
    if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
    return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
  }

  function commitKind(commit: GitCommit): "merge" | "root" | "commit" {
    if (commit.parents.length > 1) return "merge";
    if (commit.parents.length === 0) return "root";
    return "commit";
  }

  function commitKindLabel(commit: GitCommit): string {
    const kind = commitKind(commit);
    if (kind === "merge") return "merge";
    if (kind === "root") return "root";
    return "commit";
  }

  let localBranchNameSet = $derived(new Set(localBranchNames));
  let remoteBranchNameSet = $derived(new Set(remoteBranchNames));

  function uniqueStrings(values: string[]): string[] {
    return [...new Set(values.filter(Boolean))];
  }

  let graphBranchNames = $derived(uniqueStrings([...localBranchNames, ...remoteBranchNames, activeUpstream].filter(Boolean)));
  let graphBranchNameSet = $derived(new Set(graphBranchNames));

  function focusBranchNames(): string[] {
    const focused = uniqueStrings([activeBranch, activeUpstream].filter((branch) => graphBranchNameSet.has(branch)));
    if (focused.length > 0) return focused;
    return localBranchNames[0] ? [localBranchNames[0]] : graphBranchNames.slice(0, 1);
  }

  function branchNamesForMode(): string[] {
    if (branchVisibilityMode === "focus") return focusBranchNames();
    if (branchVisibilityMode === "local") return localBranchNames;
    if (branchVisibilityMode === "all") return graphBranchNames;
    return graphBranchNames.filter((branch) => customVisibleBranches.has(branch));
  }

  let visibleGraphBranchNames = $derived(branchNamesForMode());
  let visibleGraphBranchNameSet = $derived(new Set(visibleGraphBranchNames));

  function branchIsVisible(branch: string): boolean {
    return visibleGraphBranchNameSet.has(branch);
  }

  function commitHoverTitle(commit: GitCommit, row: GraphRow | undefined): string {
    const directBranches = graphBranchRefs(commit).filter(branchIsVisible);
    if (directBranches.length > 0) return `Branches: ${directBranches.join(", ")}`;

    const containingBranches = (row?.branchLabels ?? []).filter(branchIsVisible);
    if (containingBranches.length === 0) return commit.short_hash;
    return `Branches containing this commit: ${containingBranches.join(", ")}`;
  }

  function segmentIsVisible(segment: GraphSegment): boolean {
    return segment.branches.length === 0 || segment.branches.some(branchIsVisible);
  }

  function syncClassForBranches(labels: string[]): "" | "ahead" | "behind" {
    if (!activeBranch || !activeUpstream) return "";
    const hasLocal = labels.includes(activeBranch);
    const hasRemote = labels.includes(activeUpstream);
    if (hasLocal && !hasRemote) return "ahead";
    if (hasRemote && !hasLocal) return "behind";
    return "";
  }

  function branchesAreVisible(branches: string[]): boolean {
    if (graphBranchNames.length === 0) return true;
    return branches.some((branch) => visibleGraphBranchNameSet.has(branch));
  }

  function rowGraphIsVisible(row: GraphRow | undefined): boolean {
    return branchesAreVisible(row?.branchLabels ?? []);
  }

  function nearestVisibleGraphParents(
    hash: string,
    visibleHashes: Set<string>,
    commitByHash: Map<string, GitCommit>,
    seen: Set<string>,
  ): string[] {
    if (visibleHashes.has(hash)) return [hash];
    if (seen.has(hash)) return [];
    seen.add(hash);

    const commit = commitByHash.get(hash);
    if (!commit) return [];
    return uniqueStrings(
      commit.parents.flatMap((parentHash) => (
        nearestVisibleGraphParents(parentHash, visibleHashes, commitByHash, new Set(seen))
      )),
    );
  }

  function branchMembershipByHash(items: GitCommit[]): Map<string, string[]> {
    const commitByHash = new Map(items.map((commit) => [commit.hash, commit]));
    const membership = new Map<string, Set<string>>();

    for (const commit of items) {
      for (const branch of graphBranchRefs(commit)) {
        const stack = [commit.hash];
        const seen = new Set<string>();

        while (stack.length > 0) {
          const hash = stack.pop();
          if (!hash || seen.has(hash)) continue;
          seen.add(hash);

          let branches = membership.get(hash);
          if (!branches) {
            branches = new Set<string>();
            membership.set(hash, branches);
          }
          branches.add(branch);

          const parentCommit = commitByHash.get(hash);
          if (parentCommit) stack.push(...parentCommit.parents);
        }
      }
    }

    return new Map(
      items.map((commit) => [
        commit.hash,
        graphBranchNames.filter((branch) => membership.get(commit.hash)?.has(branch)),
      ]),
    );
  }

  function visibleCommitEntriesForGraph(items: GitCommit[], branchMembership: Map<string, string[]>): VisibleCommitEntry[] {
    const visibleItems = items.filter((commit) => branchesAreVisible(branchMembership.get(commit.hash) ?? []));
    const visibleHashes = new Set(visibleItems.map((commit) => commit.hash));
    const commitByHash = new Map(items.map((commit) => [commit.hash, commit]));

    return visibleItems.map((commit) => {
      const parents = uniqueStrings(
        commit.parents.flatMap((parentHash) => (
          nearestVisibleGraphParents(parentHash, visibleHashes, commitByHash, new Set())
        )),
      );
      return { commit, graphCommit: { ...commit, parents } };
    });
  }

  function toggleGraphBranch(branch: string) {
    const next = branchVisibilityMode === "custom"
      ? new Set(customVisibleBranches)
      : new Set(visibleGraphBranchNames);
    if (next.has(branch)) next.delete(branch); else next.add(branch);
    customVisibleBranches = next;
    branchVisibilityMode = "custom";
  }

  function showAllGraphBranches() {
    branchVisibilityMode = "all";
  }

  function hideAllGraphBranches() {
    customVisibleBranches = new Set();
    branchVisibilityMode = "custom";
  }

  function showFocusGraphBranches() {
    branchVisibilityMode = "focus";
  }

  function openBranchDialog() {
    localBranchGroupOpen = true;
    remoteBranchGroupOpen = false;
    branchDialogOpen = true;
  }

  function closeBranchDialog() {
    branchDialogOpen = false;
  }

  function handleBranchDialogKeydown(event: KeyboardEvent) {
    if (branchDialogOpen && event.key === "Escape") {
      closeBranchDialog();
    }
  }

  function toggleCommitRefs(commit: GitCommit) {
    expandedRefsCommitHash = expandedRefsCommitHash === commit.hash ? "" : commit.hash;
  }

  function openCommitActionMenu(event: MouseEvent, commit: GitCommit) {
    event.preventDefault();
    event.stopPropagation();
    if (isBusy) return;

    if (contextCommit?.hash === commit.hash) {
      closeCommitContextMenu();
      return;
    }

    const panelRect = panelElement?.getBoundingClientRect();
    const buttonRect = event.currentTarget instanceof HTMLElement
      ? event.currentTarget.getBoundingClientRect()
      : null;
    const rawX = panelRect && buttonRect ? buttonRect.right - panelRect.left - 184 : event.offsetX;
    const rawY = panelRect && buttonRect ? buttonRect.bottom - panelRect.top + 4 : event.offsetY;
    const maxX = Math.max(8, (panelRect?.width ?? window.innerWidth) - 192);
    const maxY = Math.max(8, (panelRect?.height ?? window.innerHeight) - 96);

    contextCommit = commit;
    contextMenuX = Math.max(8, Math.min(rawX, maxX));
    contextMenuY = Math.max(8, Math.min(rawY, maxY));
  }

  function closeCommitContextMenu() {
    contextCommit = null;
  }

  async function createBranchFromContextCommit() {
    const commit = contextCommit;
    if (!commit || isBusy) return;
    closeCommitContextMenu();
    await onCreateBranchFromCommit(commit);
  }

  async function restoreContextCommit() {
    const commit = contextCommit;
    if (!commit || isBusy) return;
    closeCommitContextMenu();
    await onRestoreCommit(commit);
  }

  async function cherryPickContextCommit() {
    const commit = contextCommit;
    if (!commit || isBusy) return;
    closeCommitContextMenu();
    await onCherryPickCommit(commit);
  }

  async function revertContextCommit() {
    const commit = contextCommit;
    if (!commit || isBusy) return;
    closeCommitContextMenu();
    await onRevertCommit(commit);
  }

  async function openContextCommitNote() {
    const commit = contextCommit;
    if (!commit || isBusy) return;
    closeCommitContextMenu();
    onSelectCommit(commit);
    await onOpenCommitNote(commit);
  }

  async function openCommitNote(commit: GitCommit) {
    if (isBusy) return;
    onSelectCommit(commit);
    await onOpenCommitNote(commit);
  }

  async function loadCommitNotePreview(commit: GitCommit) {
    if (!commit.has_note || notePreviewLoading.has(commit.hash)) return;

    const loading = new Set(notePreviewLoading);
    loading.add(commit.hash);
    notePreviewLoading = loading;

    const errors = new Set(notePreviewErrors);
    errors.delete(commit.hash);
    notePreviewErrors = errors;

    try {
      const note = await onLoadCommitNote(commit);
      notePreviews = {
        ...notePreviews,
        [commit.hash]: note?.trim() || "This Git note is empty.",
      };
    } catch {
      const nextErrors = new Set(notePreviewErrors);
      nextErrors.add(commit.hash);
      notePreviewErrors = nextErrors;
    } finally {
      const nextLoading = new Set(notePreviewLoading);
      nextLoading.delete(commit.hash);
      notePreviewLoading = nextLoading;
    }
  }

  function handleWindowKeydown(event: KeyboardEvent) {
    if (event.key !== "Escape") return;
    closeCommitContextMenu();
    expandedRefsCommitHash = "";
    handleBranchDialogKeydown(event);
  }

  function localBranchRefs(commit: GitCommit): string[] {
    const seen = new Set<string>();
    const labels: string[] = [];
    for (const ref of commit.refs) {
      const label = refLabel(ref);
      if (!localBranchNameSet.has(label) || seen.has(label)) continue;
      seen.add(label);
      labels.push(label);
    }
    return labels;
  }

  function graphBranchRefs(commit: GitCommit): string[] {
    const seen = new Set<string>();
    const labels: string[] = [];
    for (const ref of commit.refs) {
      const label = refLabel(ref);
      if (!graphBranchNameSet.has(label) || seen.has(label)) continue;
      seen.add(label);
      labels.push(label);
    }
    return labels;
  }

  function refLabel(ref: string): string {
    return ref.replace(/^HEAD ->\s*/, "").replace(/^tag:\s*/, "");
  }

  function isSymbolicRemoteHead(ref: string): boolean {
    if (ref.startsWith("HEAD ->") || ref.startsWith("tag:")) return false;
    const label = refLabel(ref);
    return !localBranchNameSet.has(label) && /(?:^|\/)HEAD(?:\s*->|$)/.test(ref);
  }

  function configuredUpstreamForBranch(local: string): string {
    return localBranchUpstreams[local] ?? (local === activeBranch ? activeUpstream : "");
  }

  function matchingRemoteBranch(local: string, remoteBranches: string[]): string {
    const configuredUpstream = configuredUpstreamForBranch(local);
    return configuredUpstream && remoteBranches.includes(configuredUpstream) ? configuredUpstream : "";
  }

  function compareBranchDecorations(left: CommitBranchDecoration, right: CommitBranchDecoration): number {
    const priority = (item: CommitBranchDecoration) => {
      if (item.label === activeBranch) return 0;
      if (item.kind === "head") return 1;
      if (item.kind === "local") return 2;
      if (item.label === activeUpstream) return 3;
      return 4;
    };
    return priority(left) - priority(right) || left.label.localeCompare(right.label, undefined, { numeric: true });
  }

  function commitRefSummary(commit: GitCommit): CommitRefSummary {
    const local = new Set<string>();
    const remote = new Set<string>();
    const tags = new Set<string>();
    const other = new Set<string>();
    let detachedHead = false;

    for (const ref of commit.refs) {
      if (isSymbolicRemoteHead(ref)) continue;
      const label = refLabel(ref);
      if (!label) continue;
      if (ref.startsWith("tag:")) {
        tags.add(label);
      } else if (localBranchNameSet.has(label)) {
        local.add(label);
      } else if (remoteBranchNameSet.has(label) || label === activeUpstream) {
        remote.add(label);
      } else if (label === "HEAD") {
        detachedHead = true;
      } else {
        other.add(label);
      }
    }

    const remainingRemote = [...remote];
    const branches: CommitBranchDecoration[] = [...local].map((label) => {
      const configuredUpstream = configuredUpstreamForBranch(label);
      const trackedRemote = matchingRemoteBranch(label, remainingRemote);
      if (trackedRemote) remainingRemote.splice(remainingRemote.indexOf(trackedRemote), 1);
      return {
        label,
        kind: "local",
        current: label === activeBranch,
        trackedRemote,
        localOnly: !configuredUpstream,
        representedBranches: trackedRemote ? [label, trackedRemote] : [label],
      };
    });

    if (detachedHead) {
      branches.push({
        label: "HEAD",
        kind: "head",
        current: true,
        trackedRemote: "",
        localOnly: false,
        representedBranches: [],
      });
    }

    branches.push(...remainingRemote.map((label) => ({
      label,
      kind: "remote" as const,
      current: false,
      trackedRemote: "",
      localOnly: false,
      representedBranches: [label],
    })));

    const sortedBranches = branches.sort(compareBranchDecorations);
    const primaryBranch = sortedBranches.find(
      (branch) => branch.kind === "head" || branch.representedBranches.some(branchIsVisible),
    ) ?? null;
    const sortedTags = [...tags].sort((left, right) => left.localeCompare(right, undefined, { numeric: true }));
    const sortedOther = [...other].sort((left, right) => left.localeCompare(right));
    const primaryTag = sortedTags[0] ?? "";

    return {
      branches: sortedBranches,
      tags: sortedTags,
      other: sortedOther,
      primaryBranch,
      primaryTag,
      overflowCount:
        Math.max(0, sortedBranches.length - (primaryBranch ? 1 : 0))
        + Math.max(0, sortedTags.length - (primaryTag ? 1 : 0))
        + sortedOther.length,
    };
  }

  function branchStatusLabel(branch: CommitBranchDecoration): string {
    if (branch.label === activeBranch) {
      const parts = [];
      if (activeAhead > 0) parts.push(`↑${activeAhead}`);
      if (activeBehind > 0) parts.push(`↓${activeBehind}`);
      return parts.join(" ");
    }
    if (branch.label === activeUpstream && activeBehind > 0) return `↓${activeBehind}`;
    return "";
  }

  function branchDecorationTitle(branch: CommitBranchDecoration): string {
    if (branch.localOnly) {
      const status = branchStatusLabel(branch);
      return `${branch.label} · Local only — not published yet${status ? ` · ${status}` : ""}`;
    }
    const status = branchStatusLabel(branch);
    if (branch.trackedRemote) {
      return `${branch.label} · Tracks ${branch.trackedRemote}${status ? ` · ${status}` : ""}`;
    }
    if (status) return `${branch.label} · ${status}`;
    return branch.kind === "remote" ? `Remote branch ${branch.label}` : `Local branch ${branch.label}`;
  }

  function formatCommitDate(value: string): string {
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return value;
    return commitDateFormatter.format(date);
  }

  $effect(() => {
    const currentRepository = repositoryKey;
    if (loadedVisibilityRepository === currentRepository) return;
    loadedVisibilityRepository = currentRepository;
    expandedRefsCommitHash = "";

    if (!currentRepository) {
      branchVisibilityMode = "focus";
      customVisibleBranches = new Set();
      return;
    }

    try {
      const stored = localStorage.getItem(`${GRAPH_VISIBILITY_STORAGE_PREFIX}${currentRepository}`);
      if (!stored) {
        branchVisibilityMode = "focus";
        customVisibleBranches = new Set();
        return;
      }
      const parsed = JSON.parse(stored) as { mode?: unknown; branches?: unknown };
      const mode = parsed.mode;
      branchVisibilityMode = mode === "focus" || mode === "local" || mode === "all" || mode === "custom"
        ? mode
        : "focus";
      customVisibleBranches = new Set(
        Array.isArray(parsed.branches)
          ? parsed.branches.filter((branch): branch is string => typeof branch === "string")
          : [],
      );
    } catch {
      branchVisibilityMode = "focus";
      customVisibleBranches = new Set();
    }
  });

  $effect(() => {
    const currentRepository = repositoryKey;
    const mode = branchVisibilityMode;
    const branches = [...customVisibleBranches];
    if (!currentRepository || loadedVisibilityRepository !== currentRepository) return;
    try {
      localStorage.setItem(
        `${GRAPH_VISIBILITY_STORAGE_PREFIX}${currentRepository}`,
        JSON.stringify({ mode, branches }),
      );
    } catch {
      // The graph still works if storage is unavailable.
    }
  });

  $effect(() => {
    if (graphBranchNames.length === 0 || customVisibleBranches.size === 0) return;
    const available = new Set(graphBranchNames);
    const next = new Set([...customVisibleBranches].filter((branch) => available.has(branch)));
    if (next.size !== customVisibleBranches.size) customVisibleBranches = next;
  });

  $effect(() => {
    if (!selectedCommitHash) return;
    queueMicrotask(() => {
      panelElement
        ?.querySelector<HTMLElement>(`[data-commit-hash="${CSS.escape(selectedCommitHash)}"]`)
        ?.scrollIntoView({ block: "center", behavior: "smooth" });
    });
  });

  let branchMembership = $derived(branchMembershipByHash(commits));
  let visibleCommitEntries = $derived(visibleCommitEntriesForGraph(commits, branchMembership));
  let visibleCommits = $derived(visibleCommitEntries.map((entry) => entry.commit));
  let graphCommits = $derived(visibleCommitEntries.map((entry) => entry.graphCommit));
  let visibleBranchCount = $derived(visibleGraphBranchNames.length);
  let graph = $derived(computeGraph(graphCommits, branchMembership));
  let graphRows = $derived(graph.rows);
  let graphWidth = $derived(Math.max(Math.max(graph.columns, 1) * GRAPH_LANE + 18, 42));
</script>

<svelte:window onclick={closeCommitContextMenu} onkeydown={handleWindowKeydown} on:contextmenu|capture={closeCommitContextMenu} />

<section bind:this={panelElement} class="panel history-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
  <div class="section-head">
    <div>
      <span class="eyebrow">History</span>
      <h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Commits</h2>
    </div>
    {#if graphBranchNames.length > 0}
      <div class="section-head-actions">
        <button
          class="graph-branch-dialog-button"
          type="button"
          onclick={openBranchDialog}
          title="Customize visible branches"
          aria-label={`${visibleBranchCount} of ${graphBranchNames.length} branches visible. Customize branches.`}
        >
          <GitBranch size={13} aria-hidden="true" />
          Branches
          <span>{visibleBranchCount}/{graphBranchNames.length}</span>
        </button>
      </div>
    {/if}
  </div>

  {#if !hasRepository}
    <div class="blank-state">No repository loaded.</div>
  {:else if commits.length === 0}
    <div class="blank-state">No commits returned.</div>
  {:else}
    <div class="history-list graph-list overflow-auto">
      {#if visibleCommits.length === 0}
        <div class="blank-state">No loaded commits match the selected branches.</div>
      {/if}
      {#each visibleCommitEntries as entry, rowIndex (entry.commit.hash)}
        {@const item = entry.commit}
        {@const row = graphRows[rowIndex]}
        {@const refSummary = commitRefSummary(item)}
        {@const rowSyncClass = syncClassForBranches(row?.branchLabels ?? [])}
        <article
          class="commit-row graph-row"
          class:selected={selectedCommitHash === item.hash}
          class:has-note={item.has_note}
          data-commit-hash={item.hash}
          class:graph-ahead-row={rowSyncClass === "ahead"}
          class:graph-behind-row={rowSyncClass === "behind"}
          class:merge-row={item.parents.length > 1}
          class:root-row={item.parents.length === 0}
          class:tip-row={item.refs.length > 0}
        >
          <div class="graph-gutter" style={`width:${graphWidth}px`} aria-hidden="true">
            {#if row}
              <svg class="graph-svg" viewBox={`0 0 ${graphWidth} 100`} preserveAspectRatio="none">
                {#each row.top as seg}
                  <path
                    class:hidden-branch={!segmentIsVisible(seg)}
                    class:graph-segment-ahead={syncClassForBranches(seg.branches) === "ahead"}
                    class:graph-segment-behind={syncClassForBranches(seg.branches) === "behind"}
                    d={graphPath(seg, 0, 50)}
                    stroke={seg.color}
                    stroke-width="2.2"
                    vector-effect="non-scaling-stroke"
                  />
                {/each}
                {#each row.bottom as seg}
                  <path
                    class:hidden-branch={!segmentIsVisible(seg)}
                    class:graph-segment-ahead={syncClassForBranches(seg.branches) === "ahead"}
                    class:graph-segment-behind={syncClassForBranches(seg.branches) === "behind"}
                    d={graphPath(seg, 50, 100)}
                    stroke={seg.color}
                    stroke-width="2.2"
                    vector-effect="non-scaling-stroke"
                  />
                {/each}
                {#if refSummary.primaryBranch}
                  <path
                    class="graph-ref-connector"
                    d={`M ${graphColX(row.dotCol)} 50 L ${graphWidth - GRAPH_REF_ARM * 2} 50`}
                    stroke={row.dotColor}
                    stroke-width="1.5"
                    vector-effect="non-scaling-stroke"
                  />
                {/if}
              </svg>
              <span
                class="graph-dot"
                class:merge={item.parents.length > 1}
                class:tip={item.refs.length > 0}
                class:hidden-branch={!rowGraphIsVisible(row)}
                class:ahead={rowSyncClass === "ahead"}
                class:behind={rowSyncClass === "behind"}
                title={commitHoverTitle(item, row)}
                style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`}
              ></span>
            {/if}
          </div>

          <div
            class="commit-body"
            class:has-branch-ref={Boolean(refSummary.primaryBranch)}
            style={`--ref-lane-color:${row?.dotColor ?? GRAPH_COLORS[0]}`}
          >
            {#if item.has_note}
              <span class="commit-note-rail" aria-hidden="true"></span>
            {/if}
            {#if refSummary.primaryBranch || refSummary.primaryTag || refSummary.overflowCount > 0}
              <div class="commit-ref-area">
                <div class="commit-ref-strip" aria-label="Commit references">
                  {#if refSummary.primaryBranch}
                    <span class="branch-ref-cluster" class:local-only={refSummary.primaryBranch.localOnly}>
                      <span
                        class="compact-ref-chip branch"
                        class:current={refSummary.primaryBranch.current}
                        class:remote={refSummary.primaryBranch.kind === "remote"}
                        title={branchDecorationTitle(refSummary.primaryBranch)}
                      >
                        <GitBranch class="compact-ref-branch-icon" size={10} aria-hidden="true" />
                        <span>{refSummary.primaryBranch.label}</span>
                        {#if !refSummary.primaryBranch.localOnly && branchStatusLabel(refSummary.primaryBranch)}
                          <small>{branchStatusLabel(refSummary.primaryBranch)}</small>
                        {/if}
                      </span>
                      {#if refSummary.primaryBranch.localOnly}
                        <span class="compact-ref-local-marker" title="This branch exists only locally and has not been published yet">
                          LOCAL
                        </span>
                      {/if}
                    </span>
                  {/if}
                  {#if refSummary.primaryTag}
                    <span class="compact-ref-chip tag" title={`Tag ${refSummary.primaryTag}`}>
                      <Tag size={10} aria-hidden="true" />
                      <span>{refSummary.primaryTag}</span>
                    </span>
                  {/if}
                  {#if refSummary.overflowCount > 0}
                    <button
                      class="compact-ref-overflow"
                      type="button"
                      onclick={() => toggleCommitRefs(item)}
                      aria-expanded={expandedRefsCommitHash === item.hash}
                      aria-controls={`commit-refs-${item.hash}`}
                      title={`Show ${refSummary.overflowCount} more ${refSummary.overflowCount === 1 ? "reference" : "references"}`}
                    >
                      +{refSummary.overflowCount}
                    </button>
                  {/if}
                </div>

                {#if refSummary.overflowCount > 0 && expandedRefsCommitHash === item.hash}
                  <div class="commit-ref-details" id={`commit-refs-${item.hash}`}>
                    <strong>References on this commit</strong>
                    {#if refSummary.branches.some((branch) => branch.kind !== "remote")}
                      <section>
                        <span>Local</span>
                        <div>
                          {#each refSummary.branches.filter((branch) => branch.kind !== "remote") as branch}
                            <span class="commit-ref-detail-item local" title={branchDecorationTitle(branch)}>
                              <i aria-hidden="true"></i>{branch.label}
                              {#if branch.current}<small>Current</small>{/if}
                              {#if branch.trackedRemote}<small>{branch.trackedRemote}</small>{/if}
                              {#if branch.localOnly}<small class="local-only"><CloudOff size={9} aria-hidden="true" />Local only</small>{/if}
                            </span>
                          {/each}
                        </div>
                      </section>
                    {/if}
                    {#if refSummary.branches.some((branch) => branch.kind === "remote")}
                      <section>
                        <span>Remote</span>
                        <div>
                          {#each refSummary.branches.filter((branch) => branch.kind === "remote") as branch}
                            <span class="commit-ref-detail-item remote"><i aria-hidden="true"></i>{branch.label}</span>
                          {/each}
                        </div>
                      </section>
                    {/if}
                    {#if refSummary.tags.length > 0}
                      <section>
                        <span>Tags</span>
                        <div>
                          {#each refSummary.tags as tag}
                            <span class="commit-ref-detail-item tag"><Tag size={10} aria-hidden="true" />{tag}</span>
                          {/each}
                        </div>
                      </section>
                    {/if}
                    {#if refSummary.other.length > 0}
                      <section>
                        <span>Other</span>
                        <div>
                          {#each refSummary.other as ref}
                            <span class="commit-ref-detail-item">{ref}</span>
                          {/each}
                        </div>
                      </section>
                    {/if}
                  </div>
                {/if}
              </div>
            {/if}

            <div class="commit-card-head">
              <span class="commit-avatar">
                {authorInitials(item.author_name)}
              </span>
              <div class="commit-card-main">
                <div class="commit-title-row">
                  <strong class="commit-summary" title={item.summary}>{item.summary}</strong>
                  <span class={`commit-kind ${commitKind(item)}`}>
                    {#if item.parents.length > 1}
                      <GitMerge size={12} aria-hidden="true" />
                    {/if}
                    {commitKindLabel(item)}
                  </span>
                </div>
                <div class="commit-meta-line">
                  <span class="commit-hash">{item.short_hash}</span>
                  <span class="commit-author" title={item.author_email}>{item.author_name}</span>
                  {#if item.has_note}
                    <span class="commit-note-indicator">
                      <button
                        class="commit-note-presence"
                        type="button"
                        onpointerenter={() => void loadCommitNotePreview(item)}
                        onfocus={() => void loadCommitNotePreview(item)}
                        onclick={() => openCommitNote(item)}
                        disabled={isBusy}
                        aria-label={`Open Git note for ${item.short_hash}`}
                        aria-describedby={`commit-note-preview-${item.hash}`}
                      >
                        <StickyNote size={11} aria-hidden="true" />
                        <span>Note</span>
                      </button>
                      <span
                        class="commit-note-tooltip"
                        id={`commit-note-preview-${item.hash}`}
                        role="tooltip"
                      >
                        <span class="commit-note-tooltip-head">
                          <StickyNote size={12} aria-hidden="true" />
                          Git Note
                          <small>Click to open</small>
                        </span>
                        <span class="commit-note-tooltip-body">
                          {#if notePreviewLoading.has(item.hash)}
                            Loading note…
                          {:else if notePreviewErrors.has(item.hash)}
                            Note could not be loaded.
                          {:else}
                            {notePreviews[item.hash] ?? "Hover to load the note."}
                          {/if}
                        </span>
                      </span>
                    </span>
                  {/if}
                </div>
              </div>
            </div>

            {#if item.files.length > 0}
              <div class="commit-files">
                <button
                  class="commit-files-toggle"
                  type="button"
                  onclick={() => onToggleCommitFiles(item.hash)}
                  aria-expanded={expandedCommitHashes.has(item.hash)}
                >
                  {#if expandedCommitHashes.has(item.hash)}
                    <ChevronDown size={14} aria-hidden="true" />
                  {:else}
                    <ChevronRight size={14} aria-hidden="true" />
                  {/if}
                  {item.files.length} {item.files.length === 1 ? "file" : "files"} changed
                </button>

                {#if expandedCommitHashes.has(item.hash)}
                  <div class="commit-file-list" aria-label="Changed files">
                    {#each item.files as file (`${item.hash}:${file.old_path ?? ""}:${file.path}`)}
                      <button
                        class="commit-file-button"
                        type="button"
                        onclick={() => onPreviewCommitFile(item, file)}
                        disabled={isBusy}
                        title={`Show differences before restoring - ${displayCommitFile(file)}`}
                      >
                        <span class={`status-badge ${file.status}`}>{statusLabel(file.status)}</span>
                        <strong>{commitFileName(file)}</strong>
                      </button>
                    {/each}
                  </div>
                {/if}
              </div>
            {/if}

            <div class="commit-actions">
              <time class="commit-time" datetime={item.date}>{formatCommitDate(item.date)}</time>
              <div class="commit-action-buttons">
                {#if !item.has_note}
                  <button
                    class="commit-menu-button commit-note-button"
                    type="button"
                    onclick={() => openCommitNote(item)}
                    disabled={isBusy}
                    title={`Add a Git note to ${item.short_hash}`}
                    aria-label={`Add a Git note to ${item.short_hash}`}
                  >
                    <StickyNote size={14} aria-hidden="true" />
                  </button>
                {/if}
                <button
                  class="commit-menu-button"
                  type="button"
                  onclick={(event) => openCommitActionMenu(event, item)}
                  disabled={isBusy}
                  title="Commit actions"
                  aria-label={`Actions for ${item.short_hash}`}
                  aria-haspopup="menu"
                  aria-expanded={contextCommit?.hash === item.hash}
                >
                  <EllipsisVertical size={15} aria-hidden="true" />
                </button>
              </div>
            </div>
          </div>
        </article>
      {/each}
      {#if hasMore || isLoadingMore || loadMoreError}
        <div class="history-load-more" use:observeHistoryEnd aria-live="polite">
          {#if isLoadingMore}
            <LoaderCircle class="spin" size={15} aria-hidden="true" />
            <span>Loading older commits…</span>
          {:else if loadMoreError}
            <span title={loadMoreError}>Older commits could not be loaded.</span>
            <button type="button" class="btn-sm" onclick={() => { void onLoadMore(); }} disabled={isBusy}>Retry</button>
          {:else}
            <button type="button" class="history-load-more-button" onclick={() => { void onLoadMore(); }} disabled={isBusy}>
              Load older commits
            </button>
          {/if}
        </div>
      {/if}
    </div>
  {/if}

  {#if contextCommit}
    <div
      class="history-context-menu"
      style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
      role="menu"
      tabindex="-1"
      aria-label={`Actions for ${contextCommit.short_hash}`}
    >
      <button type="button" role="menuitem" onclick={createBranchFromContextCommit} disabled={isBusy}>
        <GitBranch size={14} aria-hidden="true" />
        Branch
      </button>
      <button type="button" role="menuitem" onclick={openContextCommitNote} disabled={isBusy}>
        <StickyNote size={14} aria-hidden="true" />
        Note
      </button>
      <button type="button" role="menuitem" onclick={restoreContextCommit} disabled={isBusy}>
        <RotateCcw size={14} aria-hidden="true" />
        Restore
      </button>
      <button
        type="button"
        role="menuitem"
        onclick={cherryPickContextCommit}
        disabled={isBusy}
        title="Apply this commit's changes on top of the current branch"
      >
        <Cherry size={14} aria-hidden="true" />
        Cherry-pick
      </button>
      <button type="button" role="menuitem" onclick={revertContextCommit} disabled={isBusy} title="Create a new commit that reverses this commit">
        <RotateCcw size={14} aria-hidden="true" /> Revert
      </button>
    </div>
  {/if}
</section>

{#if branchDialogOpen}
  <div class="branch-filter-backdrop" role="presentation">
    <div
      class="branch-filter-dialog"
      role="dialog"
      aria-modal="true"
      aria-label="Select visible branches"
    >
      <header class="branch-filter-dialog-head">
        <div>
          <span class="eyebrow">Git graph</span>
          <h3>Visible branches</h3>
        </div>
        <button class="dialog-icon-button" type="button" onclick={closeBranchDialog} aria-label="Close branch selection">
          <X size={16} aria-hidden="true" />
        </button>
      </header>

      <div class="branch-filter-summary">
        <span>{visibleBranchCount} of {graphBranchNames.length} branches selected</span>
        <div class="branch-filter-actions">
          <button type="button" onclick={showFocusGraphBranches} disabled={branchVisibilityMode === "focus"}>Focus</button>
          <button type="button" onclick={showAllGraphBranches} disabled={visibleBranchCount === graphBranchNames.length}>Show all</button>
          <button type="button" onclick={hideAllGraphBranches} disabled={visibleBranchCount === 0}>Hide all</button>
        </div>
      </div>

      <div class="branch-filter-dialog-list">
        {#if localBranchNames.length > 0}
          <section class="branch-filter-group">
            <button
              class="branch-filter-group-toggle"
              type="button"
              aria-expanded={localBranchGroupOpen}
              onclick={() => { localBranchGroupOpen = !localBranchGroupOpen; }}
            >
              {#if localBranchGroupOpen}<ChevronDown size={14} aria-hidden="true" />{:else}<ChevronRight size={14} aria-hidden="true" />{/if}
              <span>Local</span>
              <em>{localBranchNames.filter(branchIsVisible).length}/{localBranchNames.length}</em>
            </button>
            {#if localBranchGroupOpen}
              <div class="branch-filter-group-list">
                {#each localBranchNames as branch}
                  <label class:muted={!branchIsVisible(branch)} class="branch-filter-option" title={branch}>
                    <input
                      type="checkbox"
                      checked={branchIsVisible(branch)}
                      onchange={() => toggleGraphBranch(branch)}
                    />
                    <GitBranch size={14} aria-hidden="true" />
                    <span>{branch}</span>
                  </label>
                {/each}
              </div>
            {/if}
          </section>
        {/if}
        {#if remoteBranchNames.length > 0}
          <section class="branch-filter-group">
            <button
              class="branch-filter-group-toggle"
              type="button"
              aria-expanded={remoteBranchGroupOpen}
              onclick={() => { remoteBranchGroupOpen = !remoteBranchGroupOpen; }}
            >
              {#if remoteBranchGroupOpen}<ChevronDown size={14} aria-hidden="true" />{:else}<ChevronRight size={14} aria-hidden="true" />{/if}
              <span>Remote</span>
              <em>{remoteBranchNames.filter(branchIsVisible).length}/{remoteBranchNames.length}</em>
            </button>
            {#if remoteBranchGroupOpen}
              <div class="branch-filter-group-list">
                {#each remoteBranchNames as branch}
                  <label class:muted={!branchIsVisible(branch)} class="branch-filter-option" title={branch}>
                    <input
                      type="checkbox"
                      checked={branchIsVisible(branch)}
                      onchange={() => toggleGraphBranch(branch)}
                    />
                    <GitBranch size={14} aria-hidden="true" />
                    <span>{branch}</span>
                  </label>
                {/each}
              </div>
            {/if}
          </section>
        {/if}
      </div>
    </div>
  </div>
{/if}
