import type { NostrEvent } from "@nostrify/nostrify";
import {
  Award,
  Camera,
  Egg,
  FileCode,
  FileText,
  GitBranch,
  GitPullRequest,
  Mail,
  MessageCircle,
  Rocket,
  MoreHorizontal,
  Package,
  Play,
  Radio,
  Share2,
  SmilePlus,
  PartyPopper,
  Sparkles,
  Users,
  Zap,
} from "lucide-react";
import { nip19 } from "nostr-tools";
import { type ReactNode, lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link } from "react-router-dom";
/** Lazy-loaded markdown-heavy components — keeps react-markdown + unified pipeline out of the main feed bundle. */
const ArticleContent = lazy(() => import("@/components/ArticleContent").then(m => ({ default: m.ArticleContent })));
const BlobbiStateCard = lazy(() => import("@/components/BlobbiStateCard").then(m => ({ default: m.BlobbiStateCard })));
import {
  MusicPlaylistContent,
  MusicTrackContent,
  PodcastEpisodeContent,
  PodcastTrailerContent,
} from "@/components/AudioKindContent";
import { BadgeContent } from "@/components/BadgeContent";
import { CalendarEventContent } from "@/components/CalendarEventContent";
import {
  ColorMomentContent,
  ColorMomentEyeButton,
} from "@/components/ColorMomentContent";
import { CommentContext } from "@/components/CommentContext";
import { ContentWarningGuard } from "@/components/ContentWarningGuard";
import { EmojifiedText, ReactionEmoji } from "@/components/CustomEmoji";
const CustomNipCard = lazy(() => import("@/components/CustomNipCard").then(m => ({ default: m.CustomNipCard })));
import { EmojiPackContent } from "@/components/EmojiPackContent";
import { FileMetadataContent } from "@/components/FileMetadataContent";
import { FollowPackContent } from "@/components/FollowPackContent";
import { FoundLogContent } from "@/components/FoundLogContent";
import { GeocacheContent } from "@/components/GeocacheContent";
import { GitRepoCard } from "@/components/GitRepoCard";
import { NsiteCard } from "@/components/NsiteCard";
import { ImageGallery } from "@/components/ImageGallery";
import { CardsIcon } from "@/components/icons/CardsIcon";
import { ChestIcon } from "@/components/icons/ChestIcon";
import { RepostIcon } from "@/components/icons/RepostIcon";
import { LiveStreamPlayer } from "@/components/LiveStreamPlayer";
import { MagicDeckContent } from "@/components/MagicDeckContent";
import { Nip05Badge } from "@/components/Nip05Badge";
import { NoteContent } from "@/components/NoteContent";
import { NoteMedia } from "@/components/NoteMedia";
import { NoteMoreMenu } from "@/components/NoteMoreMenu";
import { PatchCard } from "@/components/PatchCard";
import { PollContent } from "@/components/PollContent";
import { ProfileBadgesContent } from "@/components/ProfileBadgesContent";
import { ProfileCard } from "@/components/ProfileCard";
import { ProfileHoverCard } from "@/components/ProfileHoverCard";
const PullRequestCard = lazy(() => import("@/components/PullRequestCard").then(m => ({ default: m.PullRequestCard })));
import { ReactionButton } from "@/components/ReactionButton";
import { ReplyComposeModal } from "@/components/ReplyComposeModal";
import { ReplyContext } from "@/components/ReplyContext";
import { RepostMenu } from "@/components/RepostMenu";
import { ThemeContent } from "@/components/ThemeContent";
import { EncryptedMessageContent } from "@/components/EncryptedMessageContent";
import { EncryptedLetterContent } from "@/components/EncryptedLetterContent";
import { VanishCardCompact } from "@/components/VanishEventContent";
import { ZapstoreAppContent } from "@/components/ZapstoreAppContent";
import { ZapstoreReleaseContent, ZapstoreAssetContent } from "@/components/ZapstoreReleaseContent";
import { AppHandlerContent } from "@/components/AppHandlerContent";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { getAvatarShape } from "@/lib/avatarShape";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { VideoPlayer } from "@/components/VideoPlayer";
import { VoiceMessagePlayer } from "@/components/VoiceMessagePlayer";
import { ZapDialog } from "@/components/ZapDialog";
import { useAppContext } from "@/hooks/useAppContext";
import { useAuthor } from "@/hooks/useAuthor";
import { useCurrentUser } from "@/hooks/useCurrentUser";
import { useNip05Verify } from "@/hooks/useNip05Verify";
import { useOpenPost } from "@/hooks/useOpenPost";
import { useProfileUrl } from "@/hooks/useProfileUrl";
import { toast } from "@/hooks/useToast";
import { useEventStats } from "@/hooks/useTrending";
import { canZap } from "@/lib/canZap";
import { extractZapAmount, extractZapSender, extractZapMessage } from "@/hooks/useEventInteractions";
import { getContentWarning } from "@/lib/contentWarning";
import { genUserName } from "@/lib/genUserName";
import { getDisplayName } from "@/lib/getDisplayName";
import { type ImetaEntry, parseImetaMap } from "@/lib/imeta";
import { extractAudioUrls, extractVideoUrls } from "@/lib/mediaUrls";
import { usePollVoteLabel } from "@/hooks/usePollVoteLabel";
import { getParentEventHints, isReplyEvent } from "@/lib/nostrEvents";
import { isSingleImagePost } from "@/lib/noteContent";
import { shareOrCopy } from "@/lib/share";
import { timeAgo } from "@/lib/timeAgo";
import { formatNumber } from "@/lib/formatNumber";
import { publishedAtAction } from "@/lib/publishedAtAction";
import { getEffectiveStreamStatus } from "@/lib/streamStatus";
import { cn } from "@/lib/utils";


/** Profile card for use in feeds (kind 0). */
function ProfileCardContent({ event }: { event: NostrEvent }) {
  let metadata: Record<string, unknown> = {};
  try { metadata = JSON.parse(event.content); } catch { /* ignore */ }
  return (
    <div className="mt-2" onClick={(e) => e.stopPropagation()}>
      <ProfileCard pubkey={event.pubkey} metadata={metadata} showNip05={false} />
    </div>
  );
}

/* ──── Shared activity card shell for reaction / repost / zap / poll vote ──── */

interface ActivityCardProps {
  /** The round element in the left column (icon bubble or avatar). */
  icon: ReactNode;
  /** The actor row content (avatar + name + label + timestamp). */
  actorRow: ReactNode;
  /** Optional extra content below the actor row (zap message, vote label, etc.). */
  children?: ReactNode;
  /** Threaded mode: connector line below icon, no bottom border. */
  threaded?: boolean;
  /** Last item in thread — no connector line, has bottom border. */
  threadedLast?: boolean;
  /** Custom connector line class. */
  threadedLineClassName?: string;
  className?: string;
  onClick?: React.MouseEventHandler;
  onAuxClick?: React.MouseEventHandler;
}

export function ActivityCard({
  icon,
  actorRow,
  children,
  threaded,
  threadedLast,
  threadedLineClassName,
  className,
  onClick,
  onAuxClick,
}: ActivityCardProps) {
  const isThreaded = threaded || threadedLast;
  return (
    <article
      className={cn(
        "px-4 hover:bg-secondary/30 transition-colors cursor-pointer overflow-hidden",
        isThreaded
          ? cn("pt-3", threaded ? "pb-0" : "pb-3 border-b border-border")
          : "py-3 border-b border-border",
        className,
      )}
      onClick={onClick}
      onAuxClick={onAuxClick}
    >
      <div className="flex gap-3">
        <div className="flex flex-col items-center">
          {icon}
          {threaded && (
            <div className={cn("w-0.5 flex-1 mt-2 rounded-full", threadedLineClassName || "bg-foreground/20")} />
          )}
        </div>
        <div className={cn("flex-1 min-w-0", isThreaded ? "min-h-10 flex flex-col justify-center" : "", threaded && "pb-3")}>
          {actorRow}
          {children}
        </div>
      </div>
    </article>
  );
}

/** Reusable actor row: small avatar + display name + action label + timestamp. */
export interface ActorRowProps {
  pubkey: string;
  profileUrl: string;
  avatarShape: Parameters<typeof Avatar>[0]['shape'];
  picture?: string;
  displayName: string;
  authorEvent?: NostrEvent;
  isLoading?: boolean;
  label: string;
  /** Extra inline elements after the label (e.g. zap amount). */
  extra?: ReactNode;
  /** Formatted timestamp string (e.g. timeAgo or full date). */
  timestampLabel: string;
}

export function ActorRow({ pubkey, profileUrl, avatarShape, picture, displayName, authorEvent, isLoading, label, extra, timestampLabel }: ActorRowProps) {
  if (isLoading) {
    return (
      <div className="flex items-center gap-2">
        <Skeleton className="size-6 rounded-full shrink-0" />
        <Skeleton className="h-3.5 w-20" />
      </div>
    );
  }
  return (
    <div className="flex items-center gap-2">
      <ProfileHoverCard pubkey={pubkey} asChild>
        <Link to={profileUrl} className="shrink-0" onClick={(e) => e.stopPropagation()}>
          <Avatar shape={avatarShape} className="size-6">
            <AvatarImage src={picture} alt={displayName} />
            <AvatarFallback className="bg-primary/20 text-primary text-[8px]">{displayName[0]?.toUpperCase()}</AvatarFallback>
          </Avatar>
        </Link>
      </ProfileHoverCard>
      <ProfileHoverCard pubkey={pubkey} asChild>
        <Link to={profileUrl} className="font-semibold text-sm hover:underline truncate" onClick={(e) => e.stopPropagation()}>
          {authorEvent ? <EmojifiedText tags={authorEvent.tags}>{displayName}</EmojifiedText> : displayName}
        </Link>
      </ProfileHoverCard>
      <span className="text-sm text-muted-foreground shrink-0">{label}</span>
      {extra}
      <span className="text-xs text-muted-foreground ml-auto shrink-0">{timestampLabel}</span>
    </div>
  );
}

interface NoteCardProps {
  event: NostrEvent;
  className?: string;
  /** If set, shows a "Reposted by" header with this pubkey. */
  repostedBy?: string;
  /** If true, hide action buttons (used for embeds). */
  compact?: boolean;
  /** If true, render in threaded ancestor style: connector line below avatar, no bottom border. */
  threaded?: boolean;
  /** Custom class for the threaded connector line (overrides the default color). */
  threadedLineClassName?: string;
  /** Like threaded but without the connector line — used for the last item in a thread (e.g. sub-reply hint). */
  threadedLast?: boolean;
  /** If true, briefly highlight this card (e.g. newly loaded post). */
  highlight?: boolean;
  /** If true, suppress the kind-derived action header (e.g. "created a badge"). Used when the parent already provides context. */
  hideKindHeader?: boolean;
}

/** Gets a tag value by name. */
function getTag(tags: string[][], name: string): string | undefined {
  return tags.find(([n]) => n === name)?.[1];
}

/** Parse single imeta tag into structured object (legacy, for kind 34236 vines). */
function parseImeta(tags: string[][]): { url?: string; thumbnail?: string } {
  const imetaTag = tags.find(([name]) => name === "imeta");
  if (!imetaTag) return {};
  const result: Record<string, string> = {};
  for (let i = 1; i < imetaTag.length; i++) {
    const part = imetaTag[i];
    const spaceIdx = part.indexOf(" ");
    if (spaceIdx === -1) continue;
    const key = part.slice(0, spaceIdx);
    const value = part.slice(spaceIdx + 1);
    if (key === "url") result.url = value;
    else if (key === "image") result.thumbnail = value;
  }
  return result;
}

/** Encodes the NIP-19 identifier for navigating to an event. */
function encodeEventId(event: NostrEvent): string {
  // Addressable events (30000-39999) use naddr with their d-tag
  if (event.kind >= 30000 && event.kind < 40000) {
    const dTag = getTag(event.tags, "d");
    if (dTag) {
      return nip19.naddrEncode({
        kind: event.kind,
        pubkey: event.pubkey,
        identifier: dTag,
      });
    }
  }
  // Replaceable events (10000-19999) use naddr with an empty identifier
  if (event.kind >= 10000 && event.kind < 20000) {
    return nip19.naddrEncode({
      kind: event.kind,
      pubkey: event.pubkey,
      identifier: "",
    });
  }
  return nip19.neventEncode({ id: event.id, author: event.pubkey });
}

/** d-tags reserved by NIP-51 for other purposes — hide these kind 30000 events. */
const DEPRECATED_DTAGS = new Set(["mute", "pin", "bookmark", "communities"]);

/** Returns true if a kind 30000 event is a deprecated/junk list that should be hidden. */
function isDeprecatedFollowSet(event: NostrEvent): boolean {
  if (event.kind !== 30000) return false;
  const dTag = event.tags.find(([n]) => n === "d")?.[1] ?? "";
  if (DEPRECATED_DTAGS.has(dTag)) return true;
  // Filter empty lists with no p-tags or title
  const hasPTags = event.tags.some(([n]) => n === "p");
  const hasTitle = event.tags.some(([n]) => n === "title" || n === "name");
  if (!hasPTags && !hasTitle) return true;
  return false;
}

export const NoteCard = memo(function NoteCard({
  event,
  className,
  repostedBy,
  compact,
  threaded,
  threadedLineClassName,
  threadedLast,
  highlight,
  hideKindHeader,
}: NoteCardProps) {
  const { config } = useAppContext();
  const { user } = useCurrentUser();
  const author = useAuthor(event.pubkey);
  const zapSenderPubkey = useMemo(() => event.kind === 9735 ? extractZapSender(event) : '', [event]);
  const zapSender = useAuthor(zapSenderPubkey || undefined);
  const zapSenderMeta = zapSender.data?.metadata;
  const zapSenderShape = getAvatarShape(zapSenderMeta);
  const zapSenderName = getDisplayName(zapSenderMeta, zapSenderPubkey);
  const zapSenderUrl = useProfileUrl(zapSenderPubkey, zapSenderMeta);

  const pollVoteLabel = usePollVoteLabel(event);

  const metadata = author.data?.metadata;
  const avatarShape = getAvatarShape(metadata);
  const displayName = getDisplayName(metadata, event.pubkey);
  const nip05 = metadata?.nip05;
  const { data: nip05Verified, isPending: nip05Pending } = useNip05Verify(
    nip05,
    event.pubkey,
  );
  const profileUrl = useProfileUrl(event.pubkey, metadata);
  const encodedId = useMemo(() => encodeEventId(event), [event]);
  const { data: stats } = useEventStats(event.id, event);
  const [moreMenuOpen, setMoreMenuOpen] = useState(false);
  const [replyOpen, setReplyOpen] = useState(false);

  // Check if the current user can zap this event's author
  const canZapAuthor = user && canZap(metadata);

  const { onClick: openPost, onAuxClick: auxOpenPost } = useOpenPost(
    `/${encodedId}`,
  );

  // Handler to navigate to post detail, but only if click didn't originate from a modal
  const handleCardClick = (e: React.MouseEvent) => {
    const target = e.target as HTMLElement;
    if (
      target.closest('[role="dialog"]') ||
      target.closest("[data-radix-dialog-overlay]") ||
      target.closest("[data-radix-dialog-content]") ||
      target.closest("[data-vaul-drawer]") ||
      target.closest("[data-vaul-drawer-overlay]") ||
      target.closest('[data-testid="zap-modal"]') ||
      target.closest("button") ||
      target.closest("a")
    ) {
      return;
    }
    openPost();
  };

  const handleAuxClick = (e: React.MouseEvent) => {
    const target = e.target as HTMLElement;
    if (
      target.closest('[role="dialog"]') ||
      target.closest("[data-radix-dialog-overlay]") ||
      target.closest("[data-radix-dialog-content]") ||
      target.closest("[data-vaul-drawer]") ||
      target.closest("[data-vaul-drawer-overlay]") ||
      target.closest('[data-testid="zap-modal"]') ||
      target.closest("button") ||
      target.closest("a")
    ) {
      return;
    }
    auxOpenPost(e);
  };

  const isVine = event.kind === 34236;
  const isPoll = event.kind === 1068;
  const isGeocache = event.kind === 37516;
  const isFoundLog = event.kind === 7516;
  const isColor = event.kind === 3367;
  const isFollowPack = event.kind === 39089 || event.kind === 30000;
  const isArticle = event.kind === 30023;
  const isMagicDeck = event.kind === 37381;
  const isStream = event.kind === 30311;
  const isFileMetadata = event.kind === 1063;
  const isThemeDefinition = event.kind === 36767;
  const isActiveTheme = event.kind === 16767;
  const isTheme = isThemeDefinition || isActiveTheme;
  const isVoiceMessage = event.kind === 1222 || event.kind === 1244;
  const isCalendarEvent = event.kind === 31922 || event.kind === 31923;
  const isEmojiPack = event.kind === 30030;
  const isBadgeDefinition = event.kind === 30009;
  const isProfileBadges = event.kind === 10008 || event.kind === 30008;
  const isBadge = isBadgeDefinition || isProfileBadges;
  const isReaction = event.kind === 7;
  const isPollVote = event.kind === 1018;
  const isRepost = event.kind === 6 || event.kind === 16;
  const isPhoto = event.kind === 20;
  const isNormalVideo = event.kind === 21;
  const isShortVideo = event.kind === 22;
  const isVideo = isNormalVideo || isShortVideo;
  const isMusicTrack = event.kind === 36787;
  const isMusicPlaylist = event.kind === 34139;
  const isPodcastEpisode = event.kind === 30054;
  const isPodcastTrailer = event.kind === 30055;
  const isAudioKind =
    isMusicTrack || isMusicPlaylist || isPodcastEpisode || isPodcastTrailer;
  const isGitRepo = event.kind === 30617;
  const isPatch = event.kind === 1617;
  const isPullRequest = event.kind === 1618;
  const isCustomNip = event.kind === 30817;
  const isNsite = event.kind === 15128 || event.kind === 35128;
  const isZapstoreApp = event.kind === 32267;
  const isZapstoreRelease = event.kind === 30063;
  const isZapstoreAsset = event.kind === 3063;
  const isAppHandler = event.kind === 31990;
  const isEncryptedDM = event.kind === 4;
  const isLetter = event.kind === 8211;
  const isVanish = event.kind === 62;
  const isZap = event.kind === 9735;
  const isProfile = event.kind === 0;
  const isBlobbiState = event.kind === 31124;
  const isDevKind = isGitRepo || isPatch || isPullRequest || isCustomNip || isNsite;
  const isTextNote =
    !isVine &&
    !isPoll &&
    !isGeocache &&
    !isFoundLog &&
    !isColor &&
    !isFollowPack &&
    !isArticle &&
    !isMagicDeck &&
    !isStream &&
    !isFileMetadata &&
    !isTheme &&
    !isVoiceMessage &&
    !isCalendarEvent &&
    !isEmojiPack &&
    !isBadge &&
    !isReaction &&
    !isPollVote &&
    !isRepost &&
    !isPhoto &&
    !isVideo &&
    !isAudioKind &&
    !isDevKind &&
    !isZapstoreApp &&
    !isZapstoreRelease &&
    !isZapstoreAsset &&
    !isAppHandler &&
    !isEncryptedDM &&
    !isLetter &&
    !isVanish &&
    !isZap &&
    !isProfile &&
    !isBlobbiState;

  // Kind 1 specific — images now render inline in NoteContent, only videos go to NoteMedia
  const videos = useMemo(
    () => (isTextNote ? extractVideoUrls(event.content) : []),
    [event.content, isTextNote],
  );
  const audios = useMemo(() => {
    if (!isTextNote) return [];
    // Prefer imeta-declared audio over URL scraping
    const imetaAudios = Array.from(parseImetaMap(event.tags).values())
      .filter((e) => e.mime?.startsWith("audio/"))
      .map((e) => e.url);
    if (imetaAudios.length > 0) return imetaAudios;
    return extractAudioUrls(event.content);
  }, [event.content, event.tags, isTextNote]);
  const imetaMap = useMemo(
    () =>
      isTextNote ? parseImetaMap(event.tags) : new Map<string, ImetaEntry>(),
    [event.tags, isTextNote],
  );

  // Extract webxdc attachments from imeta tags
  const webxdcApps = useMemo(() => {
    if (!isTextNote) return [];
    return Array.from(imetaMap.values()).filter(
      (entry) =>
        entry.mime === "application/x-webxdc" ||
        entry.mime === "application/vnd.webxdc+zip",
    );
  }, [imetaMap, isTextNote]);
  const isComment = event.kind === 1111;
  const isReply = isTextNote && !isComment && isReplyEvent(event);

  // Find all people being replied to (for "Replying to @user1 and @user2")
  const replyToPubkeys = useMemo(() => {
    if (!isTextNote || !isReply) return [];

    // Get all p tags that aren't marked as mentions
    const pTags = event.tags.filter(
      ([name, , , marker]) => name === "p" && marker !== "mention",
    );

    if (pTags.length > 0) {
      // Remove duplicates and filter out undefined/empty pubkeys
      return [
        ...new Set(pTags.map(([, pubkey]) => pubkey).filter(Boolean)),
      ] as string[];
    }

    // Fallback: if all p tags are mentions, use all p tags anyway
    const allPTags = event.tags.filter(([name]) => name === "p");
    if (allPTags.length > 0) {
      return [
        ...new Set(allPTags.map(([, pubkey]) => pubkey).filter(Boolean)),
      ] as string[];
    }

    // Self-reply fallback: when replying to own post, no p tags are added (the
    // author's own pubkey is excluded during compose). Try to extract the parent
    // author from the reply/root e-tag's 5th element (NIP-10 pubkey hint), and
    // ultimately fall back to the event author (self-reply).
    const eTags = event.tags.filter(
      ([name, , , marker]) => name === "e" && marker !== "mention",
    );
    const replyTag = eTags.find(([, , , marker]) => marker === "reply");
    const rootTag = eTags.find(([, , , marker]) => marker === "root");
    const parentAuthor = replyTag?.[4] || rootTag?.[4] || event.pubkey;
    return [parentAuthor];
  }, [event.tags, isTextNote, isReply, event.pubkey]);

  // Extract the parent event ID + relay/author hints for reply hover card preview
  const parentHints = useMemo(() => {
    if (!isReply) return undefined;
    return getParentEventHints(event);
  }, [event, isReply]);
  const parentEventId = parentHints?.id;

  // Kind 34236 specific
  const imeta = useMemo(
    () => (isVine ? parseImeta(event.tags) : undefined),
    [event.tags, isVine],
  );
  const vineTitle = isVine ? getTag(event.tags, "title") : undefined;
  const hashtags = isVine
    ? event.tags.filter(([n]) => n === "t").map(([, v]) => v)
    : [];

  // Filter out deprecated/junk kind 30000 events
  if (isDeprecatedFollowSet(event)) {
    return null;
  }

  // NIP-36: If the event has a content-warning and the policy is "hide", skip rendering entirely
  if (
    getContentWarning(event) !== undefined &&
    config.contentWarningPolicy === "hide"
  ) {
    return null;
  }

  // Hide magic decks tagged t:unlisted and treasures tagged t:hidden
  if (
    isMagicDeck &&
    event.tags.some(([n, v]) => n === "t" && v === "unlisted")
  ) {
    return null;
  }
  if (isGeocache && event.tags.some(([n, v]) => n === "t" && v === "hidden")) {
    return null;
  }

  // Shared content block used in both normal and threaded layouts
  const contentBlock = (
    <>
      {/* Reply context (kind 1) or comment context (kind 1111) — shown above content */}
      {isComment && <CommentContext event={event} />}
      {isReply && (
        <ReplyContext
          pubkeys={replyToPubkeys}
          parentEventId={parentEventId}
          parentRelayHint={parentHints?.relayHint}
          parentAuthorHint={parentHints?.authorHint}
        />
      )}

      {/* Content — kind-based dispatch, guarded by NIP-36 content-warning */}
      <ContentWarningGuard event={event}>
        {isPhoto ? (
          <PhotoContent event={event} />
        ) : isVideo ? (
          <VideoContent event={event} />
        ) : isVine ? (
          <>
            {vineTitle && (
              <p className="text-[15px] mt-2 leading-relaxed break-words overflow-hidden">
                {vineTitle}
              </p>
            )}
            <VineMedia imeta={imeta} hashtags={hashtags} />
          </>
        ) : isPoll ? (
          <PollContent event={event} />
        ) : isGeocache ? (
          <GeocacheContent event={event} />
        ) : isFoundLog ? (
          <FoundLogContent event={event} />
        ) : isColor ? (
          <ColorMomentContent event={event} />
        ) : isFollowPack ? (
          <FollowPackContent event={event} />
        ) : isArticle ? (
          <Suspense fallback={<Skeleton className="h-24 w-full rounded-lg" />}>
            <ArticleContent event={event} preview className="mt-2" />
          </Suspense>
        ) : isMagicDeck ? (
          <MagicDeckContent event={event} />
        ) : isStream ? (
          <StreamContent event={event} />
        ) : isFileMetadata ? (
          <FileMetadataContent event={event} compact />
        ) : isEmojiPack ? (
          <EmojiPackContent event={event} />
        ) : isBadgeDefinition ? (
          <BadgeContent event={event} />
        ) : isProfileBadges ? (
          <ProfileBadgesContent event={event} />
        ) : isTheme ? (
          <ThemeContent event={event} />
        ) : isVoiceMessage ? (
          <VoiceMessagePlayer event={event} />
        ) : isCalendarEvent ? (
          <CalendarEventContent event={event} compact />
        ) : isMusicTrack ? (
          <MusicTrackContent event={event} />
        ) : isMusicPlaylist ? (
          <MusicPlaylistContent event={event} />
        ) : isPodcastEpisode ? (
          <PodcastEpisodeContent event={event} />
        ) : isPodcastTrailer ? (
          <PodcastTrailerContent event={event} />
        ) : isGitRepo ? (
          <GitRepoCard event={event} />
        ) : isPatch ? (
          <PatchCard event={event} />
        ) : isPullRequest ? (
          <Suspense fallback={<Skeleton className="h-24 w-full rounded-lg" />}>
            <PullRequestCard event={event} />
          </Suspense>
        ) : isCustomNip ? (
          <Suspense fallback={<Skeleton className="h-24 w-full rounded-lg" />}>
            <CustomNipCard event={event} />
          </Suspense>
        ) : isNsite ? (
          <NsiteCard event={event} />
        ) : isZapstoreApp ? (
          <div className="mt-2 rounded-xl border border-border overflow-hidden transition-all duration-300 hover:shadow-md hover:border-primary/20">
            <div className="px-3.5 pb-3.5 pt-3">
              <ZapstoreAppContent event={event} compact />
            </div>
          </div>
        ) : isZapstoreRelease ? (
          <div className="mt-2 rounded-xl border border-border overflow-hidden transition-all duration-300 hover:shadow-md hover:border-primary/20">
            <div className="px-3.5 pb-3.5 pt-3">
              <ZapstoreReleaseContent event={event} compact />
            </div>
          </div>
        ) : isZapstoreAsset ? (
          <div className="mt-2 rounded-xl border border-border overflow-hidden transition-all duration-300 hover:shadow-md hover:border-primary/20">
            <div className="px-3.5 pb-3.5 pt-3">
              <ZapstoreAssetContent event={event} compact />
            </div>
          </div>
        ) : isAppHandler ? (
          <AppHandlerContent event={event} compact />
        ) : isEncryptedDM ? (
          <EncryptedMessageContent event={event} compact />
        ) : isLetter ? (
          <EncryptedLetterContent event={event} compact />
        ) : isProfile ? (
          <ProfileCardContent event={event} />
        ) : isBlobbiState ? (
          <Suspense fallback={<Skeleton className="h-24 w-full rounded-lg" />}>
            <BlobbiStateCard event={event} />
          </Suspense>
        ) : (
          <TruncatedNoteContent
            event={event}
            videos={videos}
            audios={audios}
            imetaMap={imetaMap}
            webxdcApps={webxdcApps}
          />
        )}
      </ContentWarningGuard>
    </>
  );

  // Shared author info block — min-h-[42px] keeps the container the same height
  // whether the skeleton or the resolved profile is rendered, preventing layout shifts.
  const authorInfo = author.isLoading ? (
    <div className="min-w-0 min-h-[42px] flex flex-col justify-center space-y-1.5">
      <Skeleton className="h-4 w-28" />
      <Skeleton className="h-3 w-36" />
    </div>
  ) : (
    <div className="min-w-0 flex-1 min-h-[42px] flex flex-col justify-center">
      <div className="flex items-center gap-1.5">
        <ProfileHoverCard pubkey={event.pubkey} asChild>
          <Link
            to={profileUrl}
            className="font-bold text-[15px] hover:underline truncate"
            onClick={(e) => e.stopPropagation()}
          >
            {author.data?.event ? (
              <EmojifiedText tags={author.data.event.tags}>
                {displayName}
              </EmojifiedText>
            ) : (
              displayName
            )}
          </Link>
        </ProfileHoverCard>
        {metadata?.bot && (
          <span className="text-xs text-primary shrink-0" title="Bot account">
            🤖
          </span>
        )}
      </div>
      <div className="flex items-center gap-1 text-sm text-muted-foreground min-w-0 pr-2">
        {nip05 && nip05Pending && <Skeleton className="h-3 w-24" />}
        {nip05 && nip05Pending && <span className="shrink-0">·</span>}
        {nip05 && nip05Verified && (
          <Nip05Badge nip05={nip05} pubkey={event.pubkey} />
        )}
        {nip05 && nip05Verified && <span className="shrink-0">·</span>}
        <span className="shrink-0 hover:underline whitespace-nowrap">
          {timeAgo(event.created_at)}
        </span>
      </div>
    </div>
  );

  // Shared avatar element
  const avatarElement = author.isLoading ? (
    <Skeleton
      className={cn(
        threaded || threadedLast ? "size-10" : "size-11",
        "rounded-full shrink-0",
      )}
    />
  ) : (
    <ProfileHoverCard pubkey={event.pubkey} asChild>
      <Link
        to={profileUrl}
        className="shrink-0"
        onClick={(e) => e.stopPropagation()}
      >
        <Avatar shape={avatarShape} className={threaded || threadedLast ? "size-10" : "size-11"}>
          <AvatarImage src={metadata?.picture} alt={displayName} />
          <AvatarFallback className="bg-primary/20 text-primary text-sm">
            {displayName[0]?.toUpperCase()}
          </AvatarFallback>
        </Avatar>
      </Link>
    </ProfileHoverCard>
  );

  // ── Shared action buttons (used in all layouts) ──
  const actionButtons = (
    <div className="flex items-center gap-5 mt-3 -ml-2">
      <button
        className="flex items-center gap-1.5 p-2 rounded-full text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
        title="Reply"
        onClick={(e) => {
          e.stopPropagation();
          setReplyOpen(true);
        }}
      >
        <MessageCircle className="size-5" />
            {stats?.replies ? (
              <span className="text-sm tabular-nums">{formatNumber(stats.replies)}</span>
            ) : null}
          </button>

          <RepostMenu event={event}>
            {(isReposted: boolean) => (
              <button
                className={`flex items-center gap-1.5 p-2 rounded-full transition-colors ${isReposted ? "text-accent hover:text-accent/80 hover:bg-accent/10" : "text-muted-foreground hover:text-accent hover:bg-accent/10"}`}
                title={isReposted ? "Undo repost" : "Repost"}
              >
                <RepostIcon className="size-5" />
                {stats?.reposts || stats?.quotes ? (
                  <span className="text-sm tabular-nums">
                    {formatNumber((stats?.reposts ?? 0) + (stats?.quotes ?? 0))}
                  </span>
                ) : null}
              </button>
            )}
          </RepostMenu>

          <ReactionButton
        eventId={event.id}
        eventPubkey={event.pubkey}
        eventKind={event.kind}
        reactionCount={stats?.reactions}
      />

      {canZapAuthor && (
        <ZapDialog target={event}>
          <button
            className="flex items-center gap-1.5 p-2 rounded-full text-muted-foreground hover:text-amber-500 hover:bg-amber-500/10 transition-colors"
            title="Zap"
          >
            <Zap className="size-5" />
            {stats?.zapAmount ? (
              <span className="text-sm tabular-nums">
                {formatNumber(stats.zapAmount)}
              </span>
            ) : null}
          </button>
        </ZapDialog>
      )}

      <button
        className="p-2 rounded-full text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors sidebar:hidden"
        title="Share"
        onClick={async (e) => {
          e.stopPropagation();
          const url = `${window.location.origin}/${encodedId}`;
          const result = await shareOrCopy(url);
          if (result === "copied") toast({ title: "Link copied to clipboard" });
        }}
      >
        <Share2 className="size-5" />
      </button>

      <button
        className="p-2 rounded-full text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
        title="More"
        onClick={(e) => {
          e.stopPropagation();
          setMoreMenuOpen(true);
        }}
      >
        <MoreHorizontal className="size-5" />
      </button>
    </div>
  );

  // ── Vanish layout (kind 62) — dramatic card, no author row ──
  if (isVanish) {
    // Threaded vanish (ancestor in a reply thread — needs connector line + avatar column)
    if (threaded || threadedLast) {
      return (
        <article
          className={cn(
            "px-4 pt-3 hover:bg-secondary/30 transition-colors cursor-pointer overflow-hidden",
            threaded ? "pb-0" : "pb-3 border-b border-border",
            className,
          )}
          onClick={handleCardClick}
          onAuxClick={handleAuxClick}
        >
          <div className="flex gap-3">
            <div className="flex flex-col items-center">
              {avatarElement}
              {threaded && (
                <div className={cn("w-0.5 flex-1 mt-2 rounded-full", threadedLineClassName || "bg-foreground/20")} />
              )}
            </div>
            <div className={cn("flex-1 min-w-0", threaded && "pb-3")}>
              <VanishCardCompact event={event} timestamp={timeAgo(event.created_at)} />
              {!compact && (
                <>
                  {actionButtons}
                  <NoteMoreMenu event={event} open={moreMenuOpen} onOpenChange={setMoreMenuOpen} />
                  <ReplyComposeModal event={event} open={replyOpen} onOpenChange={setReplyOpen} />
                </>
              )}
            </div>
          </div>
        </article>
      );
    }

    return (
      <article
        className={cn(
          "px-4 py-3 border-b border-border hover:bg-secondary/30 transition-colors cursor-pointer overflow-hidden",
          className,
        )}
        onClick={handleCardClick}
        onAuxClick={handleAuxClick}
      >
        <VanishCardCompact event={event} />
        {!compact && (
          <>
            {actionButtons}
            <NoteMoreMenu
              event={event}
              open={moreMenuOpen}
              onOpenChange={setMoreMenuOpen}
            />
            <ReplyComposeModal
              event={event}
              open={replyOpen}
              onOpenChange={setReplyOpen}
            />
          </>
        )}
      </article>
    );
  }

  // ── Reaction layout (kind 7) ──
  if (isReaction) {
    const iconSize = threaded || threadedLast ? "size-10" : "size-11";
    return (
      <ActivityCard
        icon={
          <div className={cn("flex items-center justify-center rounded-full bg-pink-500/10 shrink-0 text-lg leading-none", iconSize)}>
            <ReactionEmoji content={event.content} tags={event.tags} className="h-5 w-5 object-contain" />
          </div>
        }
        actorRow={
          <ActorRow pubkey={event.pubkey} profileUrl={profileUrl} avatarShape={avatarShape} picture={metadata?.picture}
            displayName={displayName} authorEvent={author.data?.event} isLoading={author.isLoading} label="reacted" timestampLabel={timeAgo(event.created_at)} />
        }
        threaded={threaded} threadedLast={threadedLast} threadedLineClassName={threadedLineClassName}
        className={className} onClick={handleCardClick} onAuxClick={handleAuxClick}
      />
    );
  }

  // ── Repost layout (kind 6 / 16) ──
  if (isRepost) {
    const iconSize = threaded || threadedLast ? "size-10" : "size-11";
    return (
      <ActivityCard
        icon={
          <div className={cn("flex items-center justify-center rounded-full bg-accent/10 shrink-0", iconSize)}>
            <RepostIcon className="size-5 text-accent" />
          </div>
        }
        actorRow={
          <ActorRow pubkey={event.pubkey} profileUrl={profileUrl} avatarShape={avatarShape} picture={metadata?.picture}
            displayName={displayName} authorEvent={author.data?.event} isLoading={author.isLoading} label="reposted" timestampLabel={timeAgo(event.created_at)} />
        }
        threaded={threaded} threadedLast={threadedLast} threadedLineClassName={threadedLineClassName}
        className={className} onClick={handleCardClick} onAuxClick={handleAuxClick}
      />
    );
  }

  // ── Zap receipt layout (kind 9735) ──
  if (isZap) {
    const zapAmountSats = Math.floor(extractZapAmount(event) / 1000);
    const zapMessage = extractZapMessage(event);
    const iconSize = threaded || threadedLast ? "size-10" : "size-11";
    return (
      <ActivityCard
        icon={
          <div className={cn("flex items-center justify-center rounded-full bg-amber-500/10 shrink-0", iconSize)}>
            <Zap className="size-5 text-amber-500 fill-amber-500" />
          </div>
        }
        actorRow={
          <ActorRow pubkey={zapSenderPubkey} profileUrl={zapSenderUrl} avatarShape={zapSenderShape} picture={zapSenderMeta?.picture}
            displayName={zapSenderName} authorEvent={zapSender.data?.event} isLoading={zapSender.isLoading} label="zapped" timestampLabel={timeAgo(event.created_at)}
            extra={zapAmountSats > 0 ? (
              <span className="text-sm font-semibold text-amber-500 shrink-0">
                {formatNumber(zapAmountSats)} {zapAmountSats === 1 ? 'sat' : 'sats'}
              </span>
            ) : undefined}
          />
        }
        threaded={threaded} threadedLast={threadedLast} threadedLineClassName={threadedLineClassName}
        className={className} onClick={handleCardClick} onAuxClick={handleAuxClick}
      >
        {zapMessage && <p className="text-xs text-muted-foreground italic mt-1">&ldquo;{zapMessage}&rdquo;</p>}
      </ActivityCard>
    );
  }

  // ── Poll vote layout (kind 1018) ──
  if (isPollVote) {
    const iconSize = threaded || threadedLast ? "size-10" : "size-11";
    return (
      <ActivityCard
        icon={
          <ProfileHoverCard pubkey={event.pubkey} asChild>
            <Link to={profileUrl} className="shrink-0" onClick={(e) => e.stopPropagation()}>
              <Avatar shape={avatarShape} className={iconSize}>
                <AvatarImage src={metadata?.picture} alt={displayName} />
                <AvatarFallback className="bg-primary/20 text-primary text-sm">{displayName[0]?.toUpperCase()}</AvatarFallback>
              </Avatar>
            </Link>
          </ProfileHoverCard>
        }
        actorRow={
          <div className="flex items-center gap-1.5">
            <ProfileHoverCard pubkey={event.pubkey} asChild>
              <Link to={profileUrl} className="font-semibold text-sm hover:underline truncate" onClick={(e) => e.stopPropagation()}>
                {author.data?.event ? <EmojifiedText tags={author.data.event.tags}>{displayName}</EmojifiedText> : displayName}
              </Link>
            </ProfileHoverCard>
            <span className="text-sm text-muted-foreground shrink-0">voted</span>
            <span className="text-xs text-muted-foreground ml-auto shrink-0">{timeAgo(event.created_at)}</span>
          </div>
        }
        threaded={threaded} threadedLast={threadedLast} threadedLineClassName={threadedLineClassName}
        className={className} onClick={handleCardClick} onAuxClick={handleAuxClick}
      >
        {pollVoteLabel && <p className="text-sm font-semibold mt-0.5 truncate">{pollVoteLabel}</p>}
      </ActivityCard>
    );
  }

  // ── Threaded layout (with or without connector line) ──
  if (threaded || threadedLast) {
    // Kind action header (e.g. "updated their badges") — same logic as normal layout
    const threadedKindHeader = !repostedBy && !hideKindHeader && KIND_HEADER_MAP[event.kind]
      ? (() => {
          const cfg = KIND_HEADER_MAP[event.kind];
          const isLive = event.kind === 30311 && getEffectiveStreamStatus(event) === "live";
          return (
            <EventActionHeader
              pubkey={event.pubkey}
              icon={cfg.icon}
              iconClassName={
                event.kind === 30311
                  ? isLive ? "text-primary" : "text-muted-foreground"
                  : cfg.iconClassName
              }
              action={typeof cfg.action === "function" ? cfg.action(event) : cfg.action}
              noun={cfg.noun}
              nounRoute={cfg.nounRoute}
            />
          );
        })()
      : null;

    return (
      <article
        className={cn(
          "px-4 pt-3 hover:bg-secondary/30 transition-colors cursor-pointer overflow-hidden",
          threaded ? "pb-0" : "pb-3 border-b border-border",
          className,
        )}
        onClick={handleCardClick}
        onAuxClick={handleAuxClick}
      >
        {threadedKindHeader}
        <div className="flex gap-3">
          <div className="flex flex-col items-center">
            {avatarElement}
            {threaded && (
              <div className={cn("w-0.5 flex-1 mt-2 rounded-full", threadedLineClassName || "bg-foreground/20")} />
            )}
          </div>
          <div className={cn("flex-1 min-w-0", threaded && "pb-3")}>
            {authorInfo}
            {contentBlock}
            {actionButtons}
            <NoteMoreMenu
              event={event}
              open={moreMenuOpen}
              onOpenChange={setMoreMenuOpen}
            />
            <ReplyComposeModal
              event={event}
              open={replyOpen}
              onOpenChange={setReplyOpen}
            />
          </div>
        </div>
      </article>
    );
  }

  // ── Normal layout ──
  return (
    <article
      className={cn(
        "px-4 py-3 border-b border-border hover:bg-secondary/30 transition-colors cursor-pointer overflow-hidden",
        highlight && "animate-highlight-fade",
        className,
      )}
      onClick={handleCardClick}
      onAuxClick={handleAuxClick}
    >
      {/* Action header — repost takes priority, otherwise derived from event kind */}
      {repostedBy ? (
        <EventActionHeader
          pubkey={repostedBy}
          icon={RepostIcon}
          iconClassName="text-accent"
          action="reposted"
        />
      ) : (
        !hideKindHeader && KIND_HEADER_MAP[event.kind] &&
        (() => {
          const cfg = KIND_HEADER_MAP[event.kind];
          const isLive =
            event.kind === 30311 && getEffectiveStreamStatus(event) === "live";
          return (
            <EventActionHeader
              pubkey={event.pubkey}
              icon={cfg.icon}
              iconClassName={
                event.kind === 30311
                  ? isLive
                    ? "text-primary"
                    : "text-muted-foreground"
                  : cfg.iconClassName
              }
              action={
                typeof cfg.action === "function"
                  ? cfg.action(event)
                  : cfg.action
              }
              noun={cfg.noun}
              nounRoute={cfg.nounRoute}
            />
          );
        })()
      )}

      {/* Header: avatar + name/handle stacked */}
      <div className="flex items-center gap-3">
        {avatarElement}
        {authorInfo}
        {isColor && <ColorMomentEyeButton event={event} />}
      </div>

      {contentBlock}

      {/* Action buttons — hidden in compact/embed mode */}
      {!compact && (
        <>
          {actionButtons}
          <NoteMoreMenu
            event={event}
            open={moreMenuOpen}
            onOpenChange={setMoreMenuOpen}
          />
          <ReplyComposeModal
            event={event}
            open={replyOpen}
            onOpenChange={setReplyOpen}
          />
        </>
      )}
    </article>
  );
});

const MAX_HEIGHT = 400; // px — posts taller than this get truncated

/** Truncates long text note content with a "Read more" fade + button.
 *  Media attachments are also hidden behind the truncation and revealed on expand. */
function TruncatedNoteContent({
  event,
  videos,
  audios = [],
  imetaMap,
  webxdcApps = [],
}: {
  event: NostrEvent;
  videos: string[];
  audios?: string[];
  imetaMap: Map<string, ImetaEntry>;
  webxdcApps?: ImetaEntry[];
}) {
  const contentRef = useRef<HTMLDivElement>(null);
  const [overflows, setOverflows] = useState(false);
  const [expanded, setExpanded] = useState(false);

  const singleImage = isSingleImagePost(event);

  const measure = useCallback(() => {
    const el = contentRef.current;
    if (el) setOverflows(!singleImage && el.scrollHeight > MAX_HEIGHT);
  }, [singleImage]);

  useEffect(() => {
    measure();
    window.addEventListener("resize", measure);
    return () => window.removeEventListener("resize", measure);
  }, [measure]);

  // Re-measure after images load — scrollHeight is unreliable before images have rendered.
  useEffect(() => {
    const el = contentRef.current;
    if (!el) return;
    const imgs = el.querySelectorAll("img");
    if (imgs.length === 0) return;
    imgs.forEach((img) =>
      img.addEventListener("load", measure, { once: true }),
    );
    return () =>
      imgs.forEach((img) => img.removeEventListener("load", measure));
  }, [measure]);

  const showMedia = !overflows || expanded;

  return (
    <div className="mt-2 break-words overflow-hidden">
      <div
        ref={contentRef}
        style={
          !expanded && overflows
            ? { maxHeight: MAX_HEIGHT, overflow: "hidden" }
            : undefined
        }
        className="relative"
      >
        <NoteContent event={event} className="text-[15px] leading-relaxed" />
        {!expanded && overflows && (
          <div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-background to-transparent pointer-events-none" />
        )}
      </div>
      {overflows && (
        <button
          className="mt-1 text-sm text-primary hover:underline"
          onClick={(e) => {
            e.stopPropagation();
            setExpanded((v) => !v);
          }}
        >
          {expanded ? "Show less" : "Read more"}
        </button>
      )}
      {showMedia && (
        <NoteMedia
          videos={videos}
          audios={audios}
          imetaMap={imetaMap}
          webxdcApps={webxdcApps}
          event={event}
        />
      )}
    </div>
  );
}

// ── NIP-68 Photo content (kind 20) ────────────────────────────────────────────

/** Parse all imeta image URLs from NIP-68 photo events. */
function parsePhotoUrls(
  tags: string[][],
): Array<{ url: string; alt?: string; blurhash?: string }> {
  const results: Array<{ url: string; alt?: string; blurhash?: string }> = [];
  for (const tag of tags) {
    if (tag[0] !== "imeta") continue;
    const parts: Record<string, string> = {};
    for (let i = 1; i < tag.length; i++) {
      const p = tag[i];
      const sp = p.indexOf(" ");
      if (sp !== -1) parts[p.slice(0, sp)] = p.slice(sp + 1);
    }
    if (parts.url)
      results.push({
        url: parts.url,
        alt: parts.alt,
        blurhash: parts.blurhash,
      });
  }
  return results;
}

/** Inline photo gallery for NIP-68 kind 20 events. */
function PhotoContent({ event }: { event: NostrEvent }) {
  const photos = useMemo(() => parsePhotoUrls(event.tags), [event.tags]);
  const title = getTag(event.tags, "title");
  const description = event.content;
  const hashtags = event.tags.filter(([n]) => n === "t").map(([, v]) => v);

  // Build imetaMap with dim + blurhash so ImageGallery can show blurhash placeholders
  const imetaMap = useMemo(() => {
    const map = new Map<string, { dim?: string; blurhash?: string }>();
    for (const photo of photos) {
      map.set(photo.url, { blurhash: photo.blurhash });
    }
    return map;
  }, [photos]);

  if (photos.length === 0) return null;

  return (
    <div className="mt-2 space-y-2">
      {title && <p className="font-semibold text-[15px]">{title}</p>}
      <ImageGallery
        images={photos.map((p) => p.url)}
        maxVisible={4}
        maxGridHeight="480px"
        imetaMap={imetaMap}
      />
      {description && (
        <p className="text-[15px] leading-relaxed text-muted-foreground">
          {description}
        </p>
      )}
      {hashtags.length > 0 && (
        <div className="flex flex-wrap gap-1.5">
          {hashtags.slice(0, 5).map((tag) => (
            <Link
              key={tag}
              to={`/t/${encodeURIComponent(tag)}`}
              className="text-xs text-primary hover:underline"
              onClick={(e) => e.stopPropagation()}
            >
              #{tag}
            </Link>
          ))}
        </div>
      )}
    </div>
  );
}

// ── NIP-71 Video content (kinds 21 & 22) ──────────────────────────────────────

/** Parse the primary video url and thumbnail from NIP-71 imeta tags. */
function parseVideoImeta(tags: string[][]): {
  url?: string;
  thumbnail?: string;
  duration?: string;
} {
  for (const tag of tags) {
    if (tag[0] !== "imeta") continue;
    const parts: Record<string, string> = {};
    for (let i = 1; i < tag.length; i++) {
      const p = tag[i];
      const sp = p.indexOf(" ");
      if (sp !== -1) parts[p.slice(0, sp)] = p.slice(sp + 1);
    }
    if (parts.url)
      return {
        url: parts.url,
        thumbnail: parts.image,
        duration: parts.duration,
      };
  }
  // Fallback to plain url/thumb tags
  return {
    url: tags.find(([n]) => n === "url")?.[1],
    thumbnail:
      tags.find(([n]) => n === "thumb")?.[1] ??
      tags.find(([n]) => n === "image")?.[1],
  };
}

/** Format seconds into MM:SS / HH:MM:SS. */
function fmtDuration(seconds: string | undefined): string | undefined {
  const s = parseFloat(seconds ?? "");
  if (isNaN(s) || s <= 0) return undefined;
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const sec = Math.floor(s % 60);
  const mm = String(m).padStart(2, "0");
  const ss = String(sec).padStart(2, "0");
  return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
}

/** Inline video player for NIP-71 kind 21/22 events. */
function VideoContent({ event }: { event: NostrEvent }) {
  const { url, thumbnail, duration } = useMemo(
    () => parseVideoImeta(event.tags),
    [event.tags],
  );
  const title = getTag(event.tags, "title");
  const description = event.content;
  const isShort = event.kind === 22;
  const formattedDuration = fmtDuration(duration);
  const hashtags = event.tags.filter(([n]) => n === "t").map(([, v]) => v);

  if (!url) return null;

  return (
    <div className="mt-2 space-y-2">
      {title && <p className="font-semibold text-[15px]">{title}</p>}
      <div
        className={cn(
          "relative rounded-xl overflow-hidden bg-muted",
          isShort ? "max-w-[280px]" : "",
        )}
      >
        <VideoPlayer src={url} poster={thumbnail} title={title ?? undefined} />
        {formattedDuration && (
          <div className="absolute bottom-2 right-2 bg-black/80 text-white text-[10px] px-1.5 py-0.5 rounded font-medium pointer-events-none">
            {formattedDuration}
          </div>
        )}
        {isShort && (
          <div className="absolute top-2 left-2 pointer-events-none">
            <span className="text-[10px] bg-black/60 text-white px-1.5 py-0.5 rounded">
              Short
            </span>
          </div>
        )}
      </div>
      {description && (
        <p className="text-[15px] leading-relaxed text-muted-foreground">
          {description}
        </p>
      )}
      {hashtags.length > 0 && (
        <div className="flex flex-wrap gap-1.5">
          {hashtags.slice(0, 5).map((tag) => (
            <Link
              key={tag}
              to={`/t/${encodeURIComponent(tag)}`}
              className="text-xs text-primary hover:underline"
              onClick={(e) => e.stopPropagation()}
            >
              #{tag}
            </Link>
          ))}
        </div>
      )}
    </div>
  );
}

/** Media content for kind 34236 vine events — rendered at full card width. */
function VineMedia({
  imeta,
  hashtags,
}: {
  imeta?: { url?: string; thumbnail?: string };
  hashtags: string[];
}) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const [isPlaying, setIsPlaying] = useState(false);

  // Pause video when scrolled out of view
  useEffect(() => {
    const video = videoRef.current;
    const container = containerRef.current;
    if (!video || !container) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (!entry.isIntersecting && !video.paused) {
          video.pause();
        }
      },
      { threshold: 0.25 },
    );

    observer.observe(container);
    return () => observer.disconnect();
  }, []);

  const handlePlayToggle = (e: React.MouseEvent) => {
    e.stopPropagation();
    const video = videoRef.current;
    if (!video) return;
    if (video.paused) {
      video.play();
      setIsPlaying(true);
    } else {
      video.pause();
      setIsPlaying(false);
    }
  };

  return (
    <>
      {imeta?.url && (
        <div
          ref={containerRef}
          className="relative mt-3 rounded-2xl overflow-hidden cursor-pointer"
          onClick={handlePlayToggle}
        >
          <video
            ref={videoRef}
            src={imeta.url}
            poster={imeta.thumbnail}
            className="w-full max-h-[70vh] object-cover"
            loop
            playsInline
            preload="none"
            onPlay={() => setIsPlaying(true)}
            onPause={() => setIsPlaying(false)}
          />
          {!isPlaying && (
            <div className="absolute inset-0 flex items-center justify-center bg-black/20">
              <div className="size-14 rounded-full bg-black/60 flex items-center justify-center backdrop-blur-sm">
                <Play className="size-7 text-white ml-1" fill="white" />
              </div>
            </div>
          )}
        </div>
      )}

      {hashtags.length > 0 && (
        <div className="flex flex-wrap gap-1.5 mt-2">
          {hashtags.slice(0, 5).map((tag) => (
            <Link
              key={tag}
              to={`/t/${encodeURIComponent(tag)}`}
              className="text-xs text-primary hover:underline"
              onClick={(e) => e.stopPropagation()}
            >
              #{tag}
            </Link>
          ))}
        </div>
      )}
    </>
  );
}

/** Stream status badge config. */
function getStreamStatusConfig(status: string | undefined) {
  switch (status) {
    case "live":
      return {
        label: "LIVE",
        className: "bg-red-600 hover:bg-red-600 text-white border-red-600",
      };
    case "ended":
      return {
        label: "ENDED",
        className: "bg-muted text-muted-foreground border-border",
      };
    case "planned":
      return {
        label: "PLANNED",
        className:
          "bg-blue-600/90 hover:bg-blue-600/90 text-white border-blue-600",
      };
    default:
      return {
        label: status?.toUpperCase() || "UNKNOWN",
        className: "bg-muted text-muted-foreground border-border",
      };
  }
}

/** Inline content for kind 30311 live stream events. */
function StreamContent({ event }: { event: NostrEvent }) {
  const title = getTag(event.tags, "title") || "Untitled Stream";
  const summary = getTag(event.tags, "summary");
  const imageUrl = getTag(event.tags, "image");
  const streamingUrl = getTag(event.tags, "streaming");
  const status = getEffectiveStreamStatus(event);
  const currentParticipants = getTag(event.tags, "current_participants");
  const statusConfig = getStreamStatusConfig(status);

  const isLive = status === "live" && !!streamingUrl;

  const encodedId = useMemo(() => {
    const dTag = getTag(event.tags, "d") || "";
    return nip19.naddrEncode({
      kind: event.kind,
      pubkey: event.pubkey,
      identifier: dTag,
    });
  }, [event]);

  const { onClick: openPost } = useOpenPost(`/${encodedId}`);

  return (
    <div className="mt-2 space-y-2">
      {/* Stream player / thumbnail */}
      <div className="rounded-xl overflow-hidden border border-border">
        {isLive ? (
          // Inline live player — clicks on the player are intercepted so they don't navigate away
          <div className="relative" onClick={(e) => e.stopPropagation()}>
            <LiveStreamPlayer
              src={streamingUrl}
              poster={imageUrl}
              title={title}
            />
            {/* Status + viewer overlay on top of the player */}
            <div className="absolute top-2 left-2 z-10 flex items-center gap-2 pointer-events-none">
              <Badge
                variant="outline"
                className={cn("text-[10px]", statusConfig.className)}
              >
                <div className="size-1.5 bg-white rounded-full animate-pulse mr-1" />
                {statusConfig.label}
              </Badge>
              {currentParticipants && (
                <span className="flex items-center gap-1 bg-black/60 text-white text-xs px-2 py-0.5 rounded">
                  <Users className="size-3" />
                  {currentParticipants}
                </span>
              )}
            </div>
          </div>
        ) : imageUrl ? (
          <div className="relative w-full aspect-video overflow-hidden bg-muted">
            <img
              src={imageUrl}
              alt=""
              className="w-full h-full object-cover"
              loading="lazy"
              onError={(e) => {
                (e.currentTarget.parentElement as HTMLElement).style.display =
                  "none";
              }}
            />
            <div className="absolute top-2 left-2">
              <Badge
                variant="outline"
                className={cn("text-[10px]", statusConfig.className)}
              >
                {statusConfig.label}
              </Badge>
            </div>
            {currentParticipants && (
              <div className="absolute bottom-2 right-2 flex items-center gap-1 bg-black/60 text-white text-xs px-2 py-0.5 rounded">
                <Users className="size-3" />
                {currentParticipants}
              </div>
            )}
          </div>
        ) : (
          // No image, no live stream — show a minimal placeholder with status
          <div className="flex items-center gap-3 px-3 py-2.5 bg-muted/40">
            <Radio className="size-4 text-primary shrink-0" />
            <Badge
              variant="outline"
              className={cn("text-[10px]", statusConfig.className)}
            >
              {status === "live" && (
                <div className="size-1.5 bg-white rounded-full animate-pulse mr-1" />
              )}
              {statusConfig.label}
            </Badge>
            {currentParticipants && (
              <span className="flex items-center gap-1 text-xs text-muted-foreground">
                <Users className="size-3" />
                {currentParticipants}
              </span>
            )}
          </div>
        )}
      </div>

      {/* Title + summary — clickable to open stream details */}
      <button
        type="button"
        className="flex items-start gap-2 text-left w-full group"
        onClick={(e) => {
          e.stopPropagation();
          openPost();
        }}
      >
        <Radio className="size-4 text-primary shrink-0 mt-0.5" />
        <div className="min-w-0 flex-1">
          <h3 className="font-semibold text-sm leading-snug line-clamp-2 group-hover:underline">
            {title}
          </h3>
          {summary && (
            <p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">
              {summary}
            </p>
          )}
        </div>
      </button>
    </div>
  );
}

export interface EventActionHeaderProps {
  /** Pubkey of the person performing the action. */
  pubkey: string;
  /** Lucide icon component shown to the left of the author name. */
  icon: React.ComponentType<{ className?: string }>;
  /** Optional className for the icon (defaults to text-primary). */
  iconClassName?: string;
  /** Verb phrase shown after the author name, e.g. "hid a" or "is streaming". */
  action: string;
  /** Optional noun shown after the verb, linked to a page route, e.g. "treasure" → /treasures. */
  noun?: string;
  /** Route to link the noun to, e.g. "/treasures". */
  nounRoute?: string;
}

/** Static config for deriving the action header from an event's kind and tags. */
interface KindHeaderConfig {
  icon: React.ComponentType<{ className?: string }>;
  iconClassName?: string;
  /** Static action string, or a function that computes it from the event. */
  action: string | ((event: NostrEvent) => string);
  noun?: string;
  nounRoute?: string;
}

const KIND_HEADER_MAP: Record<number, KindHeaderConfig> = {
  20: {
    icon: Camera,
    action: "shared a",
    noun: "photo",
    nounRoute: "/photos",
  },
  4: {
    icon: Mail,
    action: "sent an",
    noun: "encrypted message",
  },
  8211: {
    icon: Mail,
    action: "sent a",
    noun: "letter",
    nounRoute: "/letters",
  },
  37516: {
    icon: ChestIcon,
    action: (event) => publishedAtAction(event, { created: "hid a", updated: "updated a", fallback: "hid a" }),
    noun: "treasure",
    nounRoute: "/treasures",
  },
  7516: {
    icon: ChestIcon,
    action: "found a",
    noun: "treasure",
    nounRoute: "/treasures",
  },
  37381: {
    icon: CardsIcon,
    action: (event) => publishedAtAction(event, { created: "created a", updated: "updated a", fallback: "shared a" }),
    noun: "deck",
    nounRoute: "/decks",
  },
  36767: {
    icon: Sparkles,
    action: (event) => publishedAtAction(event, { created: "created a", updated: "updated a", fallback: "shared a" }),
    noun: "theme",
    nounRoute: "/themes",
  },
  16767: {
    icon: Sparkles,
    action: (event) => publishedAtAction(event, { created: "created a", updated: "updated their", fallback: "updated their" }),
    noun: "theme",
    nounRoute: "/themes",
  },
  30030: {
    icon: SmilePlus,
    action: (event) => publishedAtAction(event, { created: "created an", updated: "updated an", fallback: "shared an" }),
    noun: "emoji pack",
    nounRoute: "/emojis",
  },
  30009: {
    icon: Award,
    action: (event) => publishedAtAction(event, { created: "created a", updated: "updated a", fallback: "created a" }),
    noun: "badge",
    nounRoute: "/badges",
  },
  10008: {
    icon: Award,
    action: (event) => publishedAtAction(event, { created: "created their", updated: "updated their", fallback: "updated their" }),
    noun: "badges",
    nounRoute: "/badges",
  },
  30008: {
    icon: Award,
    action: (event) => publishedAtAction(event, { created: "created their", updated: "updated their", fallback: "updated their" }),
    noun: "badges",
    nounRoute: "/badges",
  },
  30311: {
    icon: Radio,
    iconClassName: undefined, // computed dynamically below
    action: (event) =>
      getEffectiveStreamStatus(event) === "live"
        ? "is streaming"
        : "streamed",
  },
  32267: {
    icon: Package,
    action: (event) => publishedAtAction(event, { created: "published a Zapstore app", updated: "updated a Zapstore app", fallback: "published a Zapstore app" }),
  },
  30063: {
    icon: Package,
    action: (event) => publishedAtAction(event, { created: "published a Zapstore release", updated: "updated a Zapstore release", fallback: "published a Zapstore release" }),
  },
  3063: {
    icon: Package,
    action: "published a Zapstore asset",
  },
  31990: {
    icon: Package,
    action: (event) => publishedAtAction(event, { created: "published an app", updated: "updated an app", fallback: "published an app" }),
  },
  30617: {
    icon: GitBranch,
    action: (event) => publishedAtAction(event, { created: "created a", updated: "updated a", fallback: "shared a" }),
    noun: "repository",
    nounRoute: "/development",
  },
  1617: {
    icon: FileText,
    action: "submitted a",
    noun: "patch",
    nounRoute: "/development",
  },
  1618: {
    icon: GitPullRequest,
    action: "opened a",
    noun: "pull request",
    nounRoute: "/development",
  },
  30817: {
    icon: FileCode,
    action: (event) => publishedAtAction(event, { created: "proposed a", updated: "updated a", fallback: "proposed a" }),
    noun: "NIP",
    nounRoute: "/development",
  },
  15128: {
    icon: Rocket,
    action: (event) => publishedAtAction(event, { created: "deployed an", updated: "redeployed an", fallback: "deployed an" }),
    noun: "nsite",
    nounRoute: "/development",
  },
  35128: {
    icon: Rocket,
    action: (event) => publishedAtAction(event, { created: "deployed an", updated: "redeployed an", fallback: "deployed an" }),
    noun: "nsite",
    nounRoute: "/development",
  },
  9735: {
    icon: Zap,
    action: "zapped",
  },
  31124: {
    icon: Egg,
    action: (event) => publishedAtAction(event, { created: "created their", updated: "updated their", fallback: "updated their" }),
    noun: "Blobbi",
    nounRoute: "/blobbi",
  },
  39089: {
    icon: PartyPopper,
    action: (event) => publishedAtAction(event, { created: "created a", updated: "updated a", fallback: "shared a" }),
    noun: "follow pack",
    nounRoute: "/packs",
  },
  30000: {
    icon: PartyPopper,
    action: (event) => publishedAtAction(event, { created: "created a", updated: "updated a", fallback: "shared a" }),
    noun: "follow set",
    nounRoute: "/packs",
  },
};

/** Generic action header: icon · [author name] [action] [linked noun] */
export function EventActionHeader({
  pubkey,
  icon: Icon,
  iconClassName,
  action,
  noun,
  nounRoute,
}: EventActionHeaderProps) {
  const author = useAuthor(pubkey);
  const name = author.data?.metadata?.name || genUserName(pubkey);
  const url = useProfileUrl(pubkey, author.data?.metadata);

  return (
    <div className="flex items-center gap-3 text-xs text-muted-foreground mb-3 min-w-0">
      <div className="w-11 shrink-0 flex justify-end">
        <Icon
          className={cn(
            "size-4 translate-y-px",
            iconClassName ?? "text-primary",
          )}
        />
      </div>
      <div className="flex items-center min-w-0">
        {author.isLoading ? (
          <Skeleton className="h-3 w-20 inline-block" />
        ) : (
          <ProfileHoverCard pubkey={pubkey} asChild>
            <Link
              to={url}
              className="font-medium hover:underline mr-1 truncate"
              onClick={(e) => e.stopPropagation()}
            >
              {author.data?.event ? (
                <EmojifiedText tags={author.data.event.tags}>
                  {name}
                </EmojifiedText>
              ) : (
                name
              )}
            </Link>
          </ProfileHoverCard>
        )}
        <span className={cn("shrink-0", author.isLoading && "ml-1")}>
          {action}
          {noun && nounRoute && (
            <>
              {" "}
              <Link
                to={nounRoute}
                className="hover:underline"
                onClick={(e) => e.stopPropagation()}
              >
                {noun}
              </Link>
            </>
          )}
          {noun && !nounRoute && <> {noun}</>}
        </span>
      </div>
    </div>
  );
}
