import React, { useCallback, useMemo, useRef, useState } from 'react';
import {
  Box,
  Button,
  Chip,
  Icon,
  IconButton,
  Icons,
  Input,
  PopOut,
  Spinner,
  Text,
  color,
  config,
  toRem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { EmojiBoard } from '../emoji-board';
import { SoundboardClip, SoundboardContent, SoundboardPack } from '../../plugins/soundboard';
import { uniqueShortcode } from '../../plugins/soundboard/utils';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import {
  playClipLocally,
  resolveClipObjectUrl,
  SOUNDBOARD_ACCEPT,
  SOUNDBOARD_MAX_CLIP_BYTES,
  SOUNDBOARD_MAX_CLIPS,
} from '../../utils/soundboardClips';
import { stopPropagation } from '../../utils/keyboard';

type ClipDraft = {
  url: string;
  body: string;
  emoji: string;
  volume: number;
  info?: SoundboardClip['info'];
};

type SoundboardPackEditorProps = {
  pack: SoundboardPack;
  canEdit?: boolean;
  onUpdate: (content: SoundboardContent) => Promise<void>;
};

/**
 * Reusable single-pack soundboard manager (used by the settings page and the
 * in-call management mode). Mirrors image-pack-view/ImagePackContent's staged-
 * edit + batched-save pattern, but per-clip fields are name + emoji + volume.
 */
export function SoundboardPackEditor({ pack, canEdit, onUpdate }: SoundboardPackEditorProps) {
  const mx = useMatrixClient();

  // Staged, unsaved state:
  const [drafts, setDrafts] = useState<Map<string, ClipDraft>>(new Map()); // shortcode -> edits
  const [deleted, setDeleted] = useState<Set<string>>(new Set());
  const [uploads, setUploads] = useState<Array<{ shortcode: string } & ClipDraft>>([]);
  const [uploading, setUploading] = useState(false);
  const [error, setError] = useState<string>();
  const [emojiFor, setEmojiFor] = useState<string>(); // shortcode currently picking an emoji
  const [busyPreview, setBusyPreview] = useState<string>();
  const fileInputRef = useRef<HTMLInputElement>(null);
  const emojiAnchorRef = useRef<HTMLElement | null>(null);

  const existing = useMemo(() => pack.getClips(), [pack]);
  const clipCount = existing.filter((c) => !deleted.has(c.shortcode)).length + uploads.length;

  const dirty = drafts.size > 0 || deleted.size > 0 || uploads.length > 0;

  const draftFor = (shortcode: string, base: { body: string; emoji: string; volume: number }) =>
    drafts.get(shortcode) ?? { url: '', ...base };

  const setDraft = (shortcode: string, patch: Partial<ClipDraft>, base: ClipDraft) => {
    setDrafts((prev) => {
      const next = new Map(prev);
      next.set(shortcode, { ...base, ...(next.get(shortcode) ?? {}), ...patch });
      return next;
    });
  };

  const preview = useCallback(
    async (id: string, mxc: string, volume: number) => {
      setBusyPreview(id);
      try {
        const url = await resolveClipObjectUrl(mx, mxc);
        playClipLocally(url, volume / 100);
      } catch {
        /* ignore preview errors */
      } finally {
        setBusyPreview(undefined);
      }
    },
    [mx],
  );

  const handleFiles = useCallback(
    async (files: FileList | null) => {
      if (!files || files.length === 0) return;
      setUploading(true);
      setError(undefined);
      try {
        const taken = new Set<string>([
          ...existing.map((c) => c.shortcode),
          ...uploads.map((u) => u.shortcode),
        ]);
        for (let i = 0; i < files.length; i += 1) {
          const file = files[i];
          if (clipCount + uploads.length >= SOUNDBOARD_MAX_CLIPS) {
            throw new Error(`Soundboard is full (max ${SOUNDBOARD_MAX_CLIPS} clips).`);
          }
          if (file.size > SOUNDBOARD_MAX_CLIP_BYTES) {
            throw new Error(`"${file.name}" is too large (max 1 MB).`);
          }
          // eslint-disable-next-line no-await-in-loop
          const res = await mx.uploadContent(file, { type: file.type || 'audio/mpeg' });
          const mxc = res.content_uri;
          if (!mxc) throw new Error('Upload failed.');
          const name = file.name.replace(/\.[^/.]+$/, '');
          const shortcode = uniqueShortcode(name, taken);
          taken.add(shortcode);
          setUploads((prev) => [
            ...prev,
            {
              shortcode,
              url: mxc,
              body: name,
              emoji: '',
              volume: 100,
              info: { mimetype: file.type || undefined, size: file.size },
            },
          ]);
        }
      } catch (e) {
        setError(e instanceof Error ? e.message : 'Upload failed.');
      } finally {
        setUploading(false);
      }
    },
    [mx, existing, uploads, clipCount],
  );

  const [saveState, save] = useAsyncCallback(
    useCallback(async () => {
      const clips: Record<string, SoundboardClip> = {};
      existing.forEach((c) => {
        if (deleted.has(c.shortcode)) return;
        const d = drafts.get(c.shortcode);
        clips[c.shortcode] = {
          url: c.url,
          body: d ? d.body : c.body,
          emoji: d ? d.emoji || undefined : c.emoji,
          volume: d ? d.volume : c.volume,
          info: c.info,
        };
      });
      uploads.forEach((u) => {
        clips[u.shortcode] = {
          url: u.url,
          body: u.body,
          emoji: u.emoji || undefined,
          volume: u.volume,
          info: u.info,
        };
      });
      await onUpdate({ pack: pack.meta.content, clips });
      setDrafts(new Map());
      setDeleted(new Set());
      setUploads([]);
    }, [existing, deleted, drafts, uploads, onUpdate, pack]),
  );
  const saving = saveState.status === AsyncStatus.Loading;

  const renderRow = (key: string, base: ClipDraft, isUpload: boolean, markedDeleted: boolean) => {
    const d = isUpload ? base : draftFor(key, base);
    const rowVolume = isUpload ? base.volume : d.volume;
    const rowBody = isUpload ? base.body : d.body;
    const rowEmoji = isUpload ? base.emoji : d.emoji;
    const commit = (patch: Partial<ClipDraft>) => {
      if (isUpload) {
        setUploads((prev) => prev.map((u) => (u.shortcode === key ? { ...u, ...patch } : u)));
      } else {
        setDraft(key, patch, base);
      }
    };
    return (
      <Box
        key={key}
        alignItems="Center"
        gap="200"
        style={{
          padding: config.space.S200,
          borderRadius: config.radii.R400,
          background: color.SurfaceVariant.Container,
          opacity: markedDeleted ? 0.5 : 1,
        }}
      >
        <IconButton
          size="300"
          radii="300"
          variant="Secondary"
          disabled={busyPreview === key}
          onClick={() => preview(key, base.url, rowVolume)}
          aria-label={`Preview ${rowBody}`}
        >
          {busyPreview === key ? <Spinner size="100" /> : <Icon size="100" src={Icons.Play} />}
        </IconButton>
        <IconButton
          size="300"
          radii="300"
          variant="Secondary"
          disabled={!canEdit || markedDeleted}
          onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
            emojiAnchorRef.current = evt.currentTarget;
            setEmojiFor(key);
          }}
          aria-label="Pick emoji"
        >
          <Text size="T400">{rowEmoji || '🔊'}</Text>
        </IconButton>
        <Box grow="Yes">
          <Input
            variant="Surface"
            size="300"
            defaultValue={rowBody}
            readOnly={!canEdit || markedDeleted}
            onChange={(e: React.ChangeEvent<HTMLInputElement>) => commit({ body: e.target.value })}
            aria-label="Clip name"
          />
        </Box>
        <Box alignItems="Center" gap="100" shrink="No" style={{ width: toRem(120) }}>
          <Icon size="50" src={Icons.VolumeHigh} />
          <input
            type="range"
            min={0}
            max={100}
            step={5}
            defaultValue={rowVolume}
            disabled={!canEdit || markedDeleted}
            onChange={(e) => commit({ volume: parseInt(e.target.value, 10) })}
            style={{ flexGrow: 1 }}
            aria-label="Clip volume"
          />
        </Box>
        {canEdit && !isUpload && (
          <IconButton
            size="300"
            radii="300"
            variant={markedDeleted ? 'Success' : 'Critical'}
            onClick={() =>
              setDeleted((prev) => {
                const next = new Set(prev);
                if (next.has(key)) next.delete(key);
                else next.add(key);
                return next;
              })
            }
            aria-label={markedDeleted ? 'Undo delete' : 'Delete clip'}
          >
            <Icon size="100" src={markedDeleted ? Icons.Plus : Icons.Delete} />
          </IconButton>
        )}
        {canEdit && isUpload && (
          <IconButton
            size="300"
            radii="300"
            variant="Critical"
            onClick={() => setUploads((prev) => prev.filter((u) => u.shortcode !== key))}
            aria-label="Remove upload"
          >
            <Icon size="100" src={Icons.Cross} />
          </IconButton>
        )}
      </Box>
    );
  };

  return (
    <Box direction="Column" gap="300">
      <input
        ref={fileInputRef}
        aria-label="Upload soundboard clip"
        type="file"
        accept={SOUNDBOARD_ACCEPT}
        multiple
        hidden
        onChange={(e) => {
          handleFiles(e.target.files);
          e.target.value = '';
        }}
      />

      <Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
        <Text size="H4">{pack.meta.name ?? 'Soundboard'}</Text>
        {canEdit && (
          <Chip
            variant="Secondary"
            radii="Pill"
            disabled={uploading || clipCount >= SOUNDBOARD_MAX_CLIPS}
            onClick={() => fileInputRef.current?.click()}
            before={uploading ? <Spinner size="100" /> : <Icon size="100" src={Icons.Plus} />}
          >
            <Text size="B300">Upload</Text>
          </Chip>
        )}
      </Box>

      <Box direction="Column" gap="100">
        {existing.map((c) =>
          renderRow(
            c.shortcode,
            { url: c.url, body: c.body ?? c.shortcode, emoji: c.emoji ?? '', volume: c.volume },
            false,
            deleted.has(c.shortcode),
          ),
        )}
        {uploads.map((u) => renderRow(u.shortcode, u, true, false))}
        {existing.length === 0 && uploads.length === 0 && (
          <Text size="T200" priority="300">
            No clips yet. Upload a short audio clip (max 1 MB){canEdit ? '' : ' — ask an admin'}.
          </Text>
        )}
      </Box>

      {error && (
        <Text size="T200" style={{ color: color.Critical.Main }}>
          {error}
        </Text>
      )}

      {canEdit && dirty && (
        <Box gap="200">
          <Button
            size="300"
            variant="Success"
            radii="300"
            disabled={saving}
            onClick={() => save()}
            before={saving ? <Spinner size="100" fill="Solid" /> : undefined}
          >
            <Text size="B300">Save changes</Text>
          </Button>
          <Button
            size="300"
            variant="Secondary"
            fill="Soft"
            radii="300"
            disabled={saving}
            onClick={() => {
              setDrafts(new Map());
              setDeleted(new Set());
              setUploads([]);
              setError(undefined);
            }}
          >
            <Text size="B300">Reset</Text>
          </Button>
        </Box>
      )}

      <PopOut
        anchor={emojiFor ? emojiAnchorRef.current?.getBoundingClientRect() : undefined}
        position="Bottom"
        content={
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              onDeactivate: () => setEmojiFor(undefined),
              clickOutsideDeactivates: true,
              escapeDeactivates: stopPropagation,
            }}
          >
            <EmojiBoard
              imagePackRooms={[]}
              returnFocusOnDeactivate={false}
              onEmojiSelect={(unicode: string) => {
                const key = emojiFor;
                setEmojiFor(undefined);
                if (!key) return;
                const up = uploads.find((u) => u.shortcode === key);
                if (up) {
                  setUploads((prev) =>
                    prev.map((u) => (u.shortcode === key ? { ...u, emoji: unicode } : u)),
                  );
                } else {
                  const c = existing.find((x) => x.shortcode === key);
                  if (c)
                    setDraft(
                      key,
                      { emoji: unicode },
                      {
                        url: c.url,
                        body: c.body ?? c.shortcode,
                        emoji: c.emoji ?? '',
                        volume: c.volume,
                      },
                    );
                }
              }}
              requestClose={() => setEmojiFor(undefined)}
            />
          </FocusTrap>
        }
      >
        <span />
      </PopOut>
    </Box>
  );
}
