/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { Box, Chip, color, config, Icon, Icons, Text, toRem } from 'folds';
import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations';
import { MatrixEvent, Room, RoomEvent, PollEvent } from 'matrix-js-sdk';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMemberName } from '../../../utils/room';
import {
  ParsedPoll,
  PollResponse,
  PollTally,
  parsePollStart,
  parseResponseAnswerIds,
  resultsVisible,
  tallyResponses,
  validateSelections,
  winningAnswerIds,
} from '../../../utils/poll';

const EMPTY_TALLY: PollTally = {
  counts: new Map(),
  voters: new Map(),
  myVotes: new Set(),
  total: 0,
};

type PollState = { tally: PollTally; isEnded: boolean };

function setsEqual(a: Set<string>, b: Set<string>): boolean {
  if (a.size !== b.size) return false;
  for (const x of a) if (!b.has(x)) return false;
  return true;
}

function computePollState(
  mx: ReturnType<typeof useMatrixClient>,
  room: Room,
  eventId: string,
  parsed: ParsedPoll,
): PollState {
  const relations = room.getUnfilteredTimelineSet().relations;
  const myUserId = mx.getSafeUserId();
  const validIds = new Set(parsed.answers.map((a) => a.id));

  const getRels = (type: string): MatrixEvent[] =>
    relations.getChildEventsForEvent(eventId, 'm.reference', type as any)?.getRelations() ?? [];

  const endEvents = [...getRels('m.poll.end'), ...getRels('org.matrix.msc3381.poll.end')];
  const respEvents = [
    ...getRels('m.poll.response'),
    ...getRels('org.matrix.msc3381.poll.response'),
  ];

  // End state: prefer the SDK Poll model (validates ender = creator or redact PL,
  // refilters responses). Fall back to an end from the poll's own creator, which is
  // always valid, in case the model hasn't processed it yet.
  const poll = room.polls.get(eventId);
  let isEnded = poll?.isEnded ?? false;
  let endTs = poll?.endEventId ? room.findEventById(poll.endEventId)?.getTs() : undefined;
  if (!isEnded) {
    const creator = room.findEventById(eventId)?.getSender();
    for (const ev of endEvents) {
      if (ev.isRedacted()) continue;
      if (creator && ev.getSender() === creator) {
        isEnded = true;
        const t = ev.getTs();
        if (endTs === undefined || t < endTs) endTs = t;
      }
    }
  }

  const responses: PollResponse[] = [];
  for (const ev of respEvents) {
    if (ev.isRedacted()) continue;
    const sender = ev.getSender();
    if (!sender) continue;
    const ts = ev.getTs();
    // Only responses cast on or before the end event are valid.
    if (isEnded && endTs !== undefined && ts > endTs) continue;
    const answerIds = validateSelections(
      parseResponseAnswerIds(ev.getContent()),
      validIds,
      parsed.maxSelections,
    );
    responses.push({ sender, ts, answerIds });
  }

  return { tally: tallyResponses(responses, myUserId), isEnded };
}

export function PollContent({
  mEvent,
  room,
  canRedact,
}: {
  mEvent: MatrixEvent;
  room: Room;
  /** Whether the current user may redact in this room (gates the End-poll action). */
  canRedact: boolean;
}) {
  const mx = useMatrixClient();
  const roomId = room.roomId;
  const eventId = mEvent.getId();
  const senderId = mEvent.getSender();

  const parsed = useMemo(() => parsePollStart(mEvent.getContent()), [mEvent]);

  const [state, setState] = useState<PollState>(() => {
    if (!eventId || !parsed) return { tally: EMPTY_TALLY, isEnded: false };
    return computePollState(mx, room, eventId, parsed);
  });
  const [pending, setPending] = useState<Set<string> | null>(null);
  const [confirmEnd, setConfirmEnd] = useState(false);
  const [ending, setEnding] = useState(false);
  const [showVoters, setShowVoters] = useState(false);

  const refresh = useCallback(() => {
    if (!eventId || !parsed) return;
    const next = computePollState(mx, room, eventId, parsed);
    setState(next);
    // Drop the optimistic selection only once our own vote is actually reflected,
    // so an unrelated refresh (another user voting) can't revert our pending click.
    setPending((prev) => (prev !== null && setsEqual(prev, next.tally.myVotes) ? null : prev));
  }, [mx, room, eventId, parsed]);

  useEffect(() => {
    if (!eventId || !parsed) return undefined;
    const relations = room.getUnfilteredTimelineSet().relations;
    const relObjs = [
      relations.getChildEventsForEvent(eventId, 'm.reference', 'm.poll.response' as any),
      relations.getChildEventsForEvent(
        eventId,
        'm.reference',
        'org.matrix.msc3381.poll.response' as any,
      ),
      relations.getChildEventsForEvent(eventId, 'm.reference', 'm.poll.end' as any),
      relations.getChildEventsForEvent(
        eventId,
        'm.reference',
        'org.matrix.msc3381.poll.end' as any,
      ),
    ];
    relObjs.forEach((r) => {
      r?.on(RelationsEvent.Add, refresh);
      r?.on(RelationsEvent.Remove, refresh);
      r?.on(RelationsEvent.Redaction, refresh);
    });

    // The relations object for the first response/end may not exist yet at mount, and
    // redactions (m.room.redaction) don't flow through the relations listeners for a
    // post-mount response — the room timeline catches those.
    const onTimeline = (ev: MatrixEvent) => {
      const type = ev.getType();
      if (type === 'm.room.redaction') {
        refresh();
        return;
      }
      const relatesTo = ev.getContent()['m.relates_to'] as { event_id?: string } | undefined;
      if (
        (type === 'm.poll.response' ||
          type === 'org.matrix.msc3381.poll.response' ||
          type === 'm.poll.end' ||
          type === 'org.matrix.msc3381.poll.end') &&
        relatesTo?.event_id === eventId
      ) {
        refresh();
      }
    };
    room.on(RoomEvent.Timeline, onTimeline);

    const poll = room.polls.get(eventId);
    poll?.on(PollEvent.Update, refresh);
    poll?.on(PollEvent.Responses, refresh);
    poll?.on(PollEvent.End, refresh);
    room.on(PollEvent.New, refresh);

    return () => {
      relObjs.forEach((r) => {
        r?.off(RelationsEvent.Add, refresh);
        r?.off(RelationsEvent.Remove, refresh);
        r?.off(RelationsEvent.Redaction, refresh);
      });
      room.off(RoomEvent.Timeline, onTimeline);
      poll?.off(PollEvent.Update, refresh);
      poll?.off(PollEvent.Responses, refresh);
      poll?.off(PollEvent.End, refresh);
      room.off(PollEvent.New, refresh);
    };
  }, [room, eventId, parsed, refresh]);

  if (!parsed) {
    return (
      <Text priority="300">
        <i>Poll (unreadable format)</i>
      </Text>
    );
  }

  const { tally, isEnded } = state;
  const { isUndisclosed, maxSelections, question, answers } = parsed;
  const isMultiple = maxSelections > 1;

  const showResults = resultsVisible(isUndisclosed, isEnded);
  const canVote = !!roomId && !!eventId && !isEnded;
  const canEnd = !!eventId && !isEnded && (senderId === mx.getUserId() || canRedact);

  const myVotes = pending ?? tally.myVotes;
  const { counts, total, voters } = tally;
  const winners = isEnded && showResults ? new Set(winningAnswerIds(counts)) : new Set<string>();
  // Voter identities are shown under the same rule as the counts: disclosed polls
  // reveal them live, undisclosed polls only once ended.
  const canShowVoters = showResults && total > 0;

  const handleVote = (answerId: string) => {
    if (!roomId || !eventId || !canVote) return;

    const next = new Set(myVotes);
    if (next.has(answerId)) {
      next.delete(answerId);
    } else if (isMultiple) {
      if (next.size >= maxSelections) return; // enforce max_selections
      next.add(answerId);
    } else {
      next.clear();
      next.add(answerId);
    }

    setPending(next);
    // Send the STABLE m.poll.response (matches Lotus's stable m.poll.start; the reader
    // accepts both namespaces).
    mx.sendEvent(roomId, 'm.poll.response' as any, {
      'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
      'm.selections': Array.from(next),
    }).catch(() => setPending(null));
  };

  const handleEndPoll = () => {
    if (!roomId || !eventId || ending) return;
    setEnding(true);
    mx.sendEvent(roomId, 'm.poll.end' as any, {
      'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
      'm.poll.end': {},
      'm.text': 'The poll has ended.',
    })
      .then(() => {
        setConfirmEnd(false);
        setEnding(false);
      })
      .catch(() => setEnding(false));
  };

  // Radiogroup keyboard model (single-choice only): arrows move focus + selection.
  const handleRadioKeyDown = (evt: KeyboardEvent<HTMLDivElement>) => {
    if (isMultiple || !canVote) return;
    const nav = ['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft', 'Home', 'End'];
    if (!nav.includes(evt.key)) return;
    evt.preventDefault();
    const buttons = Array.from(
      evt.currentTarget.querySelectorAll<HTMLButtonElement>('[data-poll-answer]'),
    );
    if (buttons.length === 0) return;
    const current = buttons.findIndex((b) => b === document.activeElement);
    let idx = current < 0 ? 0 : current;
    if (evt.key === 'ArrowDown' || evt.key === 'ArrowRight') idx = (idx + 1) % buttons.length;
    else if (evt.key === 'ArrowUp' || evt.key === 'ArrowLeft')
      idx = (idx - 1 + buttons.length) % buttons.length;
    else if (evt.key === 'Home') idx = 0;
    else if (evt.key === 'End') idx = buttons.length - 1;
    buttons[idx]?.focus();
    handleVote(answers[idx].id);
  };

  const headerLabel = isEnded
    ? 'Poll closed · Final results'
    : `Poll · ${isMultiple ? 'Multiple choice' : 'Single choice'}`;

  const footerNote = (() => {
    if (isUndisclosed && !isEnded) {
      return canVote
        ? 'Voting open · Results hidden until the poll ends'
        : 'Results hidden until the poll ends';
    }
    const votesPart = total > 0 ? `${total} vote${total === 1 ? '' : 's'}` : 'No votes yet';
    if (isEnded) return `${votesPart} · Poll closed`;
    if (!canVote) return votesPart;
    if (isMultiple) return `${votesPart} · Select up to ${maxSelections}`;
    return `${votesPart} · ${myVotes.size > 0 ? 'Click to change' : 'Click to vote'}`;
  })();

  return (
    <Box
      data-poll-content
      direction="Column"
      gap="200"
      style={{ maxWidth: '340px', paddingTop: '2px', paddingBottom: '4px' }}
    >
      <Text
        as="div"
        size="T200"
        priority="300"
        data-poll-content-label
        style={{
          fontWeight: 700,
          letterSpacing: '0.12em',
          textTransform: 'uppercase',
          marginBottom: config.space.S100,
        }}
      >
        <span aria-hidden>◉ </span>
        {headerLabel}
      </Text>
      <Text size="T400" style={{ fontWeight: 600 }}>
        {question}
      </Text>
      <Box
        direction="Column"
        gap="100"
        style={{ marginTop: '2px' }}
        role={isMultiple ? 'group' : 'radiogroup'}
        aria-label={question}
        onKeyDown={handleRadioKeyDown}
      >
        {answers.map((answer, i) => {
          const id = answer.id;
          const text = answer.text;
          const selected = myVotes.has(id);
          const voteCount = counts.get(id) ?? 0;
          const pct = showResults && total > 0 ? Math.round((voteCount / total) * 100) : 0;
          const isWinner = winners.has(id);
          // Roving tabindex for the single-choice radiogroup; checkboxes stay tabbable.
          const tabIndex = isMultiple ? 0 : selected || (myVotes.size === 0 && i === 0) ? 0 : -1;
          return (
            <React.Fragment key={id}>
              <button
                type="button"
                data-poll-answer
                data-selected={selected}
                role={isMultiple ? 'checkbox' : 'radio'}
                aria-checked={selected}
                aria-disabled={!canVote}
                aria-label={isWinner ? `${text}, winning answer` : undefined}
                aria-describedby={
                  showVoters && canShowVoters && (voters.get(id)?.length ?? 0) > 0
                    ? `poll-voters-${eventId}-${id}`
                    : undefined
                }
                tabIndex={tabIndex}
                onClick={canVote ? () => handleVote(id) : undefined}
                style={{
                  padding: `${config.space.S200} ${config.space.S300}`,
                  borderRadius: config.radii.R300,
                  background: selected ? color.Primary.Container : color.SurfaceVariant.Container,
                  border: `${config.borderWidth.B300} solid ${
                    isWinner
                      ? color.Success.Main
                      : selected
                        ? color.Primary.Main
                        : color.SurfaceVariant.ContainerLine
                  }`,
                  lineHeight: 1.4,
                  textAlign: 'left',
                  cursor: canVote ? 'pointer' : 'default',
                  color: 'inherit',
                  display: 'flex',
                  flexDirection: 'column',
                  gap: config.space.S100,
                  width: '100%',
                  position: 'relative',
                  overflow: 'hidden',
                  transition: 'border-color 0.15s, background 0.15s',
                }}
              >
                {showResults && total > 0 && (
                  <span
                    aria-hidden
                    style={{
                      position: 'absolute',
                      inset: 0,
                      right: 'auto',
                      width: `${pct}%`,
                      background: selected
                        ? color.Primary.ContainerActive
                        : color.SurfaceVariant.ContainerActive,
                      pointerEvents: 'none',
                      transition: 'width 0.3s ease',
                    }}
                  />
                )}
                <span
                  style={{
                    display: 'flex',
                    alignItems: 'center',
                    gap: config.space.S200,
                    position: 'relative',
                  }}
                >
                  <span
                    aria-hidden
                    style={{
                      flexShrink: 0,
                      width: toRem(14),
                      height: toRem(14),
                      border: `${config.borderWidth.B300} solid ${
                        selected ? color.Primary.Main : color.Primary.ContainerLine
                      }`,
                      borderRadius: isMultiple ? config.radii.R300 : config.radii.Pill,
                      background: selected ? color.Primary.Main : 'transparent',
                      display: 'flex',
                      alignItems: 'center',
                      justifyContent: 'center',
                      color: color.Primary.OnMain,
                      transition: 'all 0.15s',
                    }}
                  >
                    {selected ? <Icon size="50" src={Icons.Check} /> : null}
                  </span>
                  <Text as="span" size="T300" style={{ flexGrow: 1 }}>
                    {text}
                  </Text>
                  {isWinner && (
                    <Icon
                      size="50"
                      src={Icons.Check}
                      style={{ flexShrink: 0, color: color.Success.Main }}
                    />
                  )}
                  {showResults && total > 0 && (
                    <Text as="span" size="T200" priority="300" style={{ flexShrink: 0 }}>
                      {pct}%
                    </Text>
                  )}
                </span>
              </button>
              {showVoters && canShowVoters && (voters.get(id)?.length ?? 0) > 0 && (
                <Text
                  id={`poll-voters-${eventId}-${id}`}
                  size="T200"
                  priority="300"
                  style={{ padding: `0 ${config.space.S300} ${config.space.S100}` }}
                >
                  {`Voted by ${(voters.get(id) ?? []).map((s) => getMemberName(room, s)).join(', ')}`}
                </Text>
              )}
            </React.Fragment>
          );
        })}
      </Box>
      {canShowVoters && (
        <Box>
          <Chip
            variant={showVoters ? 'Primary' : 'SurfaceVariant'}
            radii="Pill"
            aria-pressed={showVoters}
            onClick={() => setShowVoters((v) => !v)}
            before={<Icon size="50" src={Icons.User} />}
          >
            <Text size="T200">{showVoters ? 'Hide voters' : 'Show who voted'}</Text>
          </Chip>
        </Box>
      )}
      <Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
        <Text size="T200" priority="300" style={{ minWidth: 0 }}>
          <i>{footerNote}</i>
        </Text>
        {canEnd &&
          (confirmEnd ? (
            <Box gap="100" shrink="No" alignItems="Center">
              <Chip
                variant="Critical"
                radii="Pill"
                aria-disabled={ending}
                onClick={ending ? undefined : handleEndPoll}
              >
                <Text size="T200">{ending ? 'Ending…' : 'End poll'}</Text>
              </Chip>
              <Chip variant="Secondary" radii="Pill" onClick={() => setConfirmEnd(false)}>
                <Text size="T200">Cancel</Text>
              </Chip>
            </Box>
          ) : (
            <Chip
              variant="SurfaceVariant"
              radii="Pill"
              onClick={() => setConfirmEnd(true)}
              before={<Icon size="50" src={Icons.Cross} />}
            >
              <Text size="T200">End poll</Text>
            </Chip>
          ))}
      </Box>
    </Box>
  );
}
