<script lang="ts">
  import { Dialog } from "bits-ui";
  import { untrack } from "svelte";
  import { type MediaResult, requestMedia } from "$lib/api/client";
  import { downloadMedia } from "$lib/api/download";
  import { fetchMedia, getMessageMedia } from "$lib/api/endpoints";
  import { isPreviewable, type ViewerItem } from "$lib/api/media";
  import Button from "$lib/components/ui/Button.svelte";
  import ContextMenu from "$lib/components/ui/ContextMenu.svelte";
  import ContextMenuItem from "$lib/components/ui/ContextMenuItem.svelte";
  import Icon from "$lib/components/ui/Icon.svelte";
  import Spinner from "$lib/components/ui/Spinner.svelte";
  import { formatBytes, mediaKindLabel } from "$lib/format/media";
  import { poster } from "$lib/media/poster";
  import { shareUi } from "$lib/stores/shares.svelte";
  import { toasts } from "$lib/stores/toasts.svelte";

  interface Props {
    chatId: number;
    index: number;
    items: ViewerItem[];
    open: boolean;
  }

  let {
    open = $bindable(),
    index = $bindable(),
    chatId,
    items,
  }: Props = $props();

  let kind = $state("");
  let messageId = $state<number | null>(null);
  let currentMediaId = $state<number | null>(null);
  let fileName = $state<string | null>(null);
  let fileSize = $state<number | null>(null);
  let fileOnly = $state(false);
  let result = $state<MediaResult | null>(null);
  let loading = $state(false);
  let saving = $state(false);
  let token = 0;

  const mime = $derived(result?.state === "ready" ? (result.mime ?? "") : "");
  const isImage = $derived(mime.startsWith("image/") || kind === "photo");
  const isVideo = $derived(
    mime.startsWith("video/") || kind === "video" || kind === "video_note"
  );
  const isAudio = $derived(
    mime.startsWith("audio/") || kind === "voice" || kind === "audio"
  );
  const hasNav = $derived(items.length > 1);
  const canSave = $derived(
    currentMediaId !== null && (fileOnly || result?.state === "ready")
  );
  const title = $derived(fileName ?? mediaKindLabel(kind) ?? "Медиа");

  function revoke() {
    if (result?.state === "ready" && result.url) {
      URL.revokeObjectURL(result.url);
    }
  }

  async function load(item: ViewerItem) {
    revoke();
    loading = true;
    result = null;
    kind = item.kind;
    messageId = item.messageId;
    currentMediaId = null;
    fileName = item.fileName;
    fileSize = item.fileSize;
    fileOnly = false;
    const current = ++token;
    try {
      let mediaId = item.mediaId;
      let downloaded = item.downloaded;
      let name = item.fileName;
      let size = item.fileSize;
      let mimeType = item.mime;
      if (mediaId === null) {
        const meta = await getMessageMedia(chatId, item.messageId);
        mediaId = meta.id;
        downloaded = meta.downloaded;
        kind = meta.kind;
        name = meta.file_name;
        size = meta.file_size;
        mimeType = meta.mime;
      }
      const plainFile = downloaded && !isPreviewable(kind, mimeType);
      let next: MediaResult;
      if (plainFile) {
        next = { state: "ready", url: "", mime: mimeType };
      } else if (downloaded) {
        next = await requestMedia(mediaId);
      } else {
        next = { state: "not-downloaded" } as MediaResult;
      }
      if (current === token) {
        result = next;
        currentMediaId = mediaId;
        fileName = name;
        fileSize = size;
        fileOnly = plainFile;
      }
    } catch {
      if (current === token) {
        toasts.error("Failed to load media");
      }
    } finally {
      if (current === token) {
        loading = false;
      }
    }
  }

  async function queueFetch() {
    if (messageId === null) {
      return;
    }
    try {
      await fetchMedia(chatId, messageId);
      toasts.success("Скачивание поставлено в очередь");
    } catch {
      toasts.error("Не удалось поставить в очередь");
    }
  }

  async function save() {
    if (currentMediaId === null || saving) {
      return;
    }
    saving = true;
    try {
      await downloadMedia(currentMediaId);
    } catch {
      toasts.error("Не удалось скачать файл");
    } finally {
      saving = false;
    }
  }

  function share() {
    if (currentMediaId !== null) {
      shareUi.share({ kind: "media", mediaId: currentMediaId }, title);
    }
  }

  function step(delta: number) {
    const next = index + delta;
    if (next >= 0 && next < items.length) {
      index = next;
    }
  }

  function onkeydown(event: KeyboardEvent) {
    if (!(open && hasNav)) {
      return;
    }
    if (event.key === "ArrowLeft") {
      step(-1);
    } else if (event.key === "ArrowRight") {
      step(1);
    }
  }

  $effect(() => {
    const item = items[index];
    const isOpen = open;
    untrack(() => {
      if (isOpen && item) {
        load(item);
      }
    });
  });

  $effect(() => {
    if (!open) {
      untrack(() => {
        revoke();
        result = null;
      });
    }
  });
</script>

<svelte:window {onkeydown} />

<Dialog.Root bind:open>
  <Dialog.Portal>
    <Dialog.Overlay class="media-overlay" />
    <Dialog.Content class="media-content">
      <Dialog.Title class="media-title">{title}</Dialog.Title>
      <div class="media-actions">
        {#if canSave}
          <ContextMenu>
            {#snippet children({ props })}
              <button
                {...props}
                type="button"
                class="media-action"
                aria-label="Скачать файл"
                onclick={save}
              >
                <Icon name={saving ? "timer" : "download"} size="1.375rem" />
              </button>
            {/snippet}
            {#snippet menu()}
              <ContextMenuItem icon="download" onselect={save}>
                Скачать файл
              </ContextMenuItem>
              <ContextMenuItem icon="allow-share" onselect={share}>
                Доступ по ссылке
              </ContextMenuItem>
            {/snippet}
          </ContextMenu>
          <button
            type="button"
            class="media-action"
            aria-label="Доступ по ссылке"
            onclick={share}
          >
            <Icon name="allow-share" size="1.375rem" />
          </button>
        {/if}
        <Dialog.Close class="media-close" aria-label="Закрыть">
          <Icon name="close" size="1.5rem" />
        </Dialog.Close>
      </div>
      {#if hasNav}
        <span class="media-counter">{index + 1} / {items.length}</span>
      {/if}
      <div class="media-body">
        {#if loading}
          <Spinner color="white" />
        {:else if result?.state === "ready" && isImage}
          <img class="media-image" src={result.url} alt={kind}>
        {:else if result?.state === "ready" && isVideo}
          <!-- svelte-ignore a11y_media_has_caption -->
          <!-- biome-ignore lint/a11y/useMediaCaption: archived media has no captions -->
          <video
            class="media-video"
            src={result.url}
            controls
            playsinline
            preload="auto"
            use:poster
          ></video>
        {:else if result?.state === "ready" && isAudio}
          <!-- biome-ignore lint/a11y/useMediaCaption: archived media has no captions -->
          <audio src={result.url} controls></audio>
        {:else if result?.state === "ready"}
          <div class="media-file">
            <span class="file-glyph"><Icon name="document" size="2rem" /></span>
            <p class="file-name">{title}</p>
            {#if fileSize}
              <p class="file-size">{formatBytes(fileSize)}</p>
            {/if}
            <button class="media-download" type="button" onclick={save}>
              <Icon name="download" />
              Скачать файл
            </button>
          </div>
        {:else if result?.state === "not-downloaded"}
          <div class="media-message">
            <p>Файл ещё не скачан в архив.</p>
            <Button variant="primary" fluid onclick={queueFetch}>
              Скачать в архив
            </Button>
          </div>
        {:else if result?.state === "missing"}
          <p class="media-message">Файл не найден.</p>
        {/if}
      </div>
      {#if hasNav}
        <button
          class="media-nav prev"
          type="button"
          aria-label="Previous"
          disabled={index === 0}
          onclick={() => step(-1)}
        >
          <Icon name="previous" size="1.75rem" />
        </button>
        <button
          class="media-nav next"
          type="button"
          aria-label="Next"
          disabled={index === items.length - 1}
          onclick={() => step(1)}
        >
          <Icon name="next" size="1.75rem" />
        </button>
      {/if}
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

<style lang="scss">
  :global(.media-overlay) {
    position: fixed;
    inset: 0;
    z-index: var(--z-media-viewer);
    background-color: rgba(0, 0, 0, 0.9);
  }

  :global(.media-content) {
    position: fixed;
    inset: 0;
    z-index: var(--z-media-viewer);

    display: flex;
    flex-direction: column;

    outline: none;
  }

  :global(.media-title) {
    position: absolute;
    top: 1.25rem;
    left: 1.25rem;

    overflow: hidden;
    max-width: min(24rem, calc(100% - 14rem));
    margin: 0;

    font-size: 1rem;
    font-weight: var(--font-weight-medium);
    color: var(--color-white);
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .media-counter {
    position: absolute;
    top: 1rem;
    left: 50%;
    transform: translateX(-50%);

    font-size: 0.9375rem;
    color: var(--color-white);
  }

  :global(.media-close) {
    cursor: pointer;

    display: flex;
    align-items: center;
    justify-content: center;

    width: 2.5rem;
    height: 2.5rem;
    border: 0;
    border-radius: 50%;

    color: var(--color-white);
    background-color: rgba(255, 255, 255, 0.1);
  }

  .media-body {
    display: flex;
    flex: 1;
    align-items: center;
    justify-content: center;
    padding: 4rem 2rem;
  }

  .media-image,
  .media-video {
    max-width: 90vw;
    max-height: 85vh;
    border-radius: var(--border-radius-default);
  }

  .media-message {
    max-width: 22rem;
    color: var(--color-white);
    text-align: center;
  }

  .media-file {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.25rem;

    max-width: min(24rem, 90vw);
    color: var(--color-white);
    text-align: center;
  }

  .file-glyph {
    display: flex;
    align-items: center;
    justify-content: center;

    width: 4.5rem;
    height: 4.5rem;
    margin-bottom: 0.5rem;
    border-radius: 50%;

    background-color: rgba(255, 255, 255, 0.12);
  }

  .file-name {
    overflow-wrap: anywhere;
    margin: 0;
    font-size: 1rem;
  }

  .file-size {
    margin: 0 0 0.75rem;
    font-size: 0.8125rem;
    color: rgba(255, 255, 255, 0.6);
  }

  .media-download {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;

    padding: 0.625rem 1.25rem;
    border: 0;
    border-radius: 1.5rem;

    font-family: inherit;
    font-size: 0.9375rem;
    color: var(--color-white);

    cursor: pointer;
    background-color: rgba(255, 255, 255, 0.12);
  }

  .media-actions {
    position: absolute;
    top: 0.75rem;
    right: 1rem;

    display: flex;
    align-items: center;
    gap: 0.375rem;
    z-index: 1;
  }

  .media-action {
    cursor: pointer;

    display: flex;
    align-items: center;
    justify-content: center;

    width: 2.5rem;
    height: 2.5rem;
    border: 0;
    border-radius: 50%;

    color: var(--color-white);
    background-color: rgba(255, 255, 255, 0.1);
  }

  .media-nav {
    cursor: pointer;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);

    display: flex;
    align-items: center;
    justify-content: center;

    width: 3rem;
    height: 3rem;
    border: 0;
    border-radius: 50%;

    color: var(--color-white);
    background-color: rgba(255, 255, 255, 0.1);

    &:disabled {
      cursor: default;
      opacity: 0.3;
    }

    &.prev {
      left: 1.25rem;
    }

    &.next {
      right: 1.25rem;
    }
  }
</style>
