import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import { Download, DownloadCloud, PlayCircle, Stethoscope, TriangleAlert } from 'lucide-react';
import { Button, Card, Checkbox, Input, Select, Spinner, Toast } from '../components';
import {
  downloadTroubleshootingArchive,
  downloadTroubleshootingMediaSample,
  getChannelById,
  getFFmpegProfiles,
  getGraphicsElements,
  getMediaItemInfo,
  getTroubleshootingPlaybackStatus,
  getTroubleshootingStreamSelectors,
  getTroubleshootingSubtitles,
  getWatermarks,
  messageFromTroubleshootError,
  startTroubleshootingPlayback,
  type Channel,
  type FFmpegProfile,
  type GraphicsElement,
  type MediaItemInfo,
  type StartTroubleshootingPlaybackBody,
  type TroubleshootingPlaybackStatus,
  type TroubleshootingSubtitle,
  type Watermark
} from '../api';
import { HlsPlayer } from '../media/HlsPlayer';
import { resetIptvTokenCache, withIptvToken } from '../media/iptvToken';
import { parseDurationSeconds } from '../media/mediaKinds';

// StreamingMode.HttpLiveStreamingSegmenter — mirrors PlaybackTroubleshooting.razor, which always
// requests the segmenter mode for troubleshooting playback. The start endpoint takes the StreamingMode
// as its serialized enum name (the generated request type is a string union), not the numeric value.
const STREAMING_MODE_HLS_SEGMENTER: StartTroubleshootingPlaybackBody['streamingMode'] = 'HttpLiveStreamingSegmenter';
const STATUS_POLL_MS = 2000;
// How long after Play we keep waiting for the server to report 'running' before giving up.
// PrepareTroubleshootingPlayback runs several DB queries before it takes the playback lock, so the
// first polls after Play can still see the PREVIOUS session's settled result — those are ignored
// during the 'starting' phase (see applyStatus) until this timeout elapses.
const STARTING_TIMEOUT_MS = 30_000;

// Play lifecycle owned by this screen (the server only knows idle/running/completed/failed):
// idle → (Play) → starting → (server reports running) → running → (server settles) → idle.
type PlayPhase = 'idle' | 'starting' | 'running';

type Mode =
  | { kind: 'media'; mediaItemId: number }
  | { kind: 'channel'; channelId: number }
  | { kind: 'empty' };

function parseMode(search: string): Mode {
  const params = new URLSearchParams(search);
  const rawMedia = params.get('mediaItem');
  const rawChannel = params.get('channel');
  const mediaItemId = rawMedia ? Number(rawMedia) : NaN;
  const channelId = rawChannel ? Number(rawChannel) : NaN;
  const hasMedia = Number.isInteger(mediaItemId) && mediaItemId > 0;
  const hasChannel = Number.isInteger(channelId) && channelId > 0;

  // mediaItem takes precedence when both are present (matching Blazor's OnParametersSetAsync,
  // which checks MediaItemId first); neither param is the empty state.
  if (hasMedia) {
    return { kind: 'media', mediaItemId };
  }
  if (hasChannel) {
    return { kind: 'channel', channelId };
  }
  return { kind: 'empty' };
}

// Insert a space at lowercase→uppercase boundaries, matching the Blazor page's kind humanization
// (e.g. "MusicVideo" → "Music Video", "RemoteStream" → "Remote Stream").
function humanizeKind(kind: string): string {
  return kind.replace(/([a-z])([A-Z])/g, '$1 $2');
}

// Speed thresholds copied from PlaybackTroubleshooting.razor GetSpeedClass: <0.9 slow (red),
// >1.1 fast enough (green), otherwise marginal (amber).
function speedColor(speed: number): string {
  if (speed < 0.9) {
    return 'var(--status-error)';
  }
  if (speed > 1.1) {
    return 'var(--status-ok)';
  }
  return 'var(--status-warn)';
}

interface LoadedData {
  profiles: FFmpegProfile[];
  streamSelectors: string[];
  watermarks: Watermark[];
  graphicsElements: GraphicsElement[];
  // media mode
  info: MediaItemInfo | null;
  rawKind: null | string;
  subtitles: TroubleshootingSubtitle[];
  // channel mode
  channel: Channel | null;
  title: string;
}

type LoadState =
  | { status: 'loading' }
  | { status: 'error'; error: string }
  | { status: 'success'; data: LoadedData };

interface FormState {
  ffmpegProfileId: null | number;
  streamSelector: string;
  subtitleId: null | number;
  watermarkIds: number[];
  graphicsElementIds: number[];
  startFromBeginning: boolean;
  seekSeconds: number;
  startValue: string;
}

function FormRow({ children, label }: { children: ReactNode; label: string }) {
  return (
    <div className="ctv-settings-row">
      <div className="ctv-settings-row-main">
        <div className="ctv-settings-row-label">{label}</div>
      </div>
      <div className="ctv-settings-row-control" style={{ flex: '0 0 340px' }}>
        {children}
      </div>
    </div>
  );
}

export function PlaybackTroubleshootingScreen() {
  const [search, setSearch] = useState(() => window.location.search);
  const mode = useMemo(() => parseMode(search), [search]);

  const [loadState, setLoadState] = useState<LoadState>({ status: 'loading' });
  const [form, setForm] = useState<FormState>({
    ffmpegProfileId: null,
    streamSelector: '',
    subtitleId: null,
    watermarkIds: [],
    graphicsElementIds: [],
    startFromBeginning: false,
    seekSeconds: 0,
    startValue: ''
  });
  const [playerSrc, setPlayerSrc] = useState<null | string>(null);
  // Incremented on every Play so HlsPlayer re-attaches even when the manifest URL is unchanged —
  // the manifest GET is what starts a session server-side, so a repeat Play with identical settings
  // must still issue a new request.
  const [playToken, setPlayToken] = useState(0);
  const [playbackStatus, setPlaybackStatus] = useState<null | TroubleshootingPlaybackStatus>(null);
  const [playPhase, setPlayPhase] = useState<PlayPhase>('idle');
  const [hasPlayed, setHasPlayed] = useState(false);
  const [notice, setNotice] = useState<null | { tone: 'error' | 'ok' | 'warn'; message: string }>(null);

  const activeRef = useRef(true);
  const loadSeqRef = useRef(0);
  const pollRef = useRef<null | number>(null);
  // Mirror of playPhase readable from the polling interval callback without re-creating it.
  const phaseRef = useRef<PlayPhase>('idle');
  const startingSinceRef = useRef(0);

  const setPhase = useCallback((phase: PlayPhase) => {
    phaseRef.current = phase;
    setPlayPhase(phase);
  }, []);

  useEffect(() => {
    // popstate is an external event (not an effect body), so resetting to the loading state here is
    // allowed by the react-hooks set-state-in-effect rule and mirrors MediaRouteScreen's tracking.
    const onPopState = () => {
      setSearch(window.location.search);
      setLoadState({ status: 'loading' });
    };
    window.addEventListener('popstate', onPopState);
    return () => window.removeEventListener('popstate', onPopState);
  }, []);

  useEffect(() => {
    activeRef.current = true;
    return () => {
      activeRef.current = false;
    };
  }, []);

  const stopPolling = useCallback(() => {
    if (pollRef.current !== null) {
      window.clearInterval(pollRef.current);
      pollRef.current = null;
    }
  }, []);

  // Apply a freshly-fetched status through the play-phase machine:
  // - 'starting': ignore settled/idle states entirely — the server hasn't taken the playback lock
  //   yet, so these are the PREVIOUS session's stale result (applying them would show a spurious
  //   completion over a session that is still spinning up). Promote to 'running' once the server
  //   reports it; give up with an error after STARTING_TIMEOUT_MS.
  // - 'running': apply; surface the completion notice exactly once on the settled transition.
  const applyStatus = useCallback(
    (status: TroubleshootingPlaybackStatus) => {
      if (!activeRef.current) {
        return;
      }

      if (phaseRef.current === 'starting') {
        if (status.state === 'running') {
          setPhase('running');
          setPlaybackStatus(status);
        } else if (Date.now() - startingSinceRef.current > STARTING_TIMEOUT_MS) {
          setPhase('idle');
          stopPolling();
          setNotice({
            tone: 'error',
            message: 'Troubleshooting playback did not start within 30 seconds'
          });
        }
        // Otherwise: stale settled/idle result during startup — keep waiting.
        return;
      }

      setPlaybackStatus(status);

      if (phaseRef.current === 'running' && (status.state === 'completed' || status.state === 'failed')) {
        setPhase('idle');
        stopPolling();
        if (status.exitCode === 0) {
          setNotice({ tone: 'ok', message: 'FFmpeg troubleshooting process exited successfully' });
        } else {
          setNotice({
            tone: 'warn',
            message: `FFmpeg troubleshooting process exited with code ${status.exitCode ?? 'unknown'}`
          });
        }
      }
    },
    [setPhase, stopPolling]
  );

  const pollOnce = useCallback(() => {
    getTroubleshootingPlaybackStatus()
      .then((status) => applyStatus(status))
      .catch(() => {
        // Ignore transient polling failures; the next tick retries.
      });
  }, [applyStatus]);

  const startPolling = useCallback(() => {
    stopPolling();
    pollRef.current = window.setInterval(() => pollOnce(), STATUS_POLL_MS);
  }, [pollOnce, stopPolling]);

  // Load form data whenever the mode changes. State transitions happen only in the promise
  // callbacks (never synchronously in the effect body) per docs/spa-conventions.md §3.
  const load = useCallback(() => {
    if (mode.kind === 'empty') {
      return;
    }

    const seq = ++loadSeqRef.current;
    const base = Promise.all([
      getFFmpegProfiles(),
      getTroubleshootingStreamSelectors(),
      getWatermarks(),
      // refresh=true re-syncs on-disk graphics element definitions first, matching Blazor's
      // RefreshGraphicsElements-before-GetAllGraphicsElements.
      getGraphicsElements(true)
    ]);

    base
      .then(async ([profiles, streamSelectors, watermarks, graphicsElements]) => {
        if (mode.kind === 'media') {
          const [info, subtitles] = await Promise.all([
            getMediaItemInfo(mode.mediaItemId),
            getTroubleshootingSubtitles(mode.mediaItemId)
          ]);
          return {
            profiles,
            streamSelectors,
            watermarks,
            graphicsElements,
            info,
            rawKind: info.kind,
            subtitles,
            channel: null,
            title: info.title
          } satisfies LoadedData;
        }

        const channel = await getChannelById(mode.channelId);
        return {
          profiles,
          streamSelectors,
          watermarks,
          graphicsElements,
          info: null,
          rawKind: null,
          subtitles: [],
          channel,
          title: channel.name ?? ''
        } satisfies LoadedData;
      })
      .then((data) => {
        if (!activeRef.current || seq !== loadSeqRef.current) {
          return;
        }

        // Derive initial form defaults (mirrors the Blazor OnParametersSetAsync / LoadMediaItem /
        // LoadChannel logic).
        let defaultProfileId: null | number = data.profiles[0]?.id ?? null;
        if (mode.kind === 'channel' && data.channel) {
          defaultProfileId = data.channel.ffmpegProfileId ?? defaultProfileId;
        }

        const isRemoteStream = (data.rawKind ?? '').toLowerCase() === 'remotestream';
        const durationSeconds = parseDurationSeconds(data.info?.duration) ?? 0;

        let streamSelector = '';
        if (mode.kind === 'channel' && data.channel?.streamSelectorMode === 'Custom') {
          streamSelector = data.channel.streamSelector ?? '';
        }

        setForm({
          ffmpegProfileId: defaultProfileId,
          streamSelector,
          subtitleId: null,
          watermarkIds: [],
          graphicsElementIds: [],
          // Start From Beginning defaults on for RemoteStream (and its checkbox is disabled).
          startFromBeginning: isRemoteStream,
          seekSeconds: isRemoteStream ? 0 : Math.round(durationSeconds / 2),
          startValue: ''
        });
        setLoadState({ status: 'success', data });
      })
      .catch((error: unknown) => {
        if (activeRef.current && seq === loadSeqRef.current) {
          setLoadState({ status: 'error', error: messageFromTroubleshootError(error, 'Unable to load troubleshooting options') });
        }
      });
  }, [mode]);

  useEffect(() => {
    load();
  }, [load]);

  // On mount, reflect any session already running (started here or elsewhere) so Play stays gated.
  // Intentional side effect: after an app restart the server reports state=idle with the previous
  // session's logs.txt tail still attached — we apply that status as-is so those logs are
  // recoverable from the UI (post-restart log recovery), matching what the status endpoint reports.
  useEffect(() => {
    getTroubleshootingPlaybackStatus()
      .then((status) => {
        if (!activeRef.current) {
          return;
        }
        setPlaybackStatus(status);
        if (status.state === 'running') {
          setPhase('running');
          startPolling();
        }
      })
      .catch(() => {
        // No status yet is fine.
      });
    return () => {
      stopPolling();
    };
  }, [setPhase, startPolling, stopPolling]);

  // Busy = a session this screen is aware of is starting or transcoding; gates Play + downloads.
  const isBusy = playPhase !== 'idle';

  const onDownloadArchive = async () => {
    try {
      await downloadTroubleshootingArchive();
    } catch (error: unknown) {
      setNotice({ tone: 'error', message: messageFromTroubleshootError(error, 'Unable to download results') });
    }
  };

  const onDownloadMediaSample = async (id: number) => {
    try {
      await downloadTroubleshootingMediaSample(id);
    } catch (error: unknown) {
      setNotice({ tone: 'error', message: messageFromTroubleshootError(error, 'Unable to download media sample') });
    }
  };

  // Build the start-playback request body, mirroring PlaybackTroubleshooting.razor PreviewChannel.
  // Returns null on invalid input (no profile, or a channel without a valid start datetime) — the same
  // guard the old URL builder used; Play is also disabled in those states so null is defensive.
  const buildPlaybackParams = useCallback((): StartTroubleshootingPlaybackBody | null => {
    // Sending ffmpegProfile=0 is always wrong (no such profile) — refuse without one.
    if (form.ffmpegProfileId == null) {
      return null;
    }

    // Stream selector and subtitle are mutually exclusive (selector wins).
    let streamSelector: null | string = null;
    let subtitleId: null | number = null;
    if (form.streamSelector !== '') {
      streamSelector = form.streamSelector;
    } else if (form.subtitleId != null) {
      subtitleId = form.subtitleId;
    }

    const common = {
      ffmpegProfile: form.ffmpegProfileId,
      streamingMode: STREAMING_MODE_HLS_SEGMENTER,
      watermark: form.watermarkIds,
      graphicsElement: form.graphicsElementIds,
      streamSelector,
      subtitleId
    };

    if (mode.kind === 'channel') {
      if (form.startValue === '') {
        return null;
      }
      const parsed = new Date(form.startValue);
      if (Number.isNaN(parsed.getTime())) {
        return null;
      }
      return {
        ...common,
        mediaItem: 0,
        channel: mode.channelId,
        seekSeconds: 0,
        start: parsed.toISOString()
      };
    }

    if (mode.kind === 'media') {
      return {
        ...common,
        mediaItem: mode.mediaItemId,
        channel: 0,
        seekSeconds: form.seekSeconds,
        start: null
      };
    }

    return null;
  }, [form, mode]);

  const onPlay = async () => {
    const body = buildPlaybackParams();
    if (body === null) {
      return;
    }
    setNotice(null);
    // Each explicit Play re-evaluates the IPTV token from scratch (drops any stale token / "JWT disabled"
    // latch cached from a prior config), so a manual retry can't be wedged by a stale credential (#552).
    resetIptvTokenCache();
    setPlaybackStatus((current) => (current ? { ...current, logs: null, speed: null } : current));
    setPhase('starting');
    startingSinceRef.current = Date.now();
    setHasPlayed(true);
    startPolling();

    try {
      const result = await startTroubleshootingPlayback(body);
      if (!activeRef.current) {
        return;
      }
      // The start endpoint returns the `/iptv` live manifest — feed it to HlsPlayer. Under a JWT-enabled
      // deployment /iptv/* needs a short-lived access token appended (#552); withIptvToken is a no-op when
      // JWT is off. The URL is constant across plays, so bump the token to force a re-attach even on a
      // repeat play.
      const src = await withIptvToken(result.url);
      if (!activeRef.current) {
        return;
      }
      setPlayerSrc(src);
      setPlayToken((current) => current + 1);
      // Fast-completion race (Codex MAJOR): the POST returns 200 only after segments exist, so a short
      // clip can settle between polls; applyStatus discards settled states while 'starting', turning a
      // genuine completion into a false 30s startup-failure. PrepareTroubleshootingPlayback calls
      // statusStore.Reset() server-side, so ANY status observed after a 200 belongs to THIS session —
      // promote 'starting' → 'running' here so the next poll's terminal status is accepted, not
      // discarded. The poll-based promotion is kept for the (normal) still-running case.
      if (phaseRef.current === 'starting') {
        setPhase('running');
      }
    } catch (error: unknown) {
      if (!activeRef.current) {
        return;
      }
      // A 409 (another session running) / 404 / 422 now surfaces its ProblemDetails detail inline.
      stopPolling();
      setPhase('idle');
      setNotice({
        tone: 'error',
        message: messageFromTroubleshootError(error, 'Unable to start troubleshooting playback')
      });
    }
  };

  if (mode.kind === 'empty') {
    return (
      <div className="ctv-collections">
        <Card padded>
          <div className="ctv-collections-empty" role="status">
            <Stethoscope aria-hidden="true" size={22} />
            <p style={{ marginTop: 12 }}>Playback troubleshooting starts from a channel or a media item.</p>
            <p className="ctv-settings-row-help">
              Open the Channels table and use a channel’s Troubleshoot action, or open a movie’s detail
              page and choose Troubleshoot Playback.
            </p>
          </div>
        </Card>
      </div>
    );
  }

  if (loadState.status === 'loading') {
    return (
      <div className="ctv-collections-loading" role="status">
        <Spinner size={18} />
        <span>Loading troubleshooting options…</span>
      </div>
    );
  }

  if (loadState.status === 'error') {
    return (
      <div className="ctv-collections">
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{loadState.error}</span>
          <span className="ctv-channels-spacer" />
          <Button
            onClick={() => {
              setLoadState({ status: 'loading' });
              load();
            }}
            size="sm"
            variant="secondary"
          >
            Retry
          </Button>
        </div>
      </div>
    );
  }

  const { data } = loadState;
  const isChannelMode = mode.kind === 'channel';
  const isRemoteStream = (data.rawKind ?? '').toLowerCase() === 'remotestream';
  const kindLabel = isChannelMode ? 'Channel' : data.rawKind ? humanizeKind(data.rawKind) : 'Playback';
  const settingsTitle = `${kindLabel} Settings${data.title ? ` — ${data.title}` : ''}`;

  const profileOptions = data.profiles.map((profile) => ({
    value: String(profile.id),
    label: profile.name ?? `Profile ${profile.id}`
  }));

  const streamSelectorOptions = [
    { value: '', label: '(none)' },
    ...data.streamSelectors.map((selector) => ({ value: selector, label: selector }))
  ];

  const subtitleOptions = [
    { value: '', label: '(none)' },
    ...data.subtitles.map((subtitle) => ({
      value: String(subtitle.id),
      label: `${subtitle.id}: ${subtitle.language ?? ''} - ${subtitle.title ?? ''} (${subtitle.codec ?? ''})`
    }))
  ];

  const subtitleDisabled = form.streamSelector !== '';
  const downloadsDisabled = isBusy;
  const speed = playbackStatus?.speed ?? null;
  const logs = playbackStatus?.logs ?? null;

  return (
    <div className="ctv-collections">
      <div className="ctv-channels-actionbar">
        <Button
          disabled={!hasPlayed || downloadsDisabled}
          onClick={() => void onDownloadArchive()}
          size="sm"
          startIcon={<Download aria-hidden="true" size={14} />}
          variant="secondary"
        >
          Download Results
        </Button>
        {mode.kind === 'media' && (
          <Button
            disabled={downloadsDisabled}
            onClick={() => void onDownloadMediaSample(mode.mediaItemId)}
            size="sm"
            startIcon={<DownloadCloud aria-hidden="true" size={14} />}
            variant="secondary"
          >
            Download Media Sample
          </Button>
        )}
        <span className="ctv-channels-spacer" />
      </div>

      {notice && (
        <Toast
          message={notice.message}
          onClose={() => setNotice(null)}
          tone={notice.tone}
        />
      )}

      <Card padded subtitle={isChannelMode ? 'Preview a channel at a point in time' : 'Preview a media item'} title={settingsTitle}>
        <FormRow label="FFmpeg Profile">
          <Select
            onChange={(event) => setForm((current) => ({ ...current, ffmpegProfileId: Number(event.target.value) }))}
            options={profileOptions}
            value={form.ffmpegProfileId == null ? '' : String(form.ffmpegProfileId)}
          />
        </FormRow>

        <FormRow label="Stream Selector">
          <Select
            disabled={data.streamSelectors.length === 0}
            onChange={(event) => setForm((current) => ({ ...current, streamSelector: event.target.value }))}
            options={streamSelectorOptions}
            value={form.streamSelector}
          />
        </FormRow>

        {isChannelMode ? (
          <FormRow label="Date and Time">
            <Input
              onChange={(event) => setForm((current) => ({ ...current, startValue: event.target.value }))}
              type="datetime-local"
              value={form.startValue}
            />
          </FormRow>
        ) : (
          <>
            <FormRow label="Subtitle">
              <Select
                disabled={subtitleDisabled}
                onChange={(event) =>
                  setForm((current) => ({
                    ...current,
                    subtitleId: event.target.value === '' ? null : Number(event.target.value)
                  }))
                }
                options={subtitleOptions}
                value={form.subtitleId == null ? '' : String(form.subtitleId)}
              />
            </FormRow>

            <FormRow label="Watermarks">
              <CheckboxList
                items={data.watermarks.map((watermark) => ({ id: watermark.id, name: watermark.name ?? `#${watermark.id}` }))}
                onToggle={(id) =>
                  setForm((current) => ({ ...current, watermarkIds: toggle(current.watermarkIds, id) }))
                }
                selected={form.watermarkIds}
              />
            </FormRow>

            <FormRow label="Graphics Elements">
              <CheckboxList
                items={data.graphicsElements.map((element) => ({ id: element.id, name: element.name ?? `#${element.id}` }))}
                onToggle={(id) =>
                  setForm((current) => ({ ...current, graphicsElementIds: toggle(current.graphicsElementIds, id) }))
                }
                selected={form.graphicsElementIds}
              />
            </FormRow>

            <FormRow label="Start From Beginning">
              <Checkbox
                checked={form.startFromBeginning}
                disabled={isRemoteStream}
                label="Start from the beginning"
                onChange={(next) =>
                  setForm((current) => ({
                    ...current,
                    startFromBeginning: next,
                    seekSeconds: next ? 0 : Math.round((parseDurationSeconds(data.info?.duration) ?? 0) / 2)
                  }))
                }
              />
            </FormRow>

            <FormRow label="Seek Seconds">
              <Input
                disabled={form.startFromBeginning}
                onChange={(event) => setForm((current) => ({ ...current, seekSeconds: Number(event.target.value) || 0 }))}
                type="number"
                value={String(form.seekSeconds)}
              />
            </FormRow>
          </>
        )}
      </Card>

      <Card padded subtitle="Live segmenter output" title="Preview">
        <Button
          disabled={isBusy || form.ffmpegProfileId == null || (isChannelMode && form.startValue === '')}
          onClick={() => void onPlay()}
          startIcon={<PlayCircle aria-hidden="true" size={15} />}
          variant="primary"
        >
          Play
        </Button>
        {isBusy && (
          <div className="ctv-collections-loading" role="status" style={{ marginTop: 12 }}>
            <Spinner size={16} />
            <span>{playPhase === 'starting' ? 'Starting…' : 'Transcoding…'}</span>
          </div>
        )}
        <div style={{ marginTop: 16, maxWidth: 720 }}>
          <HlsPlayer playToken={playToken} src={playerSrc} style={{ aspectRatio: '16 / 9', width: '100%', background: '#000' }} />
        </div>
      </Card>

      <Card
        padded
        title={
          <span>
            Logs{' '}
            {speed != null && (
              <span style={{ color: speedColor(speed) }}>(Speed: {speed}x)</span>
            )}
          </span>
        }
      >
        <textarea
          className="ctv-troubleshoot-pre"
          readOnly
          rows={20}
          style={{ width: '100%', fontFamily: 'var(--font-mono)', resize: 'vertical' }}
          value={logs ?? ''}
        />
      </Card>
    </div>
  );
}

function toggle(ids: number[], id: number): number[] {
  return ids.includes(id) ? ids.filter((existing) => existing !== id) : [...ids, id];
}

function CheckboxList({
  items,
  onToggle,
  selected
}: {
  items: Array<{ id: number; name: string }>;
  onToggle: (id: number) => void;
  selected: number[];
}) {
  if (items.length === 0) {
    return <div className="ctv-settings-row-help">None available.</div>;
  }
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      {items.map((item) => (
        <Checkbox
          checked={selected.includes(item.id)}
          key={item.id}
          label={item.name}
          onChange={() => onToggle(item.id)}
        />
      ))}
    </div>
  );
}
