import React from 'react'

import { optimizeMessagingAttachmentUrl } from '../../../utils/cdnImageUrl'
import Bubble from '../_shared/Bubble'
import DismissButton from '../_shared/DismissButton'
import ImageViewer, { type ImageViewerItem } from '../_shared/ImageViewer'
import MediaStackGrid, { type MediaStackTile } from '../_shared/MediaStackGrid'
import { useSingleMediaRatio } from '../_shared/useSingleMediaRatio'
import { useViewer } from '../_shared/useViewer'
import {
  bubbleVariantForState,
  type ImageItem,
  type ImageLoadingMode,
  type MessageAttachmentBaseProps,
  type MessageAttachmentState,
} from '../types'

export interface ImageAttachmentSharedProps extends MessageAttachmentBaseProps {
  /** Single image — convenience for the most common case. */
  src?: string
  alt?: string
  /** Filename used as the viewer dialog's accessible name. */
  filename?: string
  /**
   * Stacked images. 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?: ImageItem[]
  /**
   * Native lazy-load hint forwarded to every `<img>` rendered on the
   * bubble surface (Composer thumbnail, single + stacked tiles, and
   * the `+N` overflow tile). Defaults to `'lazy'` — chat surfaces
   * usually scroll a long history, and lazy loading prevents every
   * historical image from being fetched on mount. Set `'eager'` for
   * above-the-fold hero attachments. Per-tile overrides live on
   * `ImageItem.loading`. The opened `ImageViewer` always eager-loads
   * the active image regardless of this value.
   */
  loading?: ImageLoadingMode
  /**
   * Forwarded to the Image viewer trigger. When omitted the click
   * still opens the built-in viewer — supply this for analytics, or
   * return `false` to intercept the open (e.g. switch to a route-
   * based gallery / 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
}

const tileFromItem = ({
  item,
  index,
  totalCount,
  fallbackLoading,
  imgRef,
  onLoad,
}: {
  item: ImageItem
  index: number
  totalCount: number
  fallbackLoading: ImageLoadingMode
  /** Set on the single-image tile only — see `useSingleMediaRatio`. */
  imgRef?: React.Ref<HTMLImageElement>
  onLoad?: React.ReactEventHandler<HTMLImageElement>
}): MediaStackTile => ({
  ariaLabel: `Open image ${index + 1} of ${totalCount}`,
  content: (
    // No corner radius here: `MediaStackGrid` clips the four outer
    // corners of the whole cluster and keeps every inner edge square.
    <img
      ref={imgRef}
      src={optimizeMessagingAttachmentUrl(item.src) ?? item.src}
      alt={item.alt ?? ''}
      width={item.width}
      height={item.height}
      draggable={false}
      loading={item.loading ?? fallbackLoading}
      decoding="async"
      onLoad={onLoad}
      className="absolute inset-0 size-full object-cover"
    />
  ),
})

const resolveItems = ({
  src,
  alt,
  items,
}: {
  src?: string
  alt?: string
  items?: ImageItem[]
}): ImageItem[] => {
  if (items && items.length > 0) return items
  if (src) return [{ src, alt }]
  return []
}

interface InternalImageRowProps extends ImageAttachmentSharedProps {
  state: MessageAttachmentState
  /**
   * Renders a dismiss button on each tile (Composer only). When the
   * stack has multiple tiles, the dismiss applies to the whole stack
   * — that mirrors the mobile composer where attachments are removed
   * as a single unit.
   */
  onDismiss?: () => void
}

/**
 * Project the bubble's `ImageItem`s onto the carousel-aware viewer's
 * `ImageViewerItem` 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 + download default.
 */
const buildViewerItems = (
  resolvedItems: ImageItem[],
  filename?: string
): ImageViewerItem[] =>
  resolvedItems.map((item, index) => ({
    src: item.src,
    alt: item.alt,
    filename:
      filename && resolvedItems.length === 1
        ? filename
        : filename
          ? `${filename} (${index + 1})`
          : undefined,
  }))

/**
 * Composer rendering — bare 280px-square image with a dismiss `×`
 * overlay and `rounded-md` corners. Intentionally renders without the
 * shared `Bubble` chrome (no border / no background / no padding) so
 * the in-progress attachment looks like a draft preview, not a sent
 * message. The composer surface only supports a single attachment at
 * a time — `items` and `text` are ignored here.
 */
const ImageComposerInner: React.FC<{
  src: string
  alt?: string
  filename?: string
  loading?: ImageLoadingMode
  onClick?: (index: number) => boolean | void
  onDismiss?: () => void
}> = ({ src, alt, filename, loading = 'lazy', onClick, onDismiss }) => {
  const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
    useViewer(onClick)

  return (
    <div className="relative w-fit">
      <button
        type="button"
        onClick={() => handleActivate(0)}
        aria-label="Open image"
        className="block size-[280px] cursor-zoom-in overflow-hidden rounded-md outline-none focus-visible:ring-2 focus-visible:ring-black/40"
      >
        <img
          src={optimizeMessagingAttachmentUrl(src) ?? src}
          alt={alt ?? ''}
          draggable={false}
          loading={loading}
          decoding="async"
          className="size-full object-cover"
        />
      </button>
      {onDismiss ? (
        <div className="absolute right-2 top-2 z-10">
          <DismissButton onClick={onDismiss} />
        </div>
      ) : null}

      <ImageViewer
        open={viewerOpen}
        items={buildViewerItems([{ src, alt }], filename)}
        initialIndex={viewerIndex}
        onClose={closeViewer}
      />
    </div>
  )
}

/**
 * Box reserved for a single image whose ratio is not known yet — the
 * 4:3 landscape card Figma draws for the most common case (330×250
 * card, 326×246 content, `attachment-single-media` 1972:13149). A
 * bounded reserve keeps a scrolling thread from jumping by an
 * arbitrary amount between mount and decode.
 */
const RESERVED_SINGLE_ASPECT_RATIO = 326 / 246

/**
 * Sent / Received rendering — wrapped in the shared `Bubble` chrome,
 * supports single or stacked items, and renders an optional caption
 * below the media.
 */
const ImageBubbleRow: React.FC<InternalImageRowProps> = ({
  state,
  src,
  alt,
  filename,
  items,
  text,
  groupPosition,
  loading = 'lazy',
  onClick,
}) => {
  const resolvedItems = resolveItems({ src, alt, items })
  const variant = bubbleVariantForState(state)
  const { viewerOpen, viewerIndex, handleActivate, closeViewer } =
    useViewer(onClick)
  const isSingle = resolvedItems.length === 1
  const singleItem = isSingle ? resolvedItems[0] : undefined
  const single = useSingleMediaRatio({
    src: singleItem?.src,
    suppliedRatio:
      singleItem?.width && singleItem?.height
        ? singleItem.width / singleItem.height
        : undefined,
    fallbackRatio: RESERVED_SINGLE_ASPECT_RATIO,
  })

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

  const tiles: MediaStackTile[] = resolvedItems.map((item, index) =>
    tileFromItem({
      item,
      index,
      totalCount: resolvedItems.length,
      fallbackLoading: loading,
      imgRef: isSingle ? single.imgRef : undefined,
      onLoad: isSingle ? single.onLoad : undefined,
    })
  )

  return (
    <Bubble
      variant={variant}
      text={text}
      groupPosition={groupPosition}
      // Media cards drop the 280px document width and the 8px inset
      // for a 2px frame around the media, at a uniform r20 that never
      // flattens against a caption bubble — `attachment-single-media`
      // 330×250 and `attachment-single-image` 383×290, both `pad2`
      // `cornerRadius: 20` with no stroke. The single card sizes to the
      // fitted media box, capped at `min(100%, 313/330px)`: the `100%`
      // keeps it inside the attachment bubble (Stream pins that to 300px;
      // a fixed `max-w-[330px]` overflowed it into the message-list
      // gutter), and the 313/330 ceiling holds in a wider or standalone
      // container so a long caption cannot stretch the card past the
      // design maximum. The media itself is separately capped by
      // `MediaStackGrid`'s `--mes-single-width-cap`; the stacked card is a
      // fixed 383px with `max-w-full` for the same bubble fit.
      // `bordered={false}` because a hairline on top of the 2px frame
      // would read as a third edge and push the card 2px over its
      // measured width.
      bordered={false}
      widthClassName={
        isSingle
          ? 'w-fit max-w-[min(100%,313px)] sm:max-w-[min(100%,330px)]'
          : 'w-[383px] max-w-full'
      }
      paddingClassName="p-0.5"
      radiusClassName="rounded-[20px]"
      data-testid="image-attachment"
    >
      <div className="relative">
        <MediaStackGrid
          tiles={tiles}
          onTileActivate={handleActivate}
          singleAspectRatio={isSingle ? single.ratio : undefined}
        />
      </div>

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

/**
 * Composer-only props. Single image (`src`) is required; stacked
 * `items` and `text` captions are not supported in the composer state.
 */
export interface ImageComposerProps {
  src: string
  alt?: string
  filename?: string
  /**
   * Native lazy-load hint forwarded to the composer thumbnail `<img>`.
   * Defaults to `'lazy'`. See `ImageAttachmentSharedProps.loading` for
   * the rationale.
   */
  loading?: ImageLoadingMode
  onClick?: (index: number) => boolean | void
  onDismiss?: () => void
}

export type ImageSentProps = ImageAttachmentSharedProps
export type ImageReceivedProps = ImageAttachmentSharedProps

const ImageComposer: React.FC<ImageComposerProps> = (props) => (
  <ImageComposerInner {...props} />
)
const ImageSent: React.FC<ImageSentProps> = (props) => (
  <ImageBubbleRow {...props} state="sent" />
)
const ImageReceived: React.FC<ImageReceivedProps> = (props) => (
  <ImageBubbleRow {...props} state="received" />
)

const ImageAttachment = {
  Composer: ImageComposer,
  Sent: ImageSent,
  Received: ImageReceived,
}

export default ImageAttachment
