"use client";

import { useState, useCallback, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { GraphNode } from "@/lib/types";
import { buildDescriptionCopyText, formatRelativeTime } from "@/lib/utils";
import {
  speakWithElevenLabs,
  speakSelection,
  stopTts,
  pauseTts,
  resumeTts,
  stripMarkdown,
  setTtsPlaybackRate,
  type TtsState,
} from "@/lib/tts";
import { hasApiKey } from "@/lib/settings";
import { useIsMobile } from "@/hooks/useIsMobile";
import { HeartIcon } from "@/components/HeartIcon";
import type { BeadsComment } from "@/hooks/useBeadsComments";

// Issue 4 (beads-map-r2n3): Count all comments recursively (root + all replies)
function countAllComments(comments: BeadsComment[]): number {
  return comments.reduce((sum, c) => sum + 1 + countAllComments(c.replies || []), 0);
}

const SPEED_PRESETS = [
  { label: "Normal", value: 1 },
  { label: "1.25x", value: 1.25 },
  { label: "1.5x", value: 1.5 },
  { label: "1.75x", value: 1.75 },
  { label: "2x", value: 2 },
];

// ============================================================================
// ModalCommentItem — inline comment renderer for DescriptionModal
// ============================================================================

function ModalCommentItem({
  comment,
  currentDid,
  isAuthenticated,
  onDelete,
  onLike,
  onStartReply,
  replyingToUri,
  replyText,
  onReplyTextChange,
  onSubmitReply,
  onCancelReply,
  isSubmittingReply,
  depth,
}: {
  comment: BeadsComment;
  currentDid?: string;
  isAuthenticated?: boolean;
  onDelete?: (comment: { rkey: string }) => Promise<void>;
  onLike?: (comment: BeadsComment) => Promise<void>;
  onStartReply: (comment: BeadsComment) => void;
  replyingToUri: string | null;
  replyText: string;
  onReplyTextChange: (text: string) => void;
  onSubmitReply: () => void;
  onCancelReply: () => void;
  isSubmittingReply: boolean;
  depth: number;
}) {
  const [deleting, setDeleting] = useState(false);
  const [liking, setLiking] = useState(false);
  const isOwn = currentDid && currentDid === comment.did;
  const hasLiked = currentDid ? comment.likes.some((l) => l.did === currentDid) : false;
  const isReplyingToThis = replyingToUri === comment.uri;

  const handleDelete = async () => {
    if (!onDelete || deleting) return;
    setDeleting(true);
    try {
      await onDelete({ rkey: comment.rkey });
    } catch (err) {
      console.error("Failed to delete comment:", err);
    } finally {
      setDeleting(false);
    }
  };

  const handleLike = async () => {
    if (!onLike || liking) return;
    setLiking(true);
    try {
      await onLike(comment);
    } catch (err) {
      console.error("Failed to toggle like:", err);
    } finally {
      setLiking(false);
    }
  };

  return (
    <div className={`${depth > 0 ? "ml-4 pl-3 border-l border-zinc-100 dark:border-zinc-800" : ""}`}>
      <div className="py-2">
        {/* Header: avatar + handle + time */}
        <div className="flex items-center gap-1.5 mb-1">
          <div className="shrink-0 w-6 h-6 rounded-full bg-zinc-100 dark:bg-zinc-800 overflow-hidden">
            {comment.avatar ? (
              <img src={comment.avatar} alt="" className="w-full h-full object-cover" />
            ) : (
              <div className="w-full h-full flex items-center justify-center text-[9px] font-medium text-zinc-400 dark:text-zinc-500">
                {(comment.handle || comment.did).charAt(0).toUpperCase()}
              </div>
            )}
          </div>
          <a
            href={`https://www.impactindexer.org/data?did=${comment.did}`}
            target="_blank"
            rel="noopener noreferrer"
            className="text-xs font-medium text-zinc-600 dark:text-zinc-400 truncate hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors"
          >
            {comment.displayName || comment.handle || comment.did.slice(0, 16) + "..."}
          </a>
          <span className="text-[10px] text-zinc-300 dark:text-zinc-600 shrink-0">
            {formatRelativeTime(comment.createdAt)}
          </span>
        </div>

        {/* Comment text */}
        <p className="text-xs text-zinc-500 dark:text-zinc-400 leading-relaxed whitespace-pre-wrap break-words">
          {comment.text}
        </p>

        {/* Actions: like, reply, delete */}
        <div className="flex items-center gap-2 mt-1 text-[10px]">
          <button
            onClick={handleLike}
            disabled={!isAuthenticated || liking}
            className={`flex items-center gap-0.5 transition-colors ${
              hasLiked ? "text-rose-500" : "text-zinc-300 hover:text-rose-500"
            } disabled:opacity-50`}
          >
            <HeartIcon className="w-3 h-3" filled={hasLiked} />
            {comment.likes.length > 0 && <span>{comment.likes.length}</span>}
          </button>

          {isAuthenticated && (
            <button
              onClick={() => onStartReply(comment)}
              className={`transition-colors ${
                isReplyingToThis
                  ? "text-emerald-500 dark:text-emerald-400"
                  : "text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400"
              }`}
            >
              reply
            </button>
          )}

          {isOwn && onDelete && (
            <button
              onClick={handleDelete}
              disabled={deleting}
              className="ml-auto shrink-0 text-zinc-300 dark:text-zinc-600 hover:text-red-400 disabled:opacity-50 transition-colors"
            >
              {deleting ? "..." : "delete"}
            </button>
          )}
        </div>
      </div>

      {/* Inline reply form */}
      {isReplyingToThis && (
        <div className="mt-1 ml-2 pl-3 border-l border-emerald-200 dark:border-emerald-800 space-y-1.5 mb-2">
          <div className="flex items-center gap-1.5 text-[10px] text-zinc-400">
            <span>Replying to</span>
            <span className="font-medium text-zinc-600 dark:text-zinc-300">
              {comment.displayName || comment.handle}
            </span>
          </div>
          <div className="flex gap-2">
            <input
              type="text"
              value={replyText}
              onChange={(e) => onReplyTextChange(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter" && !e.shiftKey) {
                  e.preventDefault();
                  onSubmitReply();
                }
                if (e.key === "Escape") {
                  e.stopPropagation();
                  onCancelReply();
                }
              }}
              placeholder="Write a reply..."
              disabled={isSubmittingReply}
              autoFocus
              className="flex-1 px-2 py-1 text-xs bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded placeholder-zinc-400 focus:outline-none focus:border-emerald-400 disabled:opacity-50"
            />
            <button
              onClick={onSubmitReply}
              disabled={!replyText.trim() || isSubmittingReply}
              className="px-2 py-1 text-xs font-medium text-white bg-emerald-500 rounded hover:bg-emerald-600 disabled:opacity-50 transition-colors"
            >
              {isSubmittingReply ? "..." : "Reply"}
            </button>
            <button
              onClick={onCancelReply}
              className="px-2 py-1 text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors"
            >
              Cancel
            </button>
          </div>
        </div>
      )}

      {/* Nested replies */}
      {comment.replies.length > 0 && (
        <div>
          {comment.replies.map((reply) => (
            <ModalCommentItem
              key={reply.uri}
              comment={reply}
              currentDid={currentDid}
              isAuthenticated={isAuthenticated}
              onDelete={onDelete}
              onLike={onLike}
              onStartReply={onStartReply}
              replyingToUri={replyingToUri}
              replyText={replyText}
              onReplyTextChange={onReplyTextChange}
              onSubmitReply={onSubmitReply}
              onCancelReply={onCancelReply}
              isSubmittingReply={isSubmittingReply}
              depth={depth + 1}
            />
          ))}
        </div>
      )}
    </div>
  );
}

interface DescriptionModalProps {
  node: GraphNode;
  onClose: () => void;
  repoUrl?: string;
  onOpenSettings?: () => void;
  onNavigate?: (direction: "prev" | "next") => void;  // NEW
  navigationLabel?: string;  // NEW — e.g. "2 of 5" to show position
  isAuthenticated?: boolean;
  session?: { did: string; handle: string; avatar?: string } | null;
  comments?: BeadsComment[];
  onPostComment?: (nodeId: string, text: string) => Promise<void>;
  onReplyComment?: (parentComment: BeadsComment, text: string) => Promise<void>;
  onDeleteComment?: (comment: { rkey: string }) => Promise<void>;
  onLikeComment?: (comment: BeadsComment) => Promise<void>;
}

export function DescriptionModal({
  node,
  onClose,
  repoUrl,
  onOpenSettings,
  onNavigate,
  navigationLabel,
  isAuthenticated,
  session,
  comments,
  onPostComment,
  onReplyComment,
  onDeleteComment,
  onLikeComment,
}: DescriptionModalProps) {
  const [copied, setCopied] = useState(false);
  const [sharedLink, setSharedLink] = useState(false);
  const [ttsState, setTtsState] = useState<TtsState>("idle");
  const [ttsError, setTtsError] = useState<string | null>(null);
  const [ttsSpeed, setTtsSpeedState] = useState(1);
  const [speedMenuOpen, setSpeedMenuOpen] = useState(false);
  const [customSpeedInput, setCustomSpeedInput] = useState("");
  const [showCustomInput, setShowCustomInput] = useState(false);
  const speedBtnRef = useRef<HTMLButtonElement>(null);
  const speedMenuRef = useRef<HTMLDivElement>(null);

  const isMobile = useIsMobile();

  // Comment compose state
  const [commentText, setCommentText] = useState("");
  const [submittingComment, setSubmittingComment] = useState(false);

  // Reply state
  const [replyingToUri, setReplyingToUri] = useState<string | null>(null);
  const [replyText, setReplyText] = useState("");
  const [submittingReply, setSubmittingReply] = useState(false);

  // Issue 1 (beads-map-4ek2): Reset reply state when navigating to a different node
  useEffect(() => {
    setReplyingToUri(null);
    setReplyText("");
    setSubmittingReply(false);
  }, [node.id]);

  const handleSubmitComment = useCallback(async () => {
    if (!commentText.trim() || submittingComment || !onPostComment) return;
    setSubmittingComment(true);
    try {
      await onPostComment(node.id, commentText.trim());
      setCommentText("");
    } catch (err) {
      console.error("Failed to post comment:", err);
    } finally {
      setSubmittingComment(false);
    }
  }, [commentText, submittingComment, onPostComment, node.id]);

  const handleStartReply = useCallback((comment: BeadsComment) => {
    setReplyingToUri(comment.uri);
    setReplyText("");
  }, []);

  const handleCancelReply = useCallback(() => {
    setReplyingToUri(null);
    setReplyText("");
  }, []);

  const handleSubmitReply = useCallback(async () => {
    if (!replyText.trim() || submittingReply || !onReplyComment || !replyingToUri) return;
    setSubmittingReply(true);
    try {
      // Find the parent comment
      const findComment = (items: BeadsComment[]): BeadsComment | undefined => {
        for (const c of items) {
          if (c.uri === replyingToUri) return c;
          const found = findComment(c.replies);
          if (found) return found;
        }
        return undefined;
      };
      const parentComment = comments ? findComment(comments) : undefined;
      if (parentComment) {
        await onReplyComment(parentComment, replyText.trim());
      }
      setReplyingToUri(null);
      setReplyText("");
    } catch (err) {
      console.error("Failed to post reply:", err);
    } finally {
      setSubmittingReply(false);
    }
  }, [replyText, submittingReply, onReplyComment, replyingToUri, comments]);

  // Selection tooltip state
  const [selectionTooltip, setSelectionTooltip] = useState<{
    text: string;
    x: number;
    y: number;
  } | null>(null);
  const selectionTooltipRef = useRef<HTMLDivElement>(null);
  // Guard: when true, selectionchange listener won't clear the tooltip
  // (prevents race where clicking the tooltip clears browser selection before handler runs)
  const ttsStartingRef = useRef(false);

  const handleCopy = () => {
    if (!node.description) return;
    navigator.clipboard
      .writeText(buildDescriptionCopyText(node, repoUrl))
      .then(() => {
        setCopied(true);
        setTimeout(() => setCopied(false), 1500);
      });
  };

  const handleShareLink = () => {
    // Issue 3 (beads-map-tn4k): Preserve existing URL params (epic, color, filters, etc.)
    const params = new URLSearchParams(window.location.search);
    params.set('node', node.id);
    params.set('desc', node.id);
    const url = window.location.origin + window.location.pathname + '?' + params.toString();
    navigator.clipboard.writeText(url).then(() => {
      setSharedLink(true);
      setTimeout(() => setSharedLink(false), 2000);
    });
  };

  // --- TTS handlers -------------------------------------------------------

  const ttsStateChange = useCallback((state: TtsState, error?: string) => {
    setTtsState(state);
    if (error) setTtsError(error);
  }, []);

  const handleTts = useCallback(() => {
    if (!hasApiKey()) {
      onOpenSettings?.();
      return;
    }
    const plainText = stripMarkdown(node.description || "");
    if (!plainText) return;
    setTtsError(null);
    speakWithElevenLabs(plainText, ttsStateChange);
  }, [node.description, onOpenSettings, ttsStateChange]);

  const handleStopTts = useCallback(() => {
    stopTts();
    setTtsState("idle");
    setSpeedMenuOpen(false);
    setShowCustomInput(false);
  }, []);

  const handlePauseTts = useCallback(() => {
    pauseTts();
    setTtsState("paused");
  }, []);

  const handleResumeTts = useCallback(() => {
    resumeTts();
    setTtsState("playing");
  }, []);

  const handleSpeedChange = useCallback((speed: number) => {
    const clamped = Math.max(0.25, Math.min(4, speed));
    setTtsSpeedState(clamped);
    setTtsPlaybackRate(clamped);
    setSpeedMenuOpen(false);
    setShowCustomInput(false);
  }, []);

  // Apply speed when playback starts (carries over from previous session)
  useEffect(() => {
    if (ttsState === "playing") {
      setTtsPlaybackRate(ttsSpeed);
    }
  }, [ttsState, ttsSpeed]);

  // Click-outside handler for speed menu
  useEffect(() => {
    if (!speedMenuOpen) return;
    const handler = (e: MouseEvent) => {
      if (
        speedMenuRef.current && !speedMenuRef.current.contains(e.target as Node) &&
        speedBtnRef.current && !speedBtnRef.current.contains(e.target as Node)
      ) {
        setSpeedMenuOpen(false);
        setShowCustomInput(false);
      }
    };
    const timer = setTimeout(() => window.addEventListener("mousedown", handler), 50);
    return () => {
      clearTimeout(timer);
      window.removeEventListener("mousedown", handler);
    };
  }, [speedMenuOpen]);

  // --- Selection tooltip handlers -----------------------------------------

  const handleSelectionMouseUp = useCallback(() => {
    // Small delay to let the browser finalize the selection
    setTimeout(() => {
      const sel = window.getSelection();
      if (!sel || sel.isCollapsed) return;
      const selectedText = sel.toString().trim();
      if (selectedText.length < 3) return;
      try {
        const range = sel.getRangeAt(0);
        const rect = range.getBoundingClientRect();
        setSelectionTooltip({
          text: selectedText,
          x: rect.left + rect.width / 2,
          y: rect.top - 8,
        });
      } catch {
        // getRangeAt can throw if selection is weird
      }
    }, 10);
  }, []);

  // Clear tooltip when selection is lost (guarded during TTS initiation)
  // Disabled on mobile — text selection is unreliable on touch devices
  useEffect(() => {
    if (isMobile) return;
    const handler = () => {
      if (ttsStartingRef.current) return;
      const sel = window.getSelection();
      if (!sel || sel.isCollapsed || !sel.toString().trim()) {
        setSelectionTooltip(null);
      }
    };
    document.addEventListener("selectionchange", handler);
    return () => document.removeEventListener("selectionchange", handler);
  }, [isMobile]);

  const handleSelectionTts = useCallback(() => {
    if (!selectionTooltip) return;
    const text = selectionTooltip.text;
    if (!text.trim()) return;

    if (!hasApiKey()) {
      onOpenSettings?.();
      setSelectionTooltip(null);
      return;
    }

    ttsStartingRef.current = true; // Guard against selectionchange race
    setTtsError(null);
    setSelectionTooltip(null);

    // speakSelection() checks cache first — zero API call if full text was played
    speakSelection(text, ttsStateChange);

    // Release guard after a tick (React state updates are batched)
    setTimeout(() => { ttsStartingRef.current = false; }, 100);
  }, [selectionTooltip, onOpenSettings, ttsStateChange]);

  // Stop TTS on unmount / modal close
  useEffect(() => {
    return () => {
      stopTts();
      setSpeedMenuOpen(false);
      setSelectionTooltip(null);
    };
  }, []);

  // Arrow key navigation + Escape to close
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Don't capture if user is in an input/textarea
      const tag = (e.target as HTMLElement)?.tagName;
      if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement)?.isContentEditable) return;
      
      if (e.key === "Escape") {
        e.preventDefault();
        onClose();
      } else if (e.key === "ArrowLeft" && onNavigate) {
        e.preventDefault();
        onNavigate("prev");
      } else if (e.key === "ArrowRight" && onNavigate) {
        e.preventDefault();
        onNavigate("next");
      }
    };
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [onClose, onNavigate]);

  if (!node.description) return null;

  return createPortal(
    <div
      className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm"
      onClick={onClose}
    >
      <div
        className="bg-white dark:bg-zinc-900 rounded-xl shadow-2xl w-[90vw] max-w-2xl max-h-[80vh] flex flex-col"
        onClick={(e) => e.stopPropagation()}
      >
        {/* Modal header */}
        <div className="flex items-center justify-between px-5 py-3 border-b border-zinc-100 dark:border-zinc-800">
          <div className="flex items-center gap-2 min-w-0">
            <span className="text-xs font-mono font-semibold text-emerald-600 dark:text-emerald-400 shrink-0">
              {node.id}
            </span>
            <span className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 truncate">
              {node.title}
            </span>
          </div>
          {onNavigate && (
            <div className="flex items-center gap-1 shrink-0">
              <button
                onClick={() => onNavigate("prev")}
                className="p-1 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
                title="Previous (←)"
              >
                <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
                </svg>
              </button>
              {navigationLabel && (
                <span className="text-[10px] text-zinc-400 dark:text-zinc-500 font-mono tabular-nums min-w-[3ch] text-center">{navigationLabel}</span>
              )}
              <button
                onClick={() => onNavigate("next")}
                className="p-1 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
                title="Next (→)"
              >
                <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
                </svg>
              </button>
            </div>
          )}
          <div className="flex items-center gap-1 shrink-0">
            {/* Copy button */}
            <button
              onClick={handleCopy}
              className="p-1 text-zinc-400 dark:text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors"
              title="Copy description"
            >
              {copied ? (
                <svg
                  className="w-4 h-4 text-emerald-500"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  strokeWidth={2}
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M4.5 12.75l6 6 9-13.5"
                  />
                </svg>
              ) : (
                <svg
                  className="w-4 h-4"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  strokeWidth={2}
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
                  />
                </svg>
              )}
            </button>

            {/* Share link button */}
            <button
              onClick={handleShareLink}
              className="p-1 text-zinc-400 dark:text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors"
              title="Share link"
              aria-label="Share link"
            >
              {sharedLink ? (
                <svg
                  className="w-4 h-4 text-emerald-500"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  strokeWidth={2}
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M4.5 12.75l6 6 9-13.5"
                  />
                </svg>
              ) : (
                <svg
                  className="w-4 h-4"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  strokeWidth={2}
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
                  />
                </svg>
              )}
            </button>

            {/* TTS buttons — 4 states: idle, loading, playing, paused */}
            {ttsState === "loading" ? (
              <button
                disabled
                className="p-1 text-zinc-300 cursor-wait"
                title="Loading audio..."
              >
                <svg
                  className="w-4 h-4 animate-spin"
                  fill="none"
                  viewBox="0 0 24 24"
                >
                  <circle
                    className="opacity-25"
                    cx="12"
                    cy="12"
                    r="10"
                    stroke="currentColor"
                    strokeWidth={4}
                  />
                  <path
                    className="opacity-75"
                    fill="currentColor"
                    d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                  />
                </svg>
              </button>
            ) : ttsState === "playing" ? (
              <>
                {/* Pause button */}
                <button
                  onClick={handlePauseTts}
                  className="p-1 text-emerald-500 hover:text-amber-500 hover:bg-amber-50 rounded transition-colors"
                  title="Pause"
                >
                  <svg
                    className="w-4 h-4"
                    fill="none"
                    viewBox="0 0 24 24"
                    strokeWidth={1.5}
                    stroke="currentColor"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      d="M15.75 5.25v13.5m-7.5-13.5v13.5"
                    />
                  </svg>
                </button>
                {/* Stop button */}
                <button
                  onClick={handleStopTts}
                  className="p-1 text-zinc-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
                  title="Stop"
                >
                  <svg
                    className="w-4 h-4"
                    fill="none"
                    viewBox="0 0 24 24"
                    strokeWidth={1.5}
                    stroke="currentColor"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      d="M5.25 7.5A2.25 2.25 0 017.5 5.25h9a2.25 2.25 0 012.25 2.25v9a2.25 2.25 0 01-2.25 2.25h-9a2.25 2.25 0 01-2.25-2.25v-9z"
                    />
                  </svg>
                </button>
              </>
            ) : ttsState === "paused" ? (
              <>
                {/* Resume button */}
                <button
                  onClick={handleResumeTts}
                  className="p-1 text-amber-500 hover:text-emerald-500 hover:bg-emerald-50 rounded transition-colors"
                  title="Resume"
                >
                  <svg
                    className="w-4 h-4"
                    fill="none"
                    viewBox="0 0 24 24"
                    strokeWidth={1.5}
                    stroke="currentColor"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 010 1.972l-11.54 6.347a1.125 1.125 0 01-1.667-.986V5.653z"
                    />
                  </svg>
                </button>
                {/* Stop button */}
                <button
                  onClick={handleStopTts}
                  className="p-1 text-zinc-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
                  title="Stop"
                >
                  <svg
                    className="w-4 h-4"
                    fill="none"
                    viewBox="0 0 24 24"
                    strokeWidth={1.5}
                    stroke="currentColor"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      d="M5.25 7.5A2.25 2.25 0 017.5 5.25h9a2.25 2.25 0 012.25 2.25v9a2.25 2.25 0 01-2.25 2.25h-9a2.25 2.25 0 01-2.25-2.25v-9z"
                    />
                  </svg>
                </button>
              </>
            ) : (
              /* Idle — play/speaker button */
              <button
                onClick={handleTts}
                className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
                title="Read aloud"
              >
                <svg
                  className="w-4 h-4"
                  fill="none"
                  viewBox="0 0 24 24"
                  strokeWidth={1.5}
                  stroke="currentColor"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
                  />
                </svg>
              </button>
            )}

            {/* Speed selector — visible during playback/loading/paused */}
            {(ttsState === "playing" || ttsState === "loading" || ttsState === "paused") && (
              <div className="relative">
                <button
                  ref={speedBtnRef}
                  onClick={() => setSpeedMenuOpen((v) => !v)}
                  className="px-1.5 py-0.5 text-[11px] font-mono font-medium rounded text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 transition-colors"
                  title="Playback speed"
                >
                  {ttsSpeed === 1 ? "1x" : `${ttsSpeed}x`}
                </button>

                {speedMenuOpen && (
                  <div
                    ref={speedMenuRef}
                    className="absolute right-0 top-full mt-1 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-lg overflow-hidden z-[110]"
                    style={{ minWidth: 150 }}
                    onClick={(e) => e.stopPropagation()}
                  >
                    {SPEED_PRESETS.map((preset) => (
                      <button
                        key={preset.value}
                        onClick={() => handleSpeedChange(preset.value)}
                        className={`w-full px-3 py-2 text-xs text-left flex items-center justify-between hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors ${
                          ttsSpeed === preset.value ? "text-emerald-600 dark:text-emerald-400 font-medium" : "text-zinc-700 dark:text-zinc-300"
                        }`}
                      >
                        <span>{preset.label}</span>
                        {ttsSpeed === preset.value && (
                          <svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
                            <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
                          </svg>
                        )}
                      </button>
                    ))}

                    <div className="border-t border-zinc-100 dark:border-zinc-800" />

                    {!showCustomInput ? (
                      <button
                        onClick={() => {
                          setShowCustomInput(true);
                          setCustomSpeedInput(String(ttsSpeed));
                        }}
                        className={`w-full px-3 py-2 text-xs text-left flex items-center justify-between hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors ${
                          !SPEED_PRESETS.some((p) => p.value === ttsSpeed) ? "text-emerald-600 dark:text-emerald-400 font-medium" : "text-zinc-700 dark:text-zinc-300"
                        }`}
                      >
                        <span>Custom{!SPEED_PRESETS.some((p) => p.value === ttsSpeed) ? ` (${ttsSpeed}x)` : "..."}</span>
                        {!SPEED_PRESETS.some((p) => p.value === ttsSpeed) && (
                          <svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
                            <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
                          </svg>
                        )}
                      </button>
                    ) : (
                      <div className="px-3 py-2 flex items-center gap-2">
                        <input
                          type="number"
                          min={0.25}
                          max={4}
                          step={0.25}
                          value={customSpeedInput}
                          onChange={(e) => setCustomSpeedInput(e.target.value)}
                          onKeyDown={(e) => {
                            if (e.key === "Enter") {
                              const val = parseFloat(customSpeedInput);
                              if (!isNaN(val) && val >= 0.25 && val <= 4) handleSpeedChange(val);
                            }
                            if (e.key === "Escape") {
                              e.stopPropagation(); // Prevent modal close
                              setShowCustomInput(false);
                              setSpeedMenuOpen(false);
                            }
                          }}
                          onBlur={() => {
                            const val = parseFloat(customSpeedInput);
                            if (!isNaN(val) && val >= 0.25 && val <= 4) handleSpeedChange(val);
                            else setShowCustomInput(false);
                          }}
                          autoFocus
                          className="w-16 rounded border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 px-2 py-1 text-xs text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500"
                        />
                        <span className="text-[11px] text-zinc-400 dark:text-zinc-400">x</span>
                      </div>
                    )}
                  </div>
                )}
              </div>
            )}

            {/* Close button */}
            <button
              onClick={onClose}
              className="p-1 text-zinc-400 dark:text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors"
            >
              <svg
                className="w-4 h-4"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
                strokeWidth={2}
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  d="M6 18L18 6M6 6l12 12"
                />
              </svg>
            </button>
          </div>
        </div>

        {/* TTS error banner */}
        {ttsError && (
          <div className="px-5 py-2 text-xs text-red-500 bg-red-50 dark:bg-red-950 border-b border-red-100 dark:border-red-800 flex items-center justify-between">
            <span>{ttsError}</span>
            <button
              onClick={() => setTtsError(null)}
              className="text-red-400 hover:text-red-600 ml-2"
            >
              <svg
                className="w-3 h-3"
                fill="none"
                viewBox="0 0 24 24"
                strokeWidth={2}
                stroke="currentColor"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  d="M6 18L18 6M6 6l12 12"
                />
              </svg>
            </button>
          </div>
        )}

        {/* Modal body */}
        <div
          className="flex-1 overflow-y-auto px-5 py-4 custom-scrollbar description-markdown text-sm text-zinc-700 dark:text-zinc-300 leading-relaxed"
          onMouseUp={isMobile ? undefined : handleSelectionMouseUp}
        >
          <ReactMarkdown remarkPlugins={[remarkGfm]}>
            {node.description}
          </ReactMarkdown>

          {node.acceptanceCriteria && (
            <>
              <div className="border-t border-zinc-200 dark:border-zinc-700 my-4" />
              <h4 className="text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider mb-2">
                Acceptance Criteria
              </h4>
              <pre className="bg-zinc-50 dark:bg-zinc-800 rounded-lg p-3 border border-zinc-100 dark:border-zinc-800 overflow-x-auto">
                <code className="text-[11px] font-mono text-zinc-600 dark:text-zinc-300 whitespace-pre-wrap break-all">
                  {node.acceptanceCriteria}
                </code>
              </pre>
            </>
          )}

          {/* Comments section */}
          <div className="border-t border-zinc-200 dark:border-zinc-700 mt-4 pt-4">
            <h4 className="text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider mb-3 flex items-center gap-2">
              Comments
              {comments && comments.length > 0 && (
                <span className="inline-flex items-center justify-center w-4 h-4 rounded-full bg-zinc-100 dark:bg-zinc-800 text-[10px] font-medium text-zinc-500 dark:text-zinc-400">
                  {countAllComments(comments)}
                </span>
              )}
            </h4>

            {/* Comment list */}
            {comments && comments.length > 0 ? (
              <div className="space-y-0 mb-4">
                {comments.map((comment) => (
                  <ModalCommentItem
                    key={comment.uri}
                    comment={comment}
                    currentDid={session?.did}
                    isAuthenticated={isAuthenticated}
                    onDelete={onDeleteComment}
                    onLike={onLikeComment}
                    onStartReply={handleStartReply}
                    replyingToUri={replyingToUri}
                    replyText={replyText}
                    onReplyTextChange={setReplyText}
                    onSubmitReply={handleSubmitReply}
                    onCancelReply={handleCancelReply}
                    isSubmittingReply={submittingReply}
                    depth={0}
                  />
                ))}
              </div>
            ) : (
              <p className="text-xs text-zinc-400 dark:text-zinc-500 mb-4">No comments yet.</p>
            )}

            {/* Compose area or sign-in prompt */}
            {isAuthenticated ? (
              <div className="space-y-2">
                <div className="flex items-center gap-2 mb-1">
                  <div className="shrink-0 w-6 h-6 rounded-full bg-zinc-100 dark:bg-zinc-800 overflow-hidden">
                    {session?.avatar ? (
                      <img src={session.avatar} alt="" className="w-full h-full object-cover" />
                    ) : (
                      <div className="w-full h-full flex items-center justify-center text-[10px] font-medium text-zinc-400 dark:text-zinc-500">
                        {session?.handle?.charAt(0).toUpperCase() ?? "?"}
                      </div>
                    )}
                  </div>
                  <span className="text-xs text-zinc-500 dark:text-zinc-400">{session?.handle}</span>
                </div>
                <textarea
                  value={commentText}
                  onChange={(e) => setCommentText(e.target.value)}
                  onKeyDown={(e) => {
                    if (e.key === "Escape") {
                      // Issue 5 (beads-map-u0tb): Escape clears compose textarea
                      e.stopPropagation();
                      setCommentText('');
                      (e.target as HTMLTextAreaElement).blur();
                    }
                    if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
                      e.preventDefault();
                      handleSubmitComment();
                    }
                  }}
                  placeholder="Add a comment..."
                  rows={3}
                  className="w-full px-2.5 py-1.5 text-xs border border-zinc-200 dark:border-zinc-700 rounded-md bg-zinc-50 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 placeholder-zinc-400 dark:placeholder-zinc-500 resize-none focus:outline-none focus:ring-1 focus:ring-emerald-500 focus:border-emerald-500"
                />
                <button
                  onClick={handleSubmitComment}
                  disabled={!commentText.trim() || submittingComment}
                  className="px-3 py-1 text-xs font-medium text-white bg-emerald-500 rounded-md hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
                >
                  {submittingComment ? "Sending..." : "Comment"}
                </button>
              </div>
            ) : (
              <p className="text-xs text-zinc-400 dark:text-zinc-500">
                {/* Issue 2 (beads-map-ednf): Navigate to /login instead of closing the modal */}
                <a
                  href="/login"
                  className="text-emerald-600 dark:text-emerald-400 hover:underline"
                >
                  Sign in
                </a>{" "}
                to comment
              </p>
            )}
          </div>
        </div>
      </div>

      {/* Selection TTS tooltip — outside modal card, inside portal backdrop */}
      {selectionTooltip && !isMobile && (
        <div
          ref={selectionTooltipRef}
          className="fixed z-[110] flex items-center gap-1.5 px-2.5 py-1.5 bg-zinc-800 text-white text-xs rounded-lg shadow-lg select-none"
          style={{
            left: selectionTooltip.x,
            top: selectionTooltip.y,
            transform: "translate(-50%, -100%)",
            pointerEvents: "auto",
          }}
          onMouseDown={(e) => e.stopPropagation()}
          onClick={(e) => e.stopPropagation()}
        >
          <button
            onClick={handleSelectionTts}
            className="flex items-center gap-1 hover:text-emerald-300 transition-colors"
          >
            <svg
              className="w-3.5 h-3.5"
              fill="none"
              viewBox="0 0 24 24"
              strokeWidth={1.5}
              stroke="currentColor"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
              />
            </svg>
            <span>Read aloud</span>
          </button>
        </div>
      )}
    </div>,
    document.body
  );
}
