<script lang="ts">
  import { tick, untrack } from 'svelte';
  import { enhance } from '$app/forms';
  import { resolve } from '$app/paths';

  import { Dropdown, DropdownItem, DropdownDivider, Tooltip } from 'flowbite-svelte';
  import Check from '@lucide/svelte/icons/check';
  import FlaskConical from '@lucide/svelte/icons/flask-conical';

  import { TaskGrade, getTaskGrade, type TaskResult } from '$lib/types/task';
  import { errorMessageStore } from '$lib/stores/error_message';
  import {
    fetchMyVote,
    submitVote,
    fetchMedianVote,
  } from '$features/votes/internal_clients/vote_grade';

  import { getTaskGradeLabel } from '$lib/utils/task';
  import { nonPendingGrades, resolveDisplayGrade } from '$features/votes/utils/grade_options';
  import { calcCenteredScrollTop } from '$features/votes/utils/grade_scroll_position';
  import {
    calcGradeDiff,
    getRelativeEvaluationLabel,
    getRelativeEvaluationJapaneseLabel,
    getRelativeEvaluationColorClass,
  } from '$features/votes/utils/relative_evaluation';
  import { SIGNUP_PAGE, LOGIN_PAGE, EDIT_PROFILE_PAGE } from '$lib/constants/navbar-links';

  import GradeLabel from '$lib/components/GradeLabel.svelte';
  import InputFieldWrapper from '$lib/components/InputFieldWrapper.svelte';
  import RelativeEvaluationBadge from '$features/votes/components/RelativeEvaluationBadge.svelte';

  interface Props {
    taskResult: TaskResult;
    isLoggedIn: boolean;
    // undefined means the prop was not passed — treat as verified to maintain backward compatibility.
    isAtCoderVerified?: boolean;
    estimatedGrade?: TaskGrade | null;
    defaultPadding?: number;
    defaultWidth?: number;
    reducedWidth?: number;
    showNeutralBadge?: boolean;
  }

  let {
    taskResult,
    isLoggedIn,
    isAtCoderVerified,
    estimatedGrade,
    defaultPadding = 1,
    defaultWidth = 10,
    reducedWidth = 8,
    showNeutralBadge = true,
  }: Props = $props();

  // 表示用のグレード（投票後に画面リロードなしで差し替えるためのローカル状態）
  // PENDING かつ estimatedGrade（集計済み中央値）があればそれを優先表示。
  // DBグレード付与済みの場合はそちらを優先。
  const initialGrade = resolveDisplayGrade(taskResult.grade, estimatedGrade);
  let displayGrade = $state<TaskGrade | string>(initialGrade);

  // Use task_id as a deterministic component ID to avoid SSR/hydration mismatches.
  const componentId = taskResult.task_id;

  // @ts-expect-error svelte-check TS2554: AppTypes declaration merging causes RouteId to resolve as string, requiring params. Runtime behavior is correct.
  const editProfileHref = `${resolve(EDIT_PROFILE_PAGE)}?tab=atcoder`;

  let selectedVoteGrade = $state<TaskGrade>();
  let showForm = $state(false);
  let formElement = $state<HTMLFormElement | undefined>(undefined);

  const isProvisional = $derived(
    taskResult.grade === TaskGrade.PENDING && displayGrade !== TaskGrade.PENDING,
  );

  // Track the latest median grade locally so the badge updates immediately after voting,
  // even for confirmed-grade tasks (where displayGrade does not change on vote).
  // Initialized from the prop; updated by the vote handler after fetchMedianVote.
  let latestMedianGrade = $state<TaskGrade | null>(untrack(() => estimatedGrade ?? null));

  // Relative evaluation badge label: shown only for confirmed grades with a known median.
  const relativeEvaluationLabel = $derived.by(() => {
    if (taskResult.grade === TaskGrade.PENDING || !latestMedianGrade) {
      return '';
    }
    return getRelativeEvaluationLabel(calcGradeDiff(taskResult.grade, latestMedianGrade));
  });

  // Whether to show the relative evaluation label (badge + sr-only text).
  // Hidden only when showNeutralBadge is false and the label is exactly '±0'.
  const shouldShowRelativeEvaluation = $derived(
    showNeutralBadge || relativeEvaluationLabel !== '±0',
  );

  let isOpening = $state(false);
  let votedGrade = $state<TaskGrade | null>(null);
  let voteAbortController: AbortController | null = null;

  let gradeListContainer = $state<HTMLElement | undefined>(undefined);

  $effect(() => {
    if (!gradeListContainer || taskResult.grade === TaskGrade.PENDING) {
      return;
    }

    const container = gradeListContainer;
    const targetIndex = nonPendingGrades.findIndex((grade) => grade === taskResult.grade);

    if (targetIndex < 0) {
      return;
    }

    // Defer via RAF: Flowbite's showPopover() is also RAF-scheduled (onintrostart),
    // and since it was registered first, it fires before this RAF so scrollHeight is non-zero.
    const rafId = requestAnimationFrame(() => {
      container.scrollTop = calcCenteredScrollTop(
        nonPendingGrades.length,
        targetIndex,
        container.scrollHeight,
        container.clientHeight,
      );
    });

    return () => cancelAnimationFrame(rafId);
  });

  async function onTriggerClick() {
    if (!isLoggedIn || isAtCoderVerified === false || isOpening) {
      return;
    }
    isOpening = true;
    try {
      votedGrade = await fetchMyVote(taskResult.task_id);
    } catch (err) {
      console.error(err);
    } finally {
      isOpening = false;
    }
  }

  async function handleClick(voteGrade: string): Promise<void> {
    // Cancel any in-flight vote request before starting a new one.
    voteAbortController?.abort();
    voteAbortController = new AbortController();
    selectedVoteGrade = getTaskGrade(voteGrade);
    showForm = true;
    // Wait for Svelte to render the form, then submit via the enhance directive.
    await tick();
    formElement?.requestSubmit();
  }

  type EnhanceForVote = {
    formData: FormData;
    action: URL;
    cancel: () => void;
  };

  const FAILED_TO_UPDATE_VOTE_STATUS = '投票状況の更新に失敗しました。もう一度試してください。';

  const handleSubmit = () => {
    return ({ formData, action, cancel }: EnhanceForVote) => {
      // Cancel the default form submission.
      cancel();

      const signal = voteAbortController?.signal;

      submitVote(action, formData, signal)
        .then(async (succeeded) => {
          if (signal?.aborted) {
            return; // Intentional abort — user selected a different grade
          }

          if (!succeeded) {
            throw new Error('vote failed');
          }

          // Reflect the voted grade in local state (to update the checkmark in the dropdown), even though the server response may later indicate a different median grade due to other voters or admin overrides.
          votedGrade = selectedVoteGrade ?? null;

          // Update the displayed grade to the latest median from the server, which may differ from the just-submitted vote due to other voters or admin overrides.
          const taskId = formData.get('taskId') as string;
          const medianGrade = await fetchMedianVote(taskId, signal);

          if (medianGrade !== null) {
            // Always update latestMedianGrade so the relative evaluation badge refreshes.
            latestMedianGrade = medianGrade;
            if (taskResult.grade === TaskGrade.PENDING) {
              displayGrade = medianGrade;
            }
          }
        })
        .catch((error) => {
          console.error('Failed to update submission status: ', error);
          errorMessageStore.setAndClearAfterTimeout(FAILED_TO_UPDATE_VOTE_STATUS, 10000);
        })
        .finally(() => {
          resetDropdown();
        });

      // Do not change anything in SvelteKit.
      return () => {};
    };
  };

  function resetDropdown(): void {
    showForm = false;
  }
</script>

<!-- Grade Icon（全問題で投票ドロップダウンを表示） -->
<div class="inline-flex items-center gap-1">
  {#if isProvisional}
    <FlaskConical
      id={`provisional-flask-${componentId}`}
      class="w-3.5 h-3.5 shrink-0 text-gray-400 dark:text-gray-500"
      aria-label="非公式グレード（投票に基づく）"
    />
    <Tooltip triggeredBy={`#provisional-flask-${componentId}`} placement="bottom">
      非公式グレード（投票に基づく）
    </Tooltip>
  {/if}

  <button
    id={`update-grade-dropdown-trigger-${componentId}`}
    class="relative group shrink-0 cursor-pointer"
    type="button"
    tabindex="0"
    onclick={() => onTriggerClick()}
  >
    <span class="sr-only">
      Voted grade: {getTaskGradeLabel(displayGrade)}{relativeEvaluationLabel &&
      shouldShowRelativeEvaluation
        ? `, relative evaluation: ${relativeEvaluationLabel}`
        : ''}{isProvisional ? ', provisional' : ''}
    </span>

    <GradeLabel taskGrade={displayGrade} {defaultPadding} {defaultWidth} {reducedWidth} />

    {#if taskResult.grade !== TaskGrade.PENDING && latestMedianGrade && shouldShowRelativeEvaluation}
      <RelativeEvaluationBadge
        officialGrade={taskResult.grade}
        medianGrade={latestMedianGrade}
        badgeId="relative-eval-{componentId}"
        showTooltip={false}
      />
    {/if}

    <!-- Overlay -->
    <span
      aria-hidden="true"
      class="pointer-events-none absolute inset-0 rounded-lg bg-gray-200 dark:bg-gray-700 mix-blend-multiply opacity-0 transition-opacity duration-150 group-hover:opacity-100"
    ></span>
  </button>
</div>

<!-- Dropdown Menu -->
{#if isLoggedIn && isAtCoderVerified !== false}
  <Dropdown
    triggeredBy={`#update-grade-dropdown-trigger-${componentId}`}
    simple
    class="w-44 z-50 border border-gray-200 dark:border-gray-100 py-1"
  >
    <DropdownItem href={resolve('/votes/[slug]', { slug: taskResult.task_id })} class="rounded-md"
      >詳細</DropdownItem
    >
    <DropdownDivider />
    <div bind:this={gradeListContainer} class="h-44 overflow-y-auto">
      {#each nonPendingGrades as grade (grade)}
        <DropdownItem onclick={() => handleClick(grade)} class="rounded-md">
          <div
            class="flex items-center w-full gap-2 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
          >
            <span class="flex-1">{getTaskGradeLabel(grade)}</span>
            {#if taskResult.grade !== TaskGrade.PENDING}
              {@const diff = calcGradeDiff(taskResult.grade, grade)}
              {@const relLabel = getRelativeEvaluationJapaneseLabel(diff)}
              {#if relLabel}
                <span class="w-16 text-right text-xs {getRelativeEvaluationColorClass(diff)}">
                  {relLabel}
                </span>
              {/if}
            {/if}
            {#if votedGrade === grade}
              <Check class="w-4 h-4 shrink-0 text-primary-600 dark:text-gray-300" strokeWidth={3} />
            {/if}
          </div>
        </DropdownItem>
      {/each}
    </div>
  </Dropdown>
{:else if isLoggedIn}
  <!-- Logged in but not AtCoder-verified: prompt user to complete verification -->
  <Dropdown
    triggeredBy={`#update-grade-dropdown-trigger-${componentId}`}
    simple
    class="w-48 z-50 border border-gray-200 dark:border-gray-100"
  >
    <DropdownItem
      href={editProfileHref}
      class="rounded-md text-sm text-yellow-700 dark:text-yellow-300"
    >
      AtCoder認証が必要です
    </DropdownItem>
  </Dropdown>
{:else}
  <Dropdown
    triggeredBy={`#update-grade-dropdown-trigger-${componentId}`}
    simple
    class="w-32 z-50 border border-gray-200 dark:border-gray-100"
  >
    <DropdownItem href={SIGNUP_PAGE} class="rounded-md">アカウント作成</DropdownItem>
    <DropdownDivider />
    <DropdownItem href={LOGIN_PAGE} class="rounded-md">ログイン</DropdownItem>
  </Dropdown>
{/if}

{#if showForm && selectedVoteGrade}
  {@render voteGradeForm(taskResult, selectedVoteGrade)}
{/if}

{#snippet voteGradeForm(selectedTaskResult: TaskResult, voteGrade: TaskGrade)}
  <form
    bind:this={formElement}
    method="POST"
    action="?/voteAbsoluteGrade"
    style="display:none;"
    use:enhance={handleSubmit()}
  >
    <!-- Task id -->
    <InputFieldWrapper
      inputFieldType="hidden"
      inputFieldName="taskId"
      inputValue={selectedTaskResult.task_id}
    />

    <!-- Submission status -->
    <InputFieldWrapper inputFieldType="hidden" inputFieldName="grade" inputValue={voteGrade} />

    <button type="submit">Submit</button>
  </form>
{/snippet}
