import { PlayIcon, VideoCameraIcon } from '@phosphor-icons/react'
import React from 'react'

import Bubble from '../_shared/Bubble'
import DismissButton from '../_shared/DismissButton'
import MediaStackGrid, {
  MEDIA_STACK_MAX_VISIBLE,
  type MediaStackTile,
} from '../_shared/MediaStackGrid'
import { useSingleMediaRatio } from '../_shared/useSingleMediaRatio'
import { useViewer } from '../_shared/useViewer'
import VideoViewer, { type VideoViewerItem } from '../_shared/VideoViewer'
import {
  bubbleVariantForState,
  type MediaPreloadMode,
  type MessageAttachmentBaseProps,
  type MessageAttachmentState,
  type VideoItem,
} from '../types'

export interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
  /** Single video — convenience for the most common case. */
  src?: string
  /** Poster image — preview shown before playback starts. */
  poster?: string
  /**
   * Width ÷ height of the source clip. See `VideoItem.naturalAspectRatio` —
   * this is the single-`src` convenience form of the same thing, and is what
   * lets a posterless portrait clip size itself correctly (MES-1353).
   */
  naturalAspectRatio?: number
  /** MIME type hint — typed onto the inline `<source>` element. */
  mimeType?: string
  /** Filename used as the viewer dialog's accessible name. */
  filename?: string
  /**
   * Stacked videos. Takes precedence over `src` when set. Renders a
   * 1 / 2 / 3 / 4-tile grid (5+ collapse into a `+N` overflow tile).
   * Sent + Received only — the composer surface intentionally accepts
   * a single attachment at a time.
   */
  items?: VideoItem[]
  /**
   * `<video preload>` hint forwarded into the viewer. Defaults to
   * `'none'` — the poster `<img>` carries the visual weight on the
   * bubble surface, and we shouldn't fetch any video bytes until the
   * user actually opens the viewer. Per-item overrides live on
   * `VideoItem.preload`. The opened `VideoViewer` always preloads
   * metadata for the active item (so duration / first-frame appear
   * immediately) regardless of this value.
   */
  preload?: MediaPreloadMode
  /**
   * Forwarded to the viewer trigger. When omitted the click still
   * opens the built-in `VideoViewer` — supply this for analytics, or
   * return `false` to intercept the open (e.g. switch to a route-
   * based player / confirmation modal). Any other return value
   * (including `void`/`undefined`) lets the built-in viewer open.
   * For stacked attachments the `index` argument identifies the tile.
   */
  onClick?: (index: number) => boolean | void
}

/**
 * A single-video card is sized by its own clip rather than force-cropped
 * square: Figma draws the same component at 16:9 (`2032:16189`) and 4:3
 * (`2032:22660`).
 *
 * The ratio is taken from `VideoItem.naturalAspectRatio` when the sender
 * recorded it, and only otherwise measured off the decoded poster. Measuring
 * the poster alone was not enough: an uploaded clip may have no poster, and a
 * portrait video then sat in whatever `FALLBACK_ASPECT` guessed. Reading the
 * dimensions Stream already stores costs nothing at render time and needs no
 * `loadedmetadata` probe, which would fetch video bytes for every card in the
 * thread against the deliberate `preload: 'none'`.
 *
 * `FALLBACK_ASPECT` is **square** on purpose. It covers only the case where we
 * know nothing — no recorded dimensions, no decoded poster — and a square box
 * is what this card rendered before any of this existed, so an unknown clip
 * can never look worse than it used to. Guessing 16:9 here put portrait clips
 * in a letterbox, which is more wrong than a square (MES-1353).
 */
const FALLBACK_ASPECT = 1
/**
 * Tallest permitted card — Instagram's portrait limit.
 *
 * A 9:16 clip (0.5625) clamps to this, so it renders 4:5 and the poster is
 * cropped rather than shown whole. Figma specifies only 16:9 and 4:3, so true
 * portrait was never drawn; relaxing this to `9 / 16` is a one-line change if
 * design wants a full-height portrait card.
 */
const MIN_ASPECT = 4 / 5
/** Widest permitted card. */
const MAX_ASPECT = 16 / 9

const PlayBadge: React.FC = () => (
  <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
    <span className="flex size-12 items-center justify-center rounded-full bg-black/25 text-white backdrop-blur-[20px]">
      <PlayIcon className="size-6" weight="fill" aria-hidden />
    </span>
  </div>
)

// The tile shell owns the cluster's geometry: `MediaStackGrid` gives
// each tile the 18px radius on the cluster's OUTER corners only and
// clips with `overflow-hidden`, so a poster must carry no radius of
// its own. A `rounded-md` here would round all four corners of every
// tile at 6px, cutting dark wedges out of the `bg-[#0d0d0d]` backing
// at each interior grid junction.
const PosterTile: React.FC<{
  item: VideoItem
  index: number
  /**
   * Suppressed on the grid's `+N` overflow tile — Figma gives that tile
   * the count scrim as its only child (`2032:16414`), and a play badge
   * stacked underneath just muddies the number.
   */
  showPlayBadge?: boolean
  /**
   * Set on the single-video tile only — see `useSingleMediaRatio`. Both
   * are needed: the ref catches a poster the browser already had in
   * cache, which never fires `load`.
   */
  imgRef?: React.Ref<HTMLImageElement>
  onLoad?: React.ReactEventHandler<HTMLImageElement>
}> = ({ item, index, showPlayBadge = true, imgRef, onLoad }) => {
  return (
    <div className="absolute inset-0 size-full bg-[#0d0d0d]">
      {item.poster ? (
        <img
          ref={imgRef}
          src={item.poster}
          alt={`Video ${index + 1} thumbnail`}
          draggable={false}
          loading="lazy"
          decoding="async"
          onLoad={onLoad}
          className="absolute inset-0 size-full object-cover"
        />
      ) : (
        <div className="absolute inset-0 flex items-center justify-center">
          <VideoCameraIcon
            className="size-16 text-white/30"
            weight="regular"
            aria-hidden
          />
        </div>
      )}
      {showPlayBadge ? <PlayBadge /> : null}
    </div>
  )
}

const tileFromItem = ({
  item,
  index,
  totalCount,
  isOverflowTile,
  imgRef,
  onLoad,
}: {
  item: VideoItem
  index: number
  totalCount: number
  isOverflowTile: boolean
  imgRef?: React.Ref<HTMLImageElement>
  onLoad?: React.ReactEventHandler<HTMLImageElement>
}): MediaStackTile => ({
  ariaLabel: `Play video ${index + 1} of ${totalCount}`,
  // Activating a video tile starts playback rather than magnifying a
  // still, so the grid's default zoom cursor would be a lie.
  cursorClassName: 'cursor-pointer',
  content: (
    <PosterTile
      item={item}
      index={index}
      showPlayBadge={!isOverflowTile}
      imgRef={imgRef}
      onLoad={onLoad}
    />
  ),
})

const resolveItems = ({
  src,
  poster,
  mimeType,
  preload,
  naturalAspectRatio,
  items,
}: {
  src?: string
  poster?: string
  mimeType?: string
  preload?: MediaPreloadMode
  naturalAspectRatio?: number
  items?: VideoItem[]
}): VideoItem[] => {
  if (items && items.length > 0) {
    return preload
      ? items.map((it) => ({ ...it, preload: it.preload ?? preload }))
      : items
  }
  // `naturalAspectRatio` has to be carried onto the item here, not read off
  // the props later: everything downstream sizes itself from `VideoItem`, so a
  // ratio left behind on the single-`src` shape would be silently dropped and
  // the card would fall back to measuring a poster it may not have.
  if (src) return [{ src, poster, mimeType, preload, naturalAspectRatio }]
  return []
}

interface InternalVideoRowProps extends VideoAttachmentSharedProps {
  state: MessageAttachmentState
}

/**
 * Project the bubble's `VideoItem`s onto the carousel-aware viewer's
 * `VideoViewerItem` shape. When a shared outer `filename` is supplied
 * for a stacked bubble, suffix `(N)` per sibling so each carousel page
 * still gets a distinct accessible name.
 */
const buildViewerItems = (
  resolvedItems: VideoItem[],
  filename?: string
): VideoViewerItem[] =>
  resolvedItems.map((item, index) => ({
    src: item.src,
    poster: item.poster,
    mimeType: item.mimeType,
    preload: item.preload,
    filename:
      filename && resolvedItems.length === 1
        ? filename
        : filename
          ? `${filename} (${index + 1})`
          : undefined,
  }))

/**
 * Composer rendering — bare 280px-square poster with a play badge,
 * a dismiss `×` overlay, and `rounded-md` corners. Renders without the
 * shared `Bubble` chrome (no border / no background / no padding) so
 * the in-progress attachment looks like a draft preview rather than a
 * sent message. Single attachment only — `items` and `text` are not
 * supported on the composer surface.
 */
const VideoComposerInner: React.FC<{
  src: string
  poster?: string
  mimeType?: string
  filename?: string
  preload?: MediaPreloadMode
  onClick?: (index: number) => boolean | void
  onDismiss?: () => void
}> = ({ src, poster, mimeType, filename, preload, onClick, onDismiss }) => {
  const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
    useViewer(onClick)

  return (
    <div className="relative w-fit">
      <button
        type="button"
        onClick={() => handleActivate(0)}
        aria-label="Play video"
        className="relative block size-[280px] cursor-pointer overflow-hidden rounded-md outline-none focus-visible:ring-2 focus-visible:ring-black/40"
      >
        <PosterTile item={{ src, poster, mimeType }} index={0} />
      </button>
      {onDismiss ? (
        <div className="absolute right-2 top-2 z-10">
          <DismissButton onClick={onDismiss} />
        </div>
      ) : null}

      <VideoViewer
        open={viewerOpen}
        items={buildViewerItems([{ src, poster, mimeType, preload }], filename)}
        initialIndex={viewerIndex}
        onClose={closeViewer}
      />
    </div>
  )
}

/**
 * Sent / Received rendering — wrapped in the shared `Bubble` chrome,
 * supports single or stacked items, and renders an optional caption
 * below the media.
 */
const VideoBubbleRow: React.FC<InternalVideoRowProps> = ({
  state,
  src,
  poster,
  mimeType,
  filename,
  items,
  text,
  groupPosition,
  preload,
  naturalAspectRatio,
  onClick,
}) => {
  const resolvedItems = resolveItems({
    src,
    poster,
    mimeType,
    preload,
    naturalAspectRatio,
    items,
  })
  const variant = bubbleVariantForState(state)
  const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
    useViewer(onClick)
  const isSingle = resolvedItems.length === 1
  const singleItem = isSingle ? resolvedItems[0] : undefined
  // Recorded dimensions first — known on first paint and available even
  // with no poster — then the decoded poster, then square. The hook
  // measures from a ref as well as `onLoad`, so a poster the browser
  // already had in cache still resolves; `onLoad` alone left the card
  // stuck on the fallback.
  const single = useSingleMediaRatio({
    src: singleItem?.poster,
    suppliedRatio: singleItem?.naturalAspectRatio,
    fallbackRatio: FALLBACK_ASPECT,
  })

  if (resolvedItems.length === 0) {
    return null
  }

  const overflowTileIndex =
    resolvedItems.length > MEDIA_STACK_MAX_VISIBLE
      ? MEDIA_STACK_MAX_VISIBLE - 1
      : -1

  const tiles: MediaStackTile[] = resolvedItems.map((item, index) =>
    tileFromItem({
      item,
      index,
      totalCount: resolvedItems.length,
      isOverflowTile: index === overflowTileIndex,
      // Only the single card is aspect-driven — grid tiles are cropped
      // to a fixed 2×2 geometry, so measuring them is noise.
      imgRef: isSingle ? single.imgRef : undefined,
      onLoad: isSingle ? single.onLoad : undefined,
    })
  )

  // Clamped so a 9:16 clip can't render a 587px-tall card and a panorama
  // can't collapse into a strip. Only consumed by the grid's 1-tile
  // layout; ignored for 2+.
  const singleAspectRatio = Math.min(
    Math.max(single.ratio, MIN_ASPECT),
    MAX_ASPECT
  )

  return (
    <Bubble
      variant={variant}
      text={text}
      groupPosition={groupPosition}
      // The card carries no stroke in Figma, and the default hairline
      // would eat 2px of the inset: 330 − 2×1 − 2×2 = 324, not the 326
      // the inner media measures (`2032:16189`).
      bordered={false}
      // The card hugs its media plus the 2px the design leaves around
      // it, so the 20px outer radius resolves to 18px inside. Figma's
      // single card is 330px (`1972:13149`) and its grid card 383px
      // (`1973:13668`), but `MediaStackGrid` caps a single tile at
      // 326px web / 309px mobile, so the single card tracks that fitted
      // box via `w-fit` + a per-breakpoint maximum instead of pinning
      // 330px and leaving a gutter below `sm`. Same treatment the image
      // media card lands on.
      widthClassName={
        isSingle
          ? 'w-fit max-w-[313px] sm:max-w-[330px]'
          : 'w-[383px] max-w-full'
      }
      paddingClassName="p-0.5"
      // Figma draws the media card with a uniform 20px radius. Only
      // standalone bubbles get it: inside a same-author run the 18/4px
      // corner table still has to flatten the facing corners so
      // adjacent media clusters read as one surface.
      radiusClassName={
        groupPosition && groupPosition !== 'single'
          ? undefined
          : 'rounded-[20px]'
      }
      data-testid="video-attachment"
    >
      <div className="relative">
        <MediaStackGrid
          tiles={tiles}
          onTileActivate={handleActivate}
          singleAspectRatio={isSingle ? singleAspectRatio : undefined}
        />
      </div>

      <VideoViewer
        open={viewerOpen}
        items={buildViewerItems(resolvedItems, filename)}
        initialIndex={viewerIndex}
        onClose={closeViewer}
      />
    </Bubble>
  )
}

/**
 * Composer-only props. Single video (`src`) is required; stacked
 * `items` and `text` captions are not supported in the composer state.
 */
export interface VideoComposerProps {
  src: string
  poster?: string
  mimeType?: string
  filename?: string
  /**
   * `<video preload>` hint forwarded into the viewer. Defaults to
   * `'none'`. See `VideoAttachmentSharedProps.preload`.
   */
  preload?: MediaPreloadMode
  onClick?: (index: number) => boolean | void
  onDismiss?: () => void
}

export type VideoSentProps = VideoAttachmentSharedProps
export type VideoReceivedProps = VideoAttachmentSharedProps

const VideoComposer: React.FC<VideoComposerProps> = (props) => (
  <VideoComposerInner {...props} />
)
const VideoSent: React.FC<VideoSentProps> = (props) => (
  <VideoBubbleRow {...props} state="sent" />
)
const VideoReceived: React.FC<VideoReceivedProps> = (props) => (
  <VideoBubbleRow {...props} state="received" />
)

const VideoAttachment = {
  Composer: VideoComposer,
  Sent: VideoSent,
  Received: VideoReceived,
}

export default VideoAttachment
