<script setup lang="ts">
import { renderSVG } from "uqr";
import {
  CHUNK_SIZE,
  DEFAULT_EXPIRY,
  DEFAULT_READS,
  type ExpiryChoice,
  type ReadChoice,
} from "~/../shared/types/drop";
import {
  encryptChunk,
  encryptManifest,
  formatBytes,
  generateKey,
  generateNoncePrefix,
  toBase64Url,
} from "~/utils/crypto";
const fileInput = ref<HTMLInputElement | null>(null);
const selectedFile = ref<File | null>(null);
const stage = ref<"idle" | "uploading" | "done">("idle");
const errorMessage = ref("");
const shareUrl = ref("");
const progress = ref(0);
const expiresIn = ref<ExpiryChoice>(DEFAULT_EXPIRY);
const maxReads = ref<ReadChoice>(DEFAULT_READS);
const isDragging = ref(false);
const busy = computed(() => stage.value === "uploading");
const qrSvg = computed(() =>
  shareUrl.value ? renderSVG(shareUrl.value, { ecc: "M", border: 2 }) : "",
);
const expiryOptions: ExpiryChoice[] = ["1h", "2h", "6h", "12h", "24h", "7d"];
const readOptions = [1, 5, 10, 20, 50, 100] as const;
useSeoMeta({
  title: "Send a private file",
  description: "Encrypted, resumable file sharing.",
});
function selectFile(file?: File) {
  if (!file) return;
  selectedFile.value = file;
  errorMessage.value = "";
  stage.value = "idle";
}
function onDrop(e: DragEvent) {
  isDragging.value = false;
  const files = e.dataTransfer?.files;
  if (files?.length !== 1) {
    errorMessage.value = "Please choose exactly one file.";
    return;
  }
  selectFile(files[0]);
}
function onInput(e: Event) {
  selectFile((e.target as HTMLInputElement).files?.[0]);
}
async function request(
  url: string,
  init: RequestInit,
  attempts = 5,
): Promise<Response> {
  for (let attempt = 0; ; attempt++) {
    try {
      const response = await fetch(url, init);
      if (
        response.ok ||
        ![408, 425, 429, 500, 502, 503, 504].includes(response.status) ||
        attempt >= attempts - 1
      )
        return response;
    } catch (error) {
      if (attempt >= attempts - 1) throw error;
    }
    await new Promise((resolve) =>
      setTimeout(
        resolve,
        Math.round(500 * 2 ** attempt * (0.8 + Math.random() * 0.4)),
      ),
    );
  }
}
async function createDrop() {
  const file = selectedFile.value;
  if (!file || busy.value) return;
  stage.value = "uploading";
  errorMessage.value = "";
  progress.value = 0;
  const key = generateKey();
  const noncePrefix = generateNoncePrefix();
  const chunkCount = Math.max(1, Math.ceil(file.size / CHUNK_SIZE));
  try {
    const reserve = await request("/api/drops", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        version: 2,
        chunkSize: CHUNK_SIZE,
        chunkCount,
        noncePrefix,
        expiresIn: expiresIn.value,
        maxReads: maxReads.value,
      }),
    });
    if (!reserve.ok) throw new Error("Sign in or retry the reservation.");
    const reserved = (await reserve.json()) as { id: string; access: string };
    for (let index = 0; index < chunkCount; index++) {
      const plain = new Uint8Array(
        await file
          .slice(
            index * CHUNK_SIZE,
            Math.min(file.size, (index + 1) * CHUNK_SIZE),
          )
          .arrayBuffer(),
      );
      const cipher = await encryptChunk(
        plain,
        key,
        reserved.id,
        noncePrefix,
        index,
        CHUNK_SIZE,
        chunkCount,
      );
      const result = await request(
        `/api/drops/${reserved.id}/chunks/${index}`,
        {
          method: "PUT",
          headers: { "content-type": "application/octet-stream" },
          body: cipher as unknown as BodyInit,
        },
      );
      if (!result.ok)
        throw new Error(`Chunk ${index + 1} could not be uploaded.`);
      progress.value = ((index + 1) / (chunkCount + 1)) * 100;
    }
    const manifest = await encryptManifest(
      {
        version: 2,
        name: file.name,
        type: file.type || "application/octet-stream",
        size: file.size,
        lastModified: file.lastModified,
        chunkSize: CHUNK_SIZE,
        chunkCount,
      },
      key,
      reserved.id,
      noncePrefix,
    );
    const final = await request(`/api/drops/${reserved.id}/finalize`, {
      method: "POST",
      headers: { "content-type": "application/octet-stream" },
      body: manifest as unknown as BodyInit,
    });
    if (!final.ok) throw new Error("The upload could not be finalized.");
    shareUrl.value = `${window.location.origin}/room/${reserved.id}#access=${reserved.access}&key=${key}`;
    stage.value = "done";
    progress.value = 100;
  } catch (error) {
    stage.value = "idle";
    errorMessage.value =
      error instanceof Error ? error.message : "Upload failed.";
  }
}
function reset() {
  selectedFile.value = null;
  shareUrl.value = "";
  stage.value = "idle";
  progress.value = 0;
  if (fileInput.value) fileInput.value.value = "";
}
async function copy() {
  await navigator.clipboard?.writeText(shareUrl.value);
}
</script>
<template>
  <div class="page">
    <section v-if="stage !== 'done'" class="hero">
      <h1>Send a file.<br />Keep it private.</h1>
      <p class="hero-copy">
        Files are encrypted in your browser in fixed 8 MiB chunks. Keep this tab
        open while uploading.
      </p>
    </section>
    <section v-if="stage !== 'done'" class="rail-layout">
      <div class="panel panel-main">
        <label
          v-if="!selectedFile"
          class="drop-zone"
          :class="{ 'is-dragging': isDragging }"
          @dragenter.prevent="isDragging = true"
          @dragover.prevent="isDragging = true"
          @dragleave.prevent="isDragging = false"
          @drop.prevent="onDrop"
          ><span class="drop-icon">↑</span>
          <h2>Drop your file here</h2>
          <p>or choose one from your device · no total file-size limit</p>
          <span class="button button-small">Choose a file</span
          ><input
            ref="fileInput"
            class="file-input absolute w-px h-px overflow-hidden [clip:rect(0_0_0_0)] [clip-path:inset(50%)] whitespace-nowrap"
            type="file"
            @change="onInput"
        /></label>
        <div v-else class="selected-file">
          <p class="eyebrow">Ready to encrypt</p>
          <div class="selected-card">
            <div class="selected-meta">
              <p class="selected-name">{{ selectedFile.name }}</p>
              <p class="selected-detail">
                {{ formatBytes(selectedFile.size) }} ·
                {{ selectedFile.type || "Unknown type" }}
              </p>
            </div>
            <button
              class="button button-secondary button-small"
              :disabled="busy"
              @click="reset"
            >
              Change
            </button>
          </div>
          <div class="grid grid-cols-2 gap-3 mt-5">
            <label
              >Expires after<select
                v-model="expiresIn"
                class="w-full p-2 mt-1 bg-[var(--bg)] border border-[var(--border)] rounded"
              >
                <option
                  v-for="option in expiryOptions"
                  :key="option"
                  :value="option"
                >
                  {{ option }}
                </option>
              </select></label
            ><label
              >Maximum reads<select
                v-model="maxReads"
                class="w-full p-2 mt-1 bg-[var(--bg)] border border-[var(--border)] rounded"
              >
                <option
                  v-for="option in readOptions"
                  :key="option"
                  :value="option"
                >
                  {{ option }}
                </option>
                <option :value="null">Unlimited</option>
              </select></label
            >
          </div>
          <button v-if="!busy" class="button mt-5" @click="createDrop">
            Encrypt & upload
          </button>
          <div v-else class="progress-wrap">
            <div class="progress-track">
              <div
                class="progress-bar"
                :style="{ width: `${progress}%`, animation: 'none' }"
              />
            </div>
            <p class="progress-label">
              Uploading encrypted chunks · {{ Math.round(progress) }}%
            </p>
          </div>
          <p class="privacy-note">
            The decryption key stays in the URL fragment and never reaches the
            server.
          </p>
        </div>
        <p v-if="errorMessage" class="error-message" role="alert">
          {{ errorMessage }}
        </p>
      </div>
      <aside class="panel panel-side">
        <h2 class="side-heading">Private by default</h2>
        <p class="hero-copy">
          Shared links are public capabilities. Uploading requires the
          configured shared passcode.
        </p>
        <p class="privacy-note">
          Expiry starts only after finalize. A download claim consumes one
          selected read.
        </p>
      </aside>
    </section>
    <section v-else class="download-page">
      <div class="hero">
        <p class="eyebrow">Your drop is ready</p>
        <h1>Send this private link.</h1>
        <p class="hero-copy">
          The selected expiry starts at finalize; the selected read policy
          applies to explicit downloads.
        </p>
      </div>
      <div class="panel download-card">
        <label class="share-label">PRIVATE DOWNLOAD LINK</label
        ><input class="share-value w-full" readonly :value="shareUrl" /><button
          class="button mt-4"
          @click="copy"
        >
          Copy link</button
        ><button class="button button-secondary mt-4 ml-2" @click="reset">
          Send another file
        </button>
        <div class="qr-frame mt-6" v-html="qrSvg" />
      </div>
    </section>
  </div>
</template>
