import React, { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Button, config, Icon, Icons, Text, color, toRem } from 'folds';
import { IContent } from 'matrix-js-sdk';
import { JUMBO_EMOJI_REG, URL_REG } from '../../utils/regex';
import { trimReplyFromBody } from '../../utils/room';
import { MessageTextBody } from './layout';
import {
  MessageBadEncryptedContent,
  MessageBrokenContent,
  MessageDeletedContent,
  MessageEditedContent,
  MessageTranslatedContent,
  MessageUnsupportedContent,
  MessageVerificationRequestContent,
} from './content';
import { useMessageTranslation } from '../../hooks/useMessageTranslation';
import { languageName } from '../../utils/translation/langUtils';
import {
  IAudioContent,
  IAudioInfo,
  IEncryptedFile,
  IFileContent,
  IFileInfo,
  IImageContent,
  IImageInfo,
  IThumbnailContent,
  IVideoContent,
  IVideoInfo,
  MATRIX_SPOILER_PROPERTY_NAME,
  MATRIX_SPOILER_REASON_PROPERTY_NAME,
} from '../../../types/matrix/common';
import { FALLBACK_MIMETYPE, getBlobSafeMimeType } from '../../utils/mimeTypes';
import { parseGeoUri, scaleYDimension } from '../../utils/common';
import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment';
import { FileHeader, FileDownloadButton } from './FileHeader';

const COLLAPSE_MAX_HEIGHT = 320; // px ≈ 20 lines

type CollapsibleBodyProps = {
  eventId?: string;
  children: ReactNode;
};
function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
  const bodyRef = useRef<HTMLDivElement>(null);
  const [needsCollapse, setNeedsCollapse] = useState(false);
  const [collapsed, setCollapsed] = useState(true);

  // Reset collapsed state when the event changes (new message)
  useEffect(() => {
    setCollapsed(true);
    setNeedsCollapse(false);
  }, [eventId]);

  useEffect(() => {
    const el = bodyRef.current;
    if (!el) return undefined;
    const observer = new ResizeObserver(() => {
      setNeedsCollapse(el.scrollHeight > COLLAPSE_MAX_HEIGHT);
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, []);

  const prefersReducedMotion =
    typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  return (
    <div>
      <div
        ref={bodyRef}
        style={{
          position: 'relative',
          ...(needsCollapse && collapsed
            ? {
                maxHeight: `${COLLAPSE_MAX_HEIGHT}px`,
                overflow: 'hidden',
                transition: prefersReducedMotion ? undefined : 'max-height 0.2s ease',
              }
            : {
                transition: prefersReducedMotion ? undefined : 'max-height 0.2s ease',
              }),
        }}
      >
        {children}
        {needsCollapse && collapsed && (
          <div
            style={{
              position: 'absolute',
              bottom: 0,
              left: 0,
              right: 0,
              height: '3rem',
              background: `linear-gradient(transparent, ${color.Surface.Container})`,
              pointerEvents: 'none',
            }}
          />
        )}
      </div>
      {needsCollapse && (
        <button
          type="button"
          onClick={() => setCollapsed((c) => !c)}
          style={{
            cursor: 'pointer',
            background: 'none',
            border: 'none',
            padding: 0,
            marginTop: config.space.S100,
          }}
        >
          <Text as="span" size="T200" style={{ color: color.Primary.Main }}>
            {collapsed ? 'Read more ↓' : 'Show less ↑'}
          </Text>
        </button>
      )}
    </div>
  );
}

export function MBadEncrypted() {
  return (
    <Text>
      <MessageBadEncryptedContent />
    </Text>
  );
}

type RedactedContentProps = {
  reason?: string;
};
export function RedactedContent({ reason }: RedactedContentProps) {
  return (
    <Text>
      <MessageDeletedContent reason={reason} />
    </Text>
  );
}

export function UnsupportedContent() {
  return (
    <Text>
      <MessageUnsupportedContent />
    </Text>
  );
}

export function VerificationRequestContent() {
  return (
    <Text>
      <MessageVerificationRequestContent />
    </Text>
  );
}

export function BrokenContent() {
  return (
    <Text>
      <MessageBrokenContent />
    </Text>
  );
}

type RenderBodyProps = {
  body: string;
  customBody?: string;
};

// Shared body renderer for m.text / m.emote / m.notice. Handles the on-device
// translation swap: when translation is active and done, the translated text is
// rendered through the plain-text path (linkify + emoji, no stored HTML) inside
// a dir="auto" span for RTL, with a "Translated from … · Show original" chip.
type TranslatableBodyProps = {
  variant: 'text' | 'emote' | 'notice';
  displayName?: string;
  eventId: string;
  trimmedBody: string;
  customBody?: string;
  renderBody: (props: RenderBodyProps) => ReactNode;
  edited?: boolean;
  onEditHistoryClick?: () => void;
  style?: CSSProperties;
};
function TranslatableBody({
  variant,
  displayName,
  eventId,
  trimmedBody,
  customBody,
  renderBody,
  edited,
  onEditHistoryClick,
  style,
}: TranslatableBodyProps) {
  const translation = useMessageTranslation(eventId, trimmedBody);
  const showTranslated =
    translation.active && translation.status === 'done' && !!translation.translated;
  const shownBody = showTranslated ? translation.translated! : trimmedBody;

  const chipStatus = translation.status === 'detecting' ? 'translating' : translation.status;
  const showChip =
    translation.active &&
    (chipStatus === 'downloading' ||
      chipStatus === 'translating' ||
      chipStatus === 'done' ||
      chipStatus === 'error');

  return (
    <MessageTextBody
      emote={variant === 'emote'}
      notice={variant === 'notice'}
      preWrap={showTranslated || typeof customBody !== 'string'}
      jumboEmoji={JUMBO_EMOJI_REG.test(shownBody)}
      style={style}
    >
      {variant === 'emote' && <b>{`${displayName} `}</b>}
      {showTranslated ? (
        <span dir="auto">{renderBody({ body: shownBody, customBody: undefined })}</span>
      ) : (
        renderBody({
          body: trimmedBody,
          customBody: typeof customBody === 'string' ? customBody : undefined,
        })
      )}
      {edited && <MessageEditedContent onEditHistoryClick={onEditHistoryClick} />}
      {showChip && (
        <MessageTranslatedContent
          fromLangName={languageName(translation.fromLang ?? '')}
          status={chipStatus as 'downloading' | 'translating' | 'done' | 'error'}
          downloadProgress={translation.downloadProgress}
          onToggle={translation.toggle}
        />
      )}
    </MessageTextBody>
  );
}

type MTextProps = {
  edited?: boolean;
  onEditHistoryClick?: () => void;
  content: Record<string, unknown>;
  renderBody: (props: RenderBodyProps) => ReactNode;
  renderUrlsPreview?: (urls: string[]) => ReactNode;
  style?: CSSProperties;
  eventId?: string;
};
export function MText({
  edited,
  onEditHistoryClick,
  content,
  renderBody,
  renderUrlsPreview,
  style,
  eventId,
}: MTextProps) {
  const { body, formatted_body: customBody } = content;

  if (typeof body !== 'string') return <BrokenContent />;
  const trimmedBody = trimReplyFromBody(body);
  const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
  const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;

  return (
    <>
      <CollapsibleBody eventId={eventId}>
        {eventId ? (
          <TranslatableBody
            variant="text"
            eventId={eventId}
            trimmedBody={trimmedBody}
            customBody={typeof customBody === 'string' ? customBody : undefined}
            renderBody={renderBody}
            edited={edited}
            onEditHistoryClick={onEditHistoryClick}
            style={style}
          />
        ) : (
          <MessageTextBody
            preWrap={typeof customBody !== 'string'}
            jumboEmoji={JUMBO_EMOJI_REG.test(trimmedBody)}
            style={style}
          >
            {renderBody({
              body: trimmedBody,
              customBody: typeof customBody === 'string' ? customBody : undefined,
            })}
            {edited && <MessageEditedContent onEditHistoryClick={onEditHistoryClick} />}
          </MessageTextBody>
        )}
      </CollapsibleBody>
      {renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
    </>
  );
}

type MEmoteProps = {
  displayName: string;
  edited?: boolean;
  onEditHistoryClick?: () => void;
  content: Record<string, unknown>;
  renderBody: (props: RenderBodyProps) => ReactNode;
  renderUrlsPreview?: (urls: string[]) => ReactNode;
  eventId?: string;
};
export function MEmote({
  displayName,
  edited,
  onEditHistoryClick,
  content,
  renderBody,
  renderUrlsPreview,
  eventId,
}: MEmoteProps) {
  const { body, formatted_body: customBody } = content;

  if (typeof body !== 'string') return <BrokenContent />;
  const trimmedBody = trimReplyFromBody(body);
  const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
  const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;

  return (
    <>
      <CollapsibleBody eventId={eventId}>
        {eventId ? (
          <TranslatableBody
            variant="emote"
            displayName={displayName}
            eventId={eventId}
            trimmedBody={trimmedBody}
            customBody={typeof customBody === 'string' ? customBody : undefined}
            renderBody={renderBody}
            edited={edited}
            onEditHistoryClick={onEditHistoryClick}
          />
        ) : (
          <MessageTextBody
            emote
            preWrap={typeof customBody !== 'string'}
            jumboEmoji={JUMBO_EMOJI_REG.test(trimmedBody)}
          >
            <b>{`${displayName} `}</b>
            {renderBody({
              body: trimmedBody,
              customBody: typeof customBody === 'string' ? customBody : undefined,
            })}
            {edited && <MessageEditedContent onEditHistoryClick={onEditHistoryClick} />}
          </MessageTextBody>
        )}
      </CollapsibleBody>
      {renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
    </>
  );
}

type MNoticeProps = {
  edited?: boolean;
  onEditHistoryClick?: () => void;
  content: Record<string, unknown>;
  renderBody: (props: RenderBodyProps) => ReactNode;
  renderUrlsPreview?: (urls: string[]) => ReactNode;
  eventId?: string;
};
export function MNotice({
  edited,
  onEditHistoryClick,
  content,
  renderBody,
  renderUrlsPreview,
  eventId,
}: MNoticeProps) {
  const { body, formatted_body: customBody } = content;

  if (typeof body !== 'string') return <BrokenContent />;
  const trimmedBody = trimReplyFromBody(body);
  const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
  const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;

  return (
    <>
      <CollapsibleBody eventId={eventId}>
        {eventId ? (
          <TranslatableBody
            variant="notice"
            eventId={eventId}
            trimmedBody={trimmedBody}
            customBody={typeof customBody === 'string' ? customBody : undefined}
            renderBody={renderBody}
            edited={edited}
            onEditHistoryClick={onEditHistoryClick}
          />
        ) : (
          <MessageTextBody
            notice
            preWrap={typeof customBody !== 'string'}
            jumboEmoji={JUMBO_EMOJI_REG.test(trimmedBody)}
          >
            {renderBody({
              body: trimmedBody,
              customBody: typeof customBody === 'string' ? customBody : undefined,
            })}
            {edited && <MessageEditedContent onEditHistoryClick={onEditHistoryClick} />}
          </MessageTextBody>
        )}
      </CollapsibleBody>
      {renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
    </>
  );
}

type RenderImageContentProps = {
  body: string;
  filename?: string;
  info?: IImageInfo & IThumbnailContent;
  mimeType?: string;
  url: string;
  encInfo?: IEncryptedFile;
  markedAsSpoiler?: boolean;
  spoilerReason?: string;
};
// Media frame sizing. When intrinsic width/height are known, drive the box by
// aspect-ratio so its height tracks the responsive (maxWidth:100%) width — a
// fixed pixel height computed for a 400px-wide layout otherwise crops (images,
// object-fit:cover) or letterboxes (videos, object-fit:contain) on phones where
// the box narrows below 400px. On desktop the box stays 400px wide, so the
// aspect-ratio yields the identical height. Falls back to the fixed height when
// dimensions are unknown.
const attachmentMediaStyle = (
  w: number | undefined,
  h: number | undefined,
  fallbackHeight: number,
): CSSProperties =>
  w && h
    ? { aspectRatio: `${w} / ${h}`, minHeight: toRem(48) }
    : { height: toRem(fallbackHeight < 48 ? 48 : fallbackHeight) };

type MImageProps = {
  content: IImageContent;
  renderImageContent: (props: RenderImageContentProps) => ReactNode;
  outlined?: boolean;
};
export function MImage({ content, renderImageContent, outlined }: MImageProps) {
  const imgInfo = content?.info;
  const mxcUrl = content.file?.url ?? content.url;
  if (typeof mxcUrl !== 'string') {
    return <BrokenContent />;
  }
  const height = scaleYDimension(imgInfo?.w || 400, 400, imgInfo?.h || 400);

  return (
    <Attachment outlined={outlined}>
      <AttachmentBox style={attachmentMediaStyle(imgInfo?.w, imgInfo?.h, height)}>
        {renderImageContent({
          body: content.body || 'Image',
          info: imgInfo,
          mimeType: imgInfo?.mimetype,
          url: mxcUrl,
          encInfo: content.file,
          markedAsSpoiler: content[MATRIX_SPOILER_PROPERTY_NAME],
          spoilerReason: content[MATRIX_SPOILER_REASON_PROPERTY_NAME],
        })}
      </AttachmentBox>
    </Attachment>
  );
}

type RenderVideoContentProps = {
  body: string;
  info: IVideoInfo & IThumbnailContent;
  mimeType: string;
  url: string;
  encInfo?: IEncryptedFile;
  markedAsSpoiler?: boolean;
  spoilerReason?: string;
};
type MVideoProps = {
  content: IVideoContent;
  renderAsFile: () => ReactNode;
  renderVideoContent: (props: RenderVideoContentProps) => ReactNode;
  outlined?: boolean;
};
export function MVideo({ content, renderAsFile, renderVideoContent, outlined }: MVideoProps) {
  const videoInfo = content?.info;
  const mxcUrl = content.file?.url ?? content.url;
  const safeMimeType = getBlobSafeMimeType(videoInfo?.mimetype ?? '');

  if (!videoInfo || !safeMimeType.startsWith('video') || typeof mxcUrl !== 'string') {
    if (mxcUrl) {
      return renderAsFile();
    }
    return <BrokenContent />;
  }

  const height = scaleYDimension(videoInfo.w || 400, 400, videoInfo.h || 400);

  const filename = content.filename ?? content.body ?? 'Video';

  return (
    <Attachment outlined={outlined}>
      <AttachmentHeader>
        <FileHeader
          body={filename}
          mimeType={safeMimeType}
          after={
            <FileDownloadButton
              filename={filename}
              url={mxcUrl}
              mimeType={safeMimeType}
              encInfo={content.file}
            />
          }
        />
      </AttachmentHeader>
      <AttachmentBox style={attachmentMediaStyle(videoInfo.w, videoInfo.h, height)}>
        {renderVideoContent({
          body: content.body || 'Video',
          info: videoInfo,
          mimeType: safeMimeType,
          url: mxcUrl,
          encInfo: content.file,
          markedAsSpoiler: content[MATRIX_SPOILER_PROPERTY_NAME],
          spoilerReason: content[MATRIX_SPOILER_REASON_PROPERTY_NAME],
        })}
      </AttachmentBox>
    </Attachment>
  );
}

type RenderAudioContentProps = {
  info: IAudioInfo;
  mimeType: string;
  url: string;
  encInfo?: IEncryptedFile;
  waveform?: number[];
};
type MAudioProps = {
  content: IAudioContent;
  renderAsFile: () => ReactNode;
  renderAudioContent: (props: RenderAudioContentProps) => ReactNode;
  outlined?: boolean;
};
export function MAudio({ content, renderAsFile, renderAudioContent, outlined }: MAudioProps) {
  const audioInfo = content?.info;
  const mxcUrl = content.file?.url ?? content.url;
  const safeMimeType = getBlobSafeMimeType(audioInfo?.mimetype ?? '');

  if (!audioInfo || !safeMimeType.startsWith('audio') || typeof mxcUrl !== 'string') {
    if (mxcUrl) {
      return renderAsFile();
    }
    return <BrokenContent />;
  }

  const filename = content.filename ?? content.body ?? 'Audio';
  const waveform = (content as unknown as { 'org.matrix.msc1767.audio'?: { waveform?: number[] } })[
    'org.matrix.msc1767.audio'
  ]?.waveform;
  return (
    <Attachment outlined={outlined}>
      <AttachmentHeader>
        <FileHeader
          body={filename}
          mimeType={safeMimeType}
          after={
            <FileDownloadButton
              filename={filename}
              url={mxcUrl}
              mimeType={safeMimeType}
              encInfo={content.file}
            />
          }
        />
      </AttachmentHeader>
      <AttachmentBox>
        <AttachmentContent>
          {renderAudioContent({
            info: audioInfo,
            mimeType: safeMimeType,
            url: mxcUrl,
            encInfo: content.file,
            waveform,
          })}
        </AttachmentContent>
      </AttachmentBox>
    </Attachment>
  );
}

type RenderFileContentProps = {
  body: string;
  info: IFileInfo & IThumbnailContent;
  mimeType: string;
  url: string;
  encInfo?: IEncryptedFile;
};
type MFileProps = {
  content: IFileContent;
  renderFileContent: (props: RenderFileContentProps) => ReactNode;
  outlined?: boolean;
};
export function MFile({ content, renderFileContent, outlined }: MFileProps) {
  const fileInfo = content?.info;
  const mxcUrl = content.file?.url ?? content.url;

  if (typeof mxcUrl !== 'string') {
    return <BrokenContent />;
  }

  return (
    <Attachment outlined={outlined}>
      <AttachmentHeader>
        <FileHeader
          body={content.filename ?? content.body ?? 'Unnamed File'}
          mimeType={fileInfo?.mimetype ?? FALLBACK_MIMETYPE}
        />
      </AttachmentHeader>
      <AttachmentBox>
        <AttachmentContent>
          {renderFileContent({
            body: content.filename ?? content.body ?? 'File',
            info: fileInfo ?? {},
            mimeType: fileInfo?.mimetype ?? FALLBACK_MIMETYPE,
            url: mxcUrl,
            encInfo: content.file,
          })}
        </AttachmentContent>
      </AttachmentBox>
    </Attachment>
  );
}

type MLocationProps = {
  content: IContent;
};
export function MLocation({ content }: MLocationProps) {
  const { t } = useTranslation();
  // Prefer the legacy top-level geo_uri, but fall back to the MSC3488 extensible
  // location block so events from clients that only send the new shape (uri under
  // org.matrix.msc3488.location / m.location) still render instead of appearing
  // broken.
  const msc3488 = (content['org.matrix.msc3488.location'] ?? content['m.location']) as
    | { uri?: string; description?: string }
    | undefined;
  const geoUri =
    typeof content.geo_uri === 'string'
      ? content.geo_uri
      : typeof msc3488?.uri === 'string'
        ? msc3488.uri
        : undefined;
  if (typeof geoUri !== 'string') return <BrokenContent />;
  const location = parseGeoUri(geoUri);
  if (!location) return <BrokenContent />;

  const lat = parseFloat(location.latitude);
  const lon = parseFloat(location.longitude);
  if (!isFinite(lat) || !isFinite(lon)) return <BrokenContent />;
  const description = typeof msc3488?.description === 'string' ? msc3488.description : undefined;
  const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.007},${
    lat - 0.004
  },${lon + 0.007},${lat + 0.004}&layer=mapnik&marker=${lat},${lon}`;

  return (
    <Box direction="Column" alignItems="Start" gap="200">
      <iframe
        title="Location"
        src={mapSrc}
        style={{
          width: '280px',
          height: '160px',
          border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
          borderRadius: '8px',
          display: 'block',
        }}
        scrolling="no"
        loading="lazy"
        sandbox="allow-scripts"
      />
      {description && (
        <Text size="T300" style={{ wordBreak: 'break-word', maxWidth: '280px' }}>
          {description}
        </Text>
      )}
      <Text size="T300" priority="300">
        {`${lat.toFixed(5)}, ${lon.toFixed(5)}`}
      </Text>
      <Button
        as="a"
        size="400"
        href={`https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=16/${lat}/${lon}`}
        target="_blank"
        rel="noreferrer noopener"
        variant="Secondary"
        fill="Solid"
        radii="300"
        before={<Icon src={Icons.External} size="50" />}
      >
        <Text size="B300">{t('Organisms.Message.open_location')}</Text>
      </Button>
    </Box>
  );
}

type MStickerProps = {
  content: IImageContent;
  renderImageContent: (props: RenderImageContentProps) => ReactNode;
};
export function MSticker({ content, renderImageContent }: MStickerProps) {
  const imgInfo = content?.info;
  const mxcUrl = content.file?.url ?? content.url;
  if (typeof mxcUrl !== 'string') {
    return <MessageBrokenContent />;
  }
  const height = scaleYDimension(imgInfo?.w || 152, 152, imgInfo?.h || 152);

  return (
    <AttachmentBox
      style={{
        height: toRem(height < 48 ? 48 : height),
        width: toRem(152),
      }}
    >
      {renderImageContent({
        body: content.body || 'Sticker',
        info: imgInfo,
        mimeType: imgInfo?.mimetype,
        url: mxcUrl,
        encInfo: content.file,
      })}
    </AttachmentBox>
  );
}
