import React, { ReactNode, useEffect, useRef, useState } from 'react';
import {
  Box,
  Chip,
  Icon,
  IconButton,
  Icons,
  Input,
  Switch,
  Text,
  color,
  config,
  toRem,
} from 'folds';
import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { TUploadContent, tryDeleteMxcContent } from '../../utils/matrix';
import { bytesToSize, getFileTypeIcon } from '../../utils/common';
import {
  roomUploadAtomFamily,
  TUploadItem,
  TUploadMetadata,
} from '../../state/room/roomInputDrafts';
import { useObjectURL } from '../../hooks/useObjectURL';
import { useMediaConfig } from '../../hooks/useMediaConfig';
import { compressImage, formatFileSize, isCompressible } from '../../utils/imageCompression';

type PreviewImageProps = {
  fileItem: TUploadItem;
};
function PreviewImage({ fileItem }: PreviewImageProps) {
  const { originalFile, metadata } = fileItem;
  const fileUrl = useObjectURL(originalFile);

  return (
    <img
      style={{
        objectFit: 'contain',
        width: '100%',
        height: toRem(152),
        filter: metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
      }}
      alt={(originalFile as File).name}
      src={fileUrl}
    />
  );
}

type PreviewVideoProps = {
  fileItem: TUploadItem;
};
function PreviewVideo({ fileItem }: PreviewVideoProps) {
  const { originalFile, metadata } = fileItem;
  const fileUrl = useObjectURL(originalFile);

  return (
    <video
      aria-label="Video attachment preview"
      style={{
        objectFit: 'contain',
        width: '100%',
        height: toRem(152),
        filter: metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
      }}
      src={fileUrl}
    />
  );
}

type MediaPreviewProps = {
  fileItem: TUploadItem;
  onSpoiler: (marked: boolean) => void;
  children: ReactNode;
};
function MediaPreview({ fileItem, onSpoiler, children }: MediaPreviewProps) {
  const { originalFile, metadata } = fileItem;
  const fileUrl = useObjectURL(originalFile);

  return fileUrl ? (
    <Box
      style={{
        borderRadius: config.radii.R300,
        overflow: 'hidden',
        backgroundColor: 'black',
        position: 'relative',
      }}
    >
      {children}
      <Box
        justifyContent="End"
        style={{
          position: 'absolute',
          bottom: config.space.S100,
          left: config.space.S100,
          right: config.space.S100,
        }}
      >
        <Chip
          variant={metadata.markedAsSpoiler ? 'Warning' : 'Secondary'}
          fill="Soft"
          radii="Pill"
          aria-pressed={metadata.markedAsSpoiler}
          before={<Icon src={Icons.EyeBlind} size="50" />}
          onClick={() => onSpoiler(!metadata.markedAsSpoiler)}
        >
          <Text size="B300">Spoiler</Text>
        </Chip>
      </Box>
    </Box>
  ) : null;
}

type CompressionCheckboxProps = {
  fileItem: TUploadItem;
  metadata: TUploadMetadata;
  setMetadata: (fileItem: TUploadItem, metadata: TUploadMetadata) => void;
};
function CompressionCheckbox({ fileItem, metadata, setMetadata }: CompressionCheckboxProps) {
  const originalFile = fileItem.originalFile as File | Blob;
  const compressible = isCompressible(originalFile);

  // Local compression result — avoids the stale-closure bug where the async
  // .then() would call setMetadata with an outdated fileItem reference.
  const [localResult, setLocalResult] = useState<
    import('../../utils/imageCompression').CompressionResult | null | undefined
  >(undefined);
  const [compressing, setCompressing] = useState(false);

  // Always-fresh refs so the async callback never captures stale values.
  const fileItemRef = useRef(fileItem);
  const metadataRef = useRef(metadata);
  fileItemRef.current = fileItem;
  metadataRef.current = metadata;

  const checked = !!metadata.compressImage;

  // Stable file identity key — the File object reference changes when metadata is
  // updated (fileItem is replaced), but the underlying file never changes.
  const fileKey = `${(originalFile as File).name ?? ''}-${originalFile.size}`;

  // Run compression whenever the toggle is turned on; cancel on uncheck / unmount.
  useEffect(() => {
    if (!checked || !compressible) return undefined;
    let cancelled = false;
    setCompressing(true);
    setLocalResult(undefined);
    compressImage(originalFile).then((result) => {
      if (cancelled) return;
      setCompressing(false);
      setLocalResult(result);
      // Persist to metadata using fresh refs so the REPLACE action finds the
      // correct fileItem even after a previous setMetadata call has swapped it.
      setMetadata(fileItemRef.current, {
        ...metadataRef.current,
        compressImage: true,
        compressionResult: result,
      });
    });
    return () => {
      cancelled = true;
    };
    // fileKey is a stable primitive derived from the file; setMetadata is a stable useCallback.
    // Using these instead of the object refs avoids re-running on every metadata update.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [checked, compressible, fileKey, setMetadata]);

  if (!compressible) return null;

  const handleToggle = (value: boolean) => {
    if (!value) setLocalResult(undefined);
    setMetadata(fileItem, { ...metadata, compressImage: value, compressionResult: undefined });
  };

  const displayResult = localResult ?? metadata.compressionResult;
  const savingPct =
    displayResult && displayResult.originalSize > 0
      ? Math.round(
          ((displayResult.originalSize - displayResult.compressedSize) /
            displayResult.originalSize) *
            100,
        )
      : null;

  const originalSize = formatFileSize(originalFile.size);

  return (
    <Box
      direction="Column"
      gap="200"
      style={{
        marginTop: config.space.S100,
        padding: `${config.space.S200} ${config.space.S300}`,
        background: color.SurfaceVariant.Container,
        borderRadius: config.radii.R300,
        border: `1px solid ${color.SurfaceVariant.ContainerLine}`,
      }}
    >
      {/* Toggle row */}
      <Box alignItems="Center" gap="200" justifyContent="SpaceBetween">
        <Text size="T200" style={{ color: color.SurfaceVariant.OnContainer }}>
          Compress image before uploading
        </Text>
        <Box alignItems="Center" gap="200">
          {compressing && (
            <Text size="T200" style={{ opacity: 0.6 }}>
              compressing…
            </Text>
          )}
          <Switch variant="Primary" value={checked} onChange={handleToggle} />
        </Box>
      </Box>

      {/* Size comparison — original always visible; compressed appears when ready */}
      <Box gap="200" alignItems="Center" wrap="Wrap">
        <Box
          gap="100"
          alignItems="Center"
          style={{
            padding: `2px ${config.space.S200}`,
            borderRadius: config.radii.R300,
            background: color.Surface.Container,
            border: `1px solid ${color.Surface.ContainerLine}`,
          }}
        >
          <Text size="T200" style={{ opacity: 0.6 }}>
            Original
          </Text>
          <Text size="T200" style={{ fontWeight: 600 }}>
            {originalSize}
          </Text>
        </Box>

        {checked && !compressing && displayResult !== undefined && (
          <>
            <Text size="T200" style={{ opacity: 0.5 }}>
              →
            </Text>
            <Box
              gap="100"
              alignItems="Center"
              style={{
                padding: `2px ${config.space.S200}`,
                borderRadius: config.radii.R300,
                background:
                  savingPct !== null && savingPct > 5
                    ? (color.Success?.Container ?? color.Primary.Container)
                    : color.Surface.Container,
                border: `1px solid ${
                  savingPct !== null && savingPct > 5
                    ? (color.Success?.ContainerLine ?? color.Primary.ContainerLine)
                    : color.Surface.ContainerLine
                }`,
              }}
            >
              <Text size="T200" style={{ opacity: 0.6 }}>
                Compressed
              </Text>
              {displayResult ? (
                <Text size="T200" style={{ fontWeight: 600 }}>
                  {formatFileSize(displayResult.compressedSize)}
                  {savingPct !== null && savingPct > 0 && (
                    <span style={{ opacity: 0.7, marginLeft: 4 }}>({savingPct}% smaller)</span>
                  )}
                </Text>
              ) : (
                <Text size="T200" style={{ opacity: 0.6 }}>
                  unavailable
                </Text>
              )}
            </Box>
          </>
        )}
      </Box>
    </Box>
  );
}

type UploadCardRendererProps = {
  isEncrypted?: boolean;
  fileItem: TUploadItem;
  setMetadata: (fileItem: TUploadItem, metadata: TUploadMetadata) => void;
  onRemove: (file: TUploadContent) => void;
  onComplete?: (upload: UploadSuccess) => void;
};
export function UploadCardRenderer({
  isEncrypted,
  fileItem,
  setMetadata,
  onRemove,
  onComplete,
}: UploadCardRendererProps) {
  const mx = useMatrixClient();
  const mediaConfig = useMediaConfig();
  const allowSize = mediaConfig['m.upload.size'] || Infinity;

  const uploadAtom = roomUploadAtomFamily(fileItem.file);
  const { metadata } = fileItem;
  const { upload, startUpload, cancelUpload } = useBindUploadAtom(mx, uploadAtom, isEncrypted);
  const { file } = upload;
  const fileSizeExceeded = file.size >= allowSize;

  if (upload.status === UploadStatus.Idle && !fileSizeExceeded) {
    startUpload();
  }

  const handleSpoiler = (marked: boolean) => {
    setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked });
  };

  const removeUpload = () => {
    if (upload.status === UploadStatus.Success) {
      // Upload already completed — delete the orphaned MXC from the server.
      tryDeleteMxcContent(mx, upload.mxc);
    }
    cancelUpload();
    onRemove(file);
  };

  useEffect(() => {
    if (upload.status === UploadStatus.Success) {
      onComplete?.(upload);
    }
  }, [upload, onComplete]);

  return (
    <UploadCard
      radii="300"
      before={<Icon src={getFileTypeIcon(Icons, file.type)} />}
      after={
        <>
          {upload.status === UploadStatus.Error && (
            <Chip
              as="button"
              onClick={startUpload}
              aria-label="Retry Upload"
              variant="Critical"
              radii="Pill"
              outlined
            >
              <Text size="B300">Retry</Text>
            </Chip>
          )}
          <IconButton
            onClick={removeUpload}
            aria-label="Cancel Upload"
            variant="SurfaceVariant"
            radii="Pill"
            size="300"
          >
            <Icon src={Icons.Cross} size="200" />
          </IconButton>
        </>
      }
      bottom={
        <>
          {fileItem.originalFile.type.startsWith('image') && (
            <MediaPreview fileItem={fileItem} onSpoiler={handleSpoiler}>
              <PreviewImage fileItem={fileItem} />
            </MediaPreview>
          )}
          {fileItem.originalFile.type.startsWith('video') && (
            <MediaPreview fileItem={fileItem} onSpoiler={handleSpoiler}>
              <PreviewVideo fileItem={fileItem} />
            </MediaPreview>
          )}
          {(fileItem.originalFile.type.startsWith('image') ||
            fileItem.originalFile.type.startsWith('video')) && (
            <Input
              type="text"
              placeholder="Add a caption… (optional)"
              value={metadata.caption ?? ''}
              onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                setMetadata(fileItem, { ...metadata, caption: e.target.value })
              }
              data-caption-input
              variant="Secondary"
              size="300"
              radii="300"
              style={{ marginTop: config.space.S200, width: '100%' }}
            />
          )}
          <CompressionCheckbox fileItem={fileItem} metadata={metadata} setMetadata={setMetadata} />
          {upload.status === UploadStatus.Idle && !fileSizeExceeded && (
            <UploadCardProgress sentBytes={0} totalBytes={file.size} />
          )}
          {upload.status === UploadStatus.Loading && (
            <UploadCardProgress sentBytes={upload.progress.loaded} totalBytes={file.size} />
          )}
          {upload.status === UploadStatus.Error && (
            <UploadCardError>
              <Text size="T200">{upload.error.message}</Text>
            </UploadCardError>
          )}
          {upload.status === UploadStatus.Idle && fileSizeExceeded && (
            <UploadCardError>
              <Text size="T200">
                The file size exceeds the limit. Maximum allowed size is{' '}
                <b>{bytesToSize(allowSize)}</b>, but the uploaded file is{' '}
                <b>{bytesToSize(file.size)}</b>.
              </Text>
            </UploadCardError>
          )}
        </>
      }
    >
      <Text size="H6" truncate>
        {(file as File).name}
      </Text>
      {upload.status === UploadStatus.Success && (
        <Icon style={{ color: color.Success.Main }} src={Icons.Check} size="100" />
      )}
    </UploadCard>
  );
}
