'use client';

import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useMemo,
  useState,
} from 'react';
import { File as FileGlyph, FileText, X } from 'lucide-react';

import { Dialog, DialogContent, DialogTitle } from '@djangocfg/ui-core/components';
import { cn } from '@djangocfg/ui-core/lib';

import { formatFileSize } from '../../forms/Uploader';
import { LazyImageViewer } from '../../media/ImageViewer/lazy';
import { PastedTextDialog } from '../composer/PastedTextDialog';
import { useChatDestructiveStyles } from '../styles';
import type { ChatAttachment } from '../types';

export interface AttachmentRendererArgs {
  attachment: ChatAttachment;
  /** True when shown inside the composer's staging tray (denser layout). */
  isInComposer: boolean;
  onClick?: () => void;
  onRemove?: () => void;
}

export type AttachmentRenderer = (args: AttachmentRendererArgs) => ReactNode;

export interface AttachmentRendererMap {
  text?: AttachmentRenderer;
  image?: AttachmentRenderer;
  audio?: AttachmentRenderer;
  video?: AttachmentRenderer;
  file?: AttachmentRenderer;
  /** Fallback renderer when no per-type entry matched. */
  default?: AttachmentRenderer;
}

interface CommonProps {
  attachments: ChatAttachment[];
  maxVisible?: number;
  onClick?: (a: ChatAttachment) => void;
  onRemove?: (a: ChatAttachment) => void;
  isInComposer?: boolean;
  className?: string;
}

// ---------------------------------------------------------------------------
// Default image lightbox — image tiles open the shared `ImageViewer` in a
// dialog by default. All image attachments of the SAME attachments group feed
// the viewer's `images` array, so the lightbox has prev/next across the
// message's images and opens at the clicked index.
//
// Composition with a host `onClick`: the lightbox owns IMAGES, the host
// `onClick` owns NON-IMAGE files. A host `onClick` does NOT suppress the image
// lightbox — clicking an image opens the gallery, clicking a file fires the
// host handler (e.g. reveal-in-file-manager). See `useImageTileClick`.
// ---------------------------------------------------------------------------

interface ImageLightboxContextValue {
  /** Opens the default lightbox at the given attachment id. */
  openImage: (attachmentId: string) => void;
}

const ImageLightboxContext = createContext<ImageLightboxContextValue | null>(null);

/**
 * Provides a single shared lightbox dialog for a group of attachments and
 * renders it once. Image tiles call `openImage(id)` to open at their index.
 * Active only when `enabled` (i.e. the host did NOT wire `onClick`).
 */
function ImageLightboxProvider({
  attachments,
  enabled,
  children,
}: {
  attachments: ChatAttachment[];
  enabled: boolean;
  children: ReactNode;
}) {
  const [open, setOpen] = useState(false);
  const [index, setIndex] = useState(0);

  // Only image attachments seed the viewer — non-image files are skipped, so
  // the gallery index maps onto the image subset (not the full attachment list).
  const images = useMemo(
    () => attachments.filter((a) => a.type === 'image'),
    [attachments],
  );

  const openImage = useCallback(
    (attachmentId: string) => {
      const i = images.findIndex((a) => a.id === attachmentId);
      if (i < 0) return;
      setIndex(i);
      setOpen(true);
    },
    [images],
  );

  const ctx = useMemo<ImageLightboxContextValue>(() => ({ openImage }), [openImage]);

  if (!enabled || images.length === 0) return <>{children}</>;

  return (
    <ImageLightboxContext.Provider value={ctx}>
      {children}
      <Dialog open={open} onOpenChange={setOpen}>
        {/* Borderless, full-bleed lightbox: the viewer paints its own dark
            backdrop + idle-fading chrome, so the dialog provides only the
            blurred overlay. No card chrome, no built-in close (the viewer's
            macOS-grade close button + Esc own dismissal). */}
        <DialogContent
          fullscreen
          closeButton={false}
          className="border-0 bg-transparent p-0 shadow-none"
        >
          <DialogTitle className="sr-only">
            Image {index + 1} of {images.length}
          </DialogTitle>
          {open && (
            <LazyImageViewer
              images={images.map((a) => ({
                file: { name: a.name ?? 'image', path: a.url },
                src: a.url,
              }))}
              initialIndex={index}
              lightbox
              autoFocus
              onRequestClose={() => setOpen(false)}
            />
          )}
        </DialogContent>
      </Dialog>
    </ImageLightboxContext.Provider>
  );
}

/**
 * Resolves the click handler for an attachment tile, splitting ownership by
 * type:
 *   - IMAGE  → the default lightbox wins when its provider is mounted (so the
 *     gallery opens with prev/next across the message's images), regardless of
 *     a host `onClick`. Falls back to the host `onClick` only if no provider.
 *   - NON-IMAGE → the host `onClick` (e.g. reveal-in-file-manager).
 * This lets a host wire `onClick` for files without suppressing the image
 * lightbox (the two no longer fight over the same group-level flag).
 */
function useImageTileClick(
  attachment: ChatAttachment,
  hostOnClick?: () => void,
): (() => void) | undefined {
  const lightbox = useContext(ImageLightboxContext);
  if (attachment.type === 'image' && lightbox) {
    return () => lightbox.openImage(attachment.id);
  }
  return hostOnClick;
}

// ---------------------------------------------------------------------------
// AttachmentsGrid — flex-wrap, ideal for thumbnails / file chips.
// ---------------------------------------------------------------------------

export interface AttachmentsGridProps extends CommonProps {
  layout?: 'wrap' | 'grid';
}

export function AttachmentsGrid({
  attachments,
  maxVisible,
  onClick,
  onRemove,
  isInComposer = false,
  layout = 'wrap',
  className,
}: AttachmentsGridProps) {
  if (!attachments?.length) return null;
  const visible = maxVisible ? attachments.slice(0, maxVisible) : attachments;
  return (
    // Lightbox owns images even when the host wired `onClick` (that handler is
    // for non-image files). `enabled` only gates the no-images case inside the
    // provider, so passing `true` is safe — see `useImageTileClick`.
    <ImageLightboxProvider attachments={attachments} enabled>
      <div
        className={cn(
          layout === 'grid' ? 'grid grid-cols-3 gap-2' : 'flex flex-wrap gap-2',
          className,
        )}
      >
        {visible.map((a) => (
          <AttachmentTile
            key={a.id}
            attachment={a}
            isInComposer={isInComposer}
            onClick={onClick ? () => onClick(a) : undefined}
            onRemove={onRemove ? () => onRemove(a) : undefined}
          />
        ))}
      </div>
    </ImageLightboxProvider>
  );
}

// ---------------------------------------------------------------------------
// AttachmentsList — vertical stack, designed for rich custom renderers
// (LazyAudioPlayer, video players, document previews).
// ---------------------------------------------------------------------------

export interface AttachmentsListProps extends CommonProps {
  /** Per-type renderer overrides. Falls back to the default tile. */
  renderers?: AttachmentRendererMap;
}

export function AttachmentsList({
  attachments,
  maxVisible,
  onClick,
  onRemove,
  renderers,
  isInComposer = false,
  className,
}: AttachmentsListProps) {
  if (!attachments?.length) return null;
  const visible = maxVisible ? attachments.slice(0, maxVisible) : attachments;
  return (
    // Lightbox owns images even when the host wired `onClick` (that handler is
    // for non-image files). See `useImageTileClick` + AttachmentsGrid note.
    <ImageLightboxProvider attachments={attachments} enabled>
      <div className={cn('flex w-full flex-col gap-2', className)}>
        {visible.map((a) => {
          const renderer = renderers?.[a.type] ?? renderers?.default;
          const args: AttachmentRendererArgs = {
            attachment: a,
            isInComposer,
            onClick: onClick ? () => onClick(a) : undefined,
            onRemove: onRemove ? () => onRemove(a) : undefined,
          };
          if (renderer) {
            return (
              <div key={a.id} className="relative w-full min-w-0">
                {renderer(args)}
                {args.onRemove ? <RemoveBtn onRemove={args.onRemove} /> : null}
              </div>
            );
          }
          return <AttachmentTile key={a.id} {...args} />;
        })}
      </div>
    </ImageLightboxProvider>
  );
}

// ---------------------------------------------------------------------------
// Attachments — backwards-compatible facade. Picks the right component based
// on whether `renderers` are supplied. Existing call-sites keep working.
// ---------------------------------------------------------------------------

export interface AttachmentsProps extends CommonProps {
  layout?: 'grid' | 'row';
  renderers?: AttachmentRendererMap;
}

export function Attachments(props: AttachmentsProps) {
  const { renderers, layout, ...rest } = props;
  if (renderers) {
    return <AttachmentsList {...rest} renderers={renderers} />;
  }
  return <AttachmentsGrid {...rest} layout={layout === 'grid' ? 'grid' : 'wrap'} />;
}

// ---------------------------------------------------------------------------
// Tile + remove btn (default renderers).
// ---------------------------------------------------------------------------

function AttachmentTile({ attachment, onClick, onRemove }: AttachmentRendererArgs) {
  // Host `onClick` wins; otherwise image tiles fall back to the default
  // lightbox (when an `ImageLightboxProvider` is mounted). Called before any
  // early return to keep hook order stable. Non-image files stay interactive
  // only when the host wired `onClick`.
  const effectiveOnClick = useImageTileClick(attachment, onClick);

  // "Pasted text" chunks are self-contained — they open their own preview
  // dialog on click rather than going through the generic `onClick`.
  if (attachment.type === 'text') {
    return <TextChunkTile attachment={attachment} onRemove={onRemove} />;
  }

  // Rich-preview bag present → render the compact localpreview card
  // (thumbnail + title + label/value fields). Absent → fall through to the
  // existing name+thumbnail chip. Per-type `renderers` overrides still win
  // upstream (AttachmentsList resolves them before reaching this default).
  if (attachment.preview) {
    return (
      <AttachmentPreviewCard attachment={attachment} onClick={onClick} onRemove={onRemove} />
    );
  }

  const isImage = attachment.type === 'image';
  const isUploading = attachment.status === 'uploading';

  const inner = isImage ? (
    <img
      src={attachment.thumbnailUrl ?? attachment.url}
      alt={attachment.name ?? 'attachment'}
      // `--radius-control` (~10px) — the design-system token for value
      // chips / controls (see ui-core base.css). Keeps every chip rounded
      // consistently, a touch softer than the old `rounded-md`.
      className="h-16 w-16 rounded-[var(--radius-control)] object-cover"
      loading="lazy"
    />
  ) : (
    // Two-line file chip: name on top, formatted size on the meta line
    // (Telegram/Slack convention — "report.pdf" / "1.2 KB"). The size row
    // is omitted when the byte count is unknown so the chip stays single-line.
    <div className="flex max-w-44 items-center gap-2 rounded-[var(--radius-control)] border border-border bg-background/60 px-2 py-1.5 text-xs">
      <FileGlyph aria-hidden className="size-4 shrink-0 text-muted-foreground" />
      <span className="flex min-w-0 flex-col items-start text-left">
        <span className="w-full truncate">{attachment.name ?? 'file'}</span>
        {attachment.sizeBytes != null && attachment.sizeBytes > 0 ? (
          <span className="w-full truncate text-[11px] text-muted-foreground">
            {formatFileSize(attachment.sizeBytes)}
          </span>
        ) : null}
      </span>
    </div>
  );

  return (
    <div className="relative">
      {effectiveOnClick ? (
        // Interactive when the host wired `onClick` OR (for images) the
        // default lightbox is active — the chip reads as clickable (cursor +
        // hover lift + focus ring) and is a real keyboard-focusable button.
        <button
          type="button"
          onClick={effectiveOnClick}
          aria-label={isImage ? `Open image: ${attachment.name ?? 'image'}` : undefined}
          className="block cursor-pointer rounded-[var(--radius-control)] transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
        >
          {inner}
        </button>
      ) : (
        inner
      )}
      {isUploading ? (
        <div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-[var(--radius-control)] bg-background/70 text-[10px] font-medium">
          {attachment.progress != null ? `${Math.round(attachment.progress * 100)}%` : '…'}
        </div>
      ) : null}
      {onRemove ? <RemoveBtn onRemove={onRemove} /> : null}
    </div>
  );
}

/**
 * Compact rich-preview card driven by `attachment.preview` (the serializable
 * `AttachmentPreview` bag). Renders a thumbnail (cover / first page), a title,
 * and an ordered label/value field list (duration, dimensions, pages, EXIF…).
 * Kind-agnostic: the same layout serves audio/image/pdf/doc — the host fills
 * `preview.fields` per media kind. Clickable only when the host wired `onClick`.
 */
function AttachmentPreviewCard({
  attachment,
  onClick,
  onRemove,
}: {
  attachment: ChatAttachment;
  onClick?: () => void;
  onRemove?: () => void;
}) {
  const preview = attachment.preview!;
  const title = preview.title ?? attachment.name ?? 'attachment';
  const fields = preview.fields ?? [];

  const card = (
    <div className="flex max-w-72 items-center gap-3 rounded-[var(--radius-control)] border border-border bg-background/60 px-2.5 py-2 text-left">
      {preview.thumbnail ? (
        <img
          src={preview.thumbnail}
          alt={title}
          className="h-12 w-12 shrink-0 rounded-md object-cover"
          loading="lazy"
        />
      ) : (
        <span className="grid size-12 shrink-0 place-items-center rounded-md bg-primary/15 text-primary">
          <FileGlyph aria-hidden className="size-5" />
        </span>
      )}
      <span className="flex min-w-0 flex-col gap-0.5">
        <span className="truncate text-xs font-medium">{title}</span>
        {fields.slice(0, 3).map((f, i) => (
          <span
            key={`${f.label}-${i}`}
            className="truncate text-[11px] text-muted-foreground"
          >
            <span className="font-medium">{f.label}:</span> {f.value}
          </span>
        ))}
      </span>
    </div>
  );

  return (
    <div className="relative">
      {onClick ? (
        <button
          type="button"
          onClick={onClick}
          className="block cursor-pointer rounded-[var(--radius-control)] transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
        >
          {card}
        </button>
      ) : (
        card
      )}
      {onRemove ? <RemoveBtn onRemove={onRemove} /> : null}
    </div>
  );
}

/**
 * Tile for a `type:'text'` chunk — a two-line "Pasted text" chip in the
 * same surface style as file chips (title + "Pasted text" sublabel).
 * Clicking it opens a read-only `PastedTextDialog` preview of the full
 * payload. Mirrors ChatGPT's pasted-text card + click-to-expand.
 */
function TextChunkTile({
  attachment,
  onRemove,
}: {
  attachment: ChatAttachment;
  onRemove?: () => void;
}) {
  const [open, setOpen] = useState(false);
  const title = attachment.name ?? 'Pasted text';
  return (
    <div className="relative">
      <button
        type="button"
        onClick={() => setOpen(true)}
        aria-label={`Preview ${title}`}
        className="block cursor-pointer rounded-[var(--radius-control)] text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
      >
        <div className="flex max-w-56 items-center gap-2 rounded-[var(--radius-control)] border border-border bg-background/60 px-2 py-1.5 transition-colors hover:bg-background">
          {/* Inner glyph plate keeps the tighter `rounded-md` — a nested
              surface should round a notch less than its container
              (concentric radii) so the corners read cleanly. */}
          <span className="grid size-7 shrink-0 place-items-center rounded-md bg-primary/15 text-primary">
            <FileText aria-hidden className="size-4" />
          </span>
          <span className="flex min-w-0 flex-col">
            <span className="truncate text-xs font-medium">{title}</span>
            <span className="truncate text-[11px] text-muted-foreground">Pasted text</span>
          </span>
        </div>
      </button>
      {onRemove ? <RemoveBtn onRemove={onRemove} /> : null}
      <PastedTextDialog
        open={open}
        onOpenChange={setOpen}
        text={attachment.text ?? ''}
        title={title}
      />
    </div>
  );
}

function RemoveBtn({ onRemove }: { onRemove: () => void }) {
  const styles = useChatDestructiveStyles();
  return (
    <button
      type="button"
      aria-label="Remove attachment"
      onClick={onRemove}
      className={cn(
        'absolute -right-1.5 -top-1.5 grid h-4 w-4 cursor-pointer place-items-center rounded-full border border-border bg-background text-muted-foreground',
        styles.hoverStrong,
      )}
    >
      <X aria-hidden className="size-2.5" />
    </button>
  );
}
