<script>
  import { Loader2 } from "lucide-svelte";

  let {
    checked = $bindable(false),
    label = "",
    description = "",
    /** @type {boolean} disable interaction and dim the toggle */
    disabled = false,
    /** @type {boolean} render a spinner in the thumb and block interaction */
    loading = $bindable(false),
    /** @type {((next: boolean) => void | Promise<void>) | undefined} optional async handler */
    onChange = undefined,
  } = $props();

  // Internal pending flag drives the spinner when an async onChange is in-flight.
  // Kept separate from the `loading` prop so callers can also force loading externally.
  let pending = $state(false);

  // Briefly true after an async onChange rejects, so screen readers hear that
  // the action failed rather than silently inferring it from the snap-back.
  let snapBack = $state(false);

  // Effective busy state covers both the externally-controlled prop and our internal pending flag.
  let busy = $derived(loading || pending);

  async function handleClick() {
    if (disabled || busy) return;

    const next = !checked;

    if (onChange) {
      let result;
      try {
        result = onChange(next);
      } catch (err) {
        // Synchronous throw: leave checked unchanged, surface the error.
        throw err;
      }

      // If the handler returned a thenable, treat it as async and snap back on rejection.
      if (result && typeof result.then === "function") {
        pending = true;
        try {
          await result;
          checked = next;
        } catch (_) {
          // Swallow; the toggle stays at its previous value. Flag the snap-back
          // briefly so assistive tech announces aria-invalid="true" and screen
          // reader users hear that the action failed.
          snapBack = true;
          setTimeout(() => { snapBack = false; }, 1500);
        } finally {
          pending = false;
        }
      } else {
        // Synchronous handler: flip immediately after invocation.
        checked = next;
      }
    } else {
      // No handler provided: preserve original bind:checked behaviour.
      checked = next;
    }
  }
</script>

<div class="flex items-start gap-3 py-2.5">
  <button
    class="relative mt-0.5 w-[38px] min-w-[38px] h-[22px] rounded-full flex-shrink-0
      {checked ? 'bg-accent shadow-[var(--shadow-accent-sm)]' : 'bg-bg-elevated'}
      {disabled && !busy ? 'opacity-50 cursor-not-allowed' : ''}
      {busy ? 'cursor-wait' : ''}"
    onclick={handleClick}
    role="switch"
    aria-checked={checked}
    aria-label={label}
    aria-disabled={disabled ? "true" : undefined}
    aria-busy={busy ? "true" : undefined}
    aria-invalid={snapBack ? "true" : undefined}
  >
    <span
      class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full shadow-sm flex items-center justify-center
        {busy ? '' : 'bg-white'}
        {checked ? 'translate-x-[16px]' : 'translate-x-0'}"
      style="transition: transform var(--duration-ui) cubic-bezier(0.2, 0.8, 0.2, 1)"
    >
      {#if busy}
        <Loader2 class="w-4 h-4 animate-spin text-accent" />
      {/if}
    </span>
  </button>
  <div class="flex-1 min-w-0">
    <p class="text-[13px] text-text leading-tight">{label}</p>
    {#if description}
      <p class="text-[12px] text-text-secondary mt-0.5 leading-snug">{description}</p>
    {/if}
  </div>
</div>
