// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import React, { memo, useState, useEffect, useRef } from 'react';
import { Checkbox } from 'radix-ui';
import { AnimatePresence, motion } from 'motion/react';
import { type TailwindStyles, tw } from '../../../axo/tw.dom.tsx';
import { AxoButton } from '../../../axo/AxoButton.dom.tsx';
import { AxoSymbol } from '../../../axo/AxoSymbol.dom.tsx';
import type { DirectionType } from '../Message.dom.tsx';
import type { PollWithResolvedVotersType } from '../../../state/selectors/message.preload.ts';
import type { LocalizerType } from '../../../types/Util.std.ts';
import { PollVotesModal } from './PollVotesModal.dom.tsx';
import { SpinnerV2 } from '../../SpinnerV2.dom.tsx';
import { usePrevious } from '../../../hooks/usePrevious.std.ts';
import { UserText } from '../../UserText.dom.tsx';

function VotedCheckmark({
  isIncoming,
  i18n,
}: {
  isIncoming: boolean;
  i18n: LocalizerType;
}): React.JSX.Element {
  return (
    <div
      className={tw(
        'size-4 rounded-full',
        'flex items-center justify-center',
        'text-[10px]',
        isIncoming
          ? 'bg-color-fill-primary text-label-primary-on-color'
          : 'bg-label-primary-on-color text-color-fill-primary'
      )}
    >
      <AxoSymbol.InlineGlyph
        symbol="check"
        label={i18n('icu:PollMessage--YouVoted')}
      />
    </div>
  );
}

type PollCheckboxProps = {
  checked: boolean;
  onCheckedChange: (nextChecked: boolean) => void;
  isIncoming: boolean;
  isPending: boolean;
};

const PollCheckbox = memo((props: PollCheckboxProps) => {
  const { checked, isIncoming, isPending, onCheckedChange } = props;

  let bgColor: TailwindStyles;
  let borderColor: TailwindStyles;
  let strokeColor: TailwindStyles | undefined;
  let checkmarkColor: TailwindStyles | undefined;

  if (isPending || !checked) {
    bgColor = tw('bg-transparent');
    borderColor = isIncoming
      ? tw('border-label-placeholder')
      : tw('border-label-primary-on-color');
    strokeColor = isIncoming
      ? tw('stroke-label-placeholder')
      : tw('stroke-label-primary-on-color');
    checkmarkColor = isIncoming
      ? tw('text-label-placeholder')
      : tw('text-label-primary-on-color');
  } else {
    bgColor = isIncoming
      ? tw('bg-color-fill-primary')
      : tw('bg-label-primary-on-color');
    borderColor = isIncoming
      ? tw('border-color-fill-primary')
      : tw('border-label-primary-on-color');
    strokeColor = isIncoming
      ? tw('stroke-color-fill-primary')
      : tw('stroke-label-primary-on-color');
    checkmarkColor = isIncoming
      ? tw('text-label-primary-on-color')
      : tw('text-color-fill-primary');
  }

  return (
    <>
      <AnimatePresence>
        {isPending && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.25 }}
            className={tw('pointer-events-none absolute')}
          >
            <SpinnerV2
              value="indeterminate"
              size={24}
              strokeWidth={1.5}
              marginRatio={1}
              variant={{
                bg: tw('stroke-none'),
                fg: strokeColor,
              }}
            />
          </motion.div>
        )}
      </AnimatePresence>
      <Checkbox.Root
        checked={checked}
        onCheckedChange={onCheckedChange}
        className={tw(
          'flex size-6 items-center justify-center rounded-full',
          isPending ? '' : 'border-[1.5px]',
          'outline-0 outline-border-focused focused:outline-[2.5px]',
          'overflow-hidden',
          'transition-colors duration-250',
          bgColor,
          borderColor
        )}
      >
        <Checkbox.Indicator forceMount asChild>
          <motion.div
            initial={false}
            animate={{ opacity: checked ? 1 : 0 }}
            transition={{ duration: 0.25 }}
            className={tw(
              checkmarkColor,
              'flex items-center justify-center',
              // Animate color so it doesn't instantly switch to grey during fade-out
              'transition-colors duration-250'
            )}
          >
            <AxoSymbol.Icon symbol="check" size={16} label={null} />
          </motion.div>
        </Checkbox.Indicator>
      </Checkbox.Root>
    </>
  );
});

PollCheckbox.displayName = 'PollCheckbox';

export type PollMessageContentsProps = {
  poll: PollWithResolvedVotersType;
  direction: DirectionType;
  i18n: LocalizerType;
  messageId: string;
  canEndPoll?: boolean;
  canSendPollVote: boolean;
  sendPollVote: (params: {
    messageId: string;
    optionIndexes: ReadonlyArray<number>;
  }) => void;
  endPoll: (messageId: string) => void;
};

const DELAY_BEFORE_SHOWING_PENDING_ANIMATION = 500;
export function PollMessageContents({
  poll,
  direction,
  i18n,
  messageId,
  canEndPoll,
  canSendPollVote,
  sendPollVote,
  endPoll,
}: PollMessageContentsProps): React.JSX.Element {
  const [showVotesModal, setShowVotesModal] = useState(false);
  const [isPending, setIsPending] = useState(false);

  const hasPendingVotes = poll.pendingVoteDiff && poll.pendingVoteDiff.size > 0;
  const hadPendingVotesInLastRender = usePrevious(hasPendingVotes, undefined);

  const pendingCheckTimer = useRef<NodeJS.Timeout | null>(null);
  const isIncoming = direction === 'incoming';

  const { totalNumVotes: totalVotes, uniqueVoters } = poll;
  // Handle pending vote state changes
  useEffect(() => {
    if (!hasPendingVotes) {
      // Vote completed, clear pending state
      setIsPending(false);
      clearTimeout(pendingCheckTimer.current ?? undefined);
      pendingCheckTimer.current = null;
    } else if (!hadPendingVotesInLastRender) {
      pendingCheckTimer.current = setTimeout(() => {
        setIsPending(true);
      }, DELAY_BEFORE_SHOWING_PENDING_ANIMATION);
    }
  }, [hadPendingVotesInLastRender, hasPendingVotes]);

  useEffect(() => {
    return () => {
      clearTimeout(pendingCheckTimer.current ?? undefined);
    };
  }, []);

  let pollStatusText: string;
  if (poll.terminatedAt) {
    pollStatusText = i18n('icu:PollMessage--FinalResults');
  } else if (poll.allowMultiple) {
    pollStatusText = i18n('icu:PollMessage--SelectMultiple');
  } else {
    pollStatusText = i18n('icu:PollMessage--SelectOne');
  }

  function handlePollOptionClicked(index: number, nextChecked: boolean): void {
    const existingSelections = Array.from(
      poll.votesByOption
        .entries()
        .filter(([_, voters]) => (voters ?? []).some(v => v.isMe))
        .map(([optionIndex]) => optionIndex)
    );
    const optionIndexes = new Set<number>(existingSelections);

    if (poll.pendingVoteDiff) {
      for (const [idx, pendingVoteOrUnvote] of poll.pendingVoteDiff.entries()) {
        if (pendingVoteOrUnvote === 'PENDING_VOTE') {
          optionIndexes.add(idx);
        } else if (pendingVoteOrUnvote === 'PENDING_UNVOTE') {
          optionIndexes.delete(idx);
        }
      }
    }

    if (nextChecked) {
      if (!poll.allowMultiple) {
        // Single-select: clear existing selections first
        optionIndexes.clear();
      }
      optionIndexes.add(index);
    } else {
      // Removing a selection - same for both modes
      optionIndexes.delete(index);
    }

    sendPollVote({
      messageId,
      optionIndexes: [...optionIndexes],
    });
  }

  return (
    <div
      className={tw(
        'text-start wrap-break-word whitespace-pre-wrap',
        'type-body-large',
        isIncoming ? 'text-label-primary' : 'text-label-primary-on-color',
        'w-[275px] max-w-full',
        'mt-1'
      )}
    >
      <div className={tw('mb-1 font-semibold')}>
        <UserText text={poll.question} />
      </div>

      <div
        className={tw(
          'mb-4 type-body-medium font-medium',
          isIncoming ? 'text-label-secondary' : 'text-label-secondary-on-color'
        )}
      >
        {pollStatusText}
      </div>

      {/* Poll Options */}
      <div className={tw('space-y-3')}>
        {poll.options.map((option, index) => {
          const pollVoteEntries = poll.votesByOption.get(index);
          const optionVotes = pollVoteEntries?.length ?? 0;
          const percentage =
            uniqueVoters > 0 ? (optionVotes / uniqueVoters) * 100 : 0;

          const weVotedForThis = (pollVoteEntries ?? []).some(v => v.isMe);
          const pendingVoteOrUnvote = poll.pendingVoteDiff?.get(index);
          const isVotePending = isPending && pendingVoteOrUnvote != null;

          const shouldShowCheckmark = isVotePending
            ? pendingVoteOrUnvote === 'PENDING_VOTE'
            : weVotedForThis;

          return (
            // oxlint-disable-next-line react/no-array-index-key
            <div key={`option-${index}`} className={tw('flex gap-3')}>
              {canSendPollVote && poll.terminatedAt == null && (
                // 3px offset: type-body-large has 14px font-size and 20px line-height,
                // creating 3px space above text. This aligns checkbox with text baseline.
                <div className={tw('mt-[3px] self-start')}>
                  <PollCheckbox
                    checked={shouldShowCheckmark}
                    onCheckedChange={next =>
                      handlePollOptionClicked(index, next)
                    }
                    isIncoming={isIncoming}
                    isPending={isVotePending}
                  />
                </div>
              )}

              <div className={tw('flex min-w-0 flex-1 flex-col gap-1')}>
                <div className={tw('flex items-start justify-between gap-3')}>
                  <span
                    className={tw('min-w-0 type-body-large wrap-break-word')}
                  >
                    <UserText text={option} />
                  </span>
                  <div
                    className={tw(
                      'flex shrink-0 items-center gap-1',
                      'transition-opacity duration-250',
                      totalVotes > 0 ? 'opacity-100' : 'invisible opacity-0'
                    )}
                  >
                    {poll.terminatedAt != null && weVotedForThis && (
                      <VotedCheckmark isIncoming={isIncoming} i18n={i18n} />
                    )}
                    <span
                      className={tw(
                        'type-body-medium',
                        isIncoming
                          ? 'text-label-secondary'
                          : 'text-label-secondary-on-color'
                      )}
                      data-testid={`poll-option-${index}-votes-${optionVotes}`}
                    >
                      {optionVotes}
                    </span>
                  </div>
                </div>

                <div
                  className={tw(
                    'relative h-2 w-full overflow-hidden rounded-full'
                  )}
                >
                  <div
                    className={tw(
                      'absolute inset-0',
                      isIncoming
                        ? 'bg-fill-secondary'
                        : 'bg-message-fill-outgoing-tertiary'
                    )}
                  />
                  <div
                    className={tw(
                      'absolute inset-y-0 inset-s-0 rounded-s-full',
                      'transition-[width] duration-250 motion-reduce:transition-none',
                      isIncoming
                        ? 'bg-color-fill-primary'
                        : 'bg-label-primary-on-color'
                    )}
                    style={{ width: `${percentage}%` }}
                  />
                </div>
              </div>
            </div>
          );
        })}
      </div>

      <div className={tw('mt-4 flex h-10 items-center justify-center')}>
        <AnimatePresence mode="wait" initial={false}>
          {totalVotes > 0 ? (
            <motion.div
              key="view-votes"
              initial={{ opacity: 0 }}
              animate={{
                opacity: 1,
                transition: { duration: 0.25, ease: 'easeOut' },
              }}
              exit={{
                opacity: 0,
                transition: { duration: 0.25, ease: 'easeIn' },
              }}
              className={tw('scheme-light')}
            >
              <AxoButton.Root
                size="md"
                variant="floating-secondary"
                onClick={() => setShowVotesModal(true)}
              >
                {i18n('icu:PollMessage__ViewVotesButton')}
              </AxoButton.Root>
            </motion.div>
          ) : (
            <motion.div
              key="no-votes"
              className={tw(
                'type-body-medium',
                isIncoming
                  ? 'text-label-primary'
                  : 'text-label-primary-on-color'
              )}
            >
              {i18n('icu:PollVotesModal__noVotes')}
            </motion.div>
          )}
        </AnimatePresence>
      </div>

      {showVotesModal && (
        <PollVotesModal
          i18n={i18n}
          poll={poll}
          onClose={() => setShowVotesModal(false)}
          endPoll={endPoll}
          canEndPoll={canEndPoll}
          messageId={messageId}
        />
      )}
    </div>
  );
}
