<script>
  import { onDestroy, onMount } from "svelte";

  let selectedFile = $state(null);
  let loading = $state(false);
  let errorMessage = $state("");

  let maxUploadSizeBytes = $state(0);
  let defaultTTL = $state("15m");
  let uploadAllowed = $state(true);
  let configError = $state("");

  let routeMode = $state("home");
  let scratchID = $state("");

  let viewLoading = $state(false);
  let viewError = $state("");
  let viewMeta = $state(null);
  let viewContent = $state("");
  let viewIsBinary = $state(false);
  let viewIsImage = $state(false);
  let viewIsVideo = $state(false);
  let copyStatusMessage = $state("");
  let copyStatusKind = $state("");
  let refreshTriggered = $state(false);
  let nowMs = $state(Date.now());

  let nowTicker = null;

  const maxUploadSizeDisplay = $derived(formatBytes(maxUploadSizeBytes, true));
  const defaultTTLDisplay = $derived(formatTTLDisplay(defaultTTL));

  onMount(async () => {
    nowTicker = window.setInterval(() => {
      nowMs = Date.now();
    }, 1000);

    detectRoute();
    await loadUIConfig();
    if (routeMode === "view") {
      await loadScratchView();
    }
  });

  onDestroy(() => {
    if (nowTicker !== null) {
      window.clearInterval(nowTicker);
      nowTicker = null;
    }
  });

  function detectRoute() {
    const path = window.location.pathname;
    if (path === "/" || path === "") {
      routeMode = "home";
      scratchID = "";
      return;
    }

    if (path === "/u" || path === "/u/") {
      routeMode = "upload";
      scratchID = "";
      return;
    }

    if (!path.startsWith("/s/")) {
      routeMode = "notfound";
      scratchID = "";
      return;
    }

    const rawID = path.slice("/s/".length).trim();
    if (rawID.length === 0) {
      routeMode = "notfound";
      scratchID = "";
      return;
    }

    routeMode = "view";
    scratchID = decodeURIComponent(rawID);
  }

  async function loadUIConfig() {
    configError = "";

    try {
      const response = await fetch("/api/config");
      if (!response.ok) {
        throw new Error(`failed to load UI config (${response.status})`);
      }
      const data = await response.json();
      const max = Number.parseInt(String(data.max_upload_size_bytes ?? "0"), 10);
      maxUploadSizeBytes = Number.isFinite(max) ? max : 0;
      uploadAllowed = Boolean(data.upload_allowed);
      defaultTTL = String(data.default_ttl ?? "").trim() || "15m";
    } catch (err) {
      configError = err instanceof Error ? err.message : String(err);
      maxUploadSizeBytes = 0;
      uploadAllowed = true;
      defaultTTL = "15m";
    }
  }

  async function loadScratchView() {
    if (!scratchID) {
      viewError = "Scratch ID is missing.";
      return;
    }

    viewLoading = true;
    viewError = "";
    viewMeta = null;
    viewContent = "";
    viewIsBinary = false;
    viewIsImage = false;
    viewIsVideo = false;
    copyStatusMessage = "";
    copyStatusKind = "";
    refreshTriggered = false;

    try {
      const metaResponse = await fetch(`/api/scratch/${encodeURIComponent(scratchID)}`);
      if (!metaResponse.ok) {
        if (metaResponse.status === 404 || metaResponse.status === 410) {
          routeMode = "expired";
          viewError = "This scratch is no longer available.";
          return;
        }
        const msg = await metaResponse.text();
        throw new Error(msg || `Failed to load scratch metadata (${metaResponse.status}).`);
      }
      viewMeta = await metaResponse.json();

      const rawResponse = await fetch(`/api/raw/${encodeURIComponent(scratchID)}`);
      if (!rawResponse.ok) {
        const msg = await rawResponse.text();
        throw new Error(msg || `Failed to load scratch content (${rawResponse.status}).`);
      }

      const contentType = rawResponse.headers.get("content-type") ?? "";
      if (isLikelyTextContentType(contentType)) {
        viewContent = await rawResponse.text();
        viewIsBinary = false;
        viewIsImage = false;
        viewIsVideo = false;
      } else if (isBrowserImageContentType(contentType)) {
        viewIsImage = true;
        viewIsBinary = false;
        viewIsVideo = false;
      } else if (isBrowserVideoContentType(contentType)) {
        viewIsVideo = true;
        viewIsImage = false;
        viewIsBinary = false;
      } else {
        viewIsVideo = false;
        viewIsImage = false;
        viewIsBinary = true;
      }
    } catch (err) {
      viewError = err instanceof Error ? err.message : String(err);
    } finally {
      viewLoading = false;
    }
  }

  function trimTrailingZeros(value) {
    return value.replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
  }

  function formatBytes(bytes, binary) {
    if (!Number.isFinite(bytes) || bytes <= 0) {
      return "0 bytes";
    }

    const base = binary ? 1024 : 1000;
    const units = binary ? ["KiB", "MiB", "GiB"] : ["KB", "MB", "GB"];
    if (bytes < base) {
      return `${bytes} bytes`;
    }

    let value = bytes;
    let unitIndex = -1;
    while (value >= base && unitIndex < units.length - 1) {
      value /= base;
      unitIndex += 1;
    }

    const rounded = value >= 10 ? value.toFixed(1) : value.toFixed(2);
    return `${trimTrailingZeros(rounded)} ${units[unitIndex]}`;
  }

  function isLikelyTextContentType(contentType) {
    const normalized = String(contentType).toLowerCase();
    if (normalized.startsWith("text/")) {
      return true;
    }
    return (
      normalized.startsWith("application/json") ||
      normalized.startsWith("application/xml") ||
      normalized.startsWith("application/javascript") ||
      normalized.startsWith("application/x-www-form-urlencoded")
    );
  }

  const browserImageContentTypes = new Set([
    "image/apng",
    "image/avif",
    "image/gif",
    "image/jpeg",
    "image/png",
    "image/svg+xml",
    "image/webp",
    "image/bmp",
    "image/x-icon",
    "image/vnd.microsoft.icon"
  ]);

  const browserVideoContentTypes = new Set([
    "video/mp4",
    "video/webm",
    "video/ogg"
  ]);

  function normalizeContentType(contentType) {
    return String(contentType).toLowerCase().split(";")[0].trim();
  }

  function isBrowserImageContentType(contentType) {
    return browserImageContentTypes.has(normalizeContentType(contentType));
  }

  function isBrowserVideoContentType(contentType) {
    return browserVideoContentTypes.has(normalizeContentType(contentType));
  }

  function formatTTLDisplay(rawTTL) {
    const source = String(rawTTL ?? "").trim().toLowerCase();
    if (!source) {
      return "15 minutes";
    }

    const matches = Array.from(source.matchAll(/(\d+)\s*([hms])/g));
    if (matches.length === 0) {
      return rawTTL;
    }

    let hours = 0;
    let minutes = 0;
    let seconds = 0;
    for (const match of matches) {
      const value = Number.parseInt(match[1], 10);
      if (!Number.isFinite(value) || value <= 0) {
        continue;
      }
      const unit = match[2];
      if (unit === "h") {
        hours += value;
      } else if (unit === "m") {
        minutes += value;
      } else if (unit === "s") {
        seconds += value;
      }
    }

    const parts = [];
    if (hours > 0) {
      parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`);
    }
    if (minutes > 0) {
      parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`);
    }
    if (seconds > 0) {
      parts.push(`${seconds} ${seconds === 1 ? "second" : "seconds"}`);
    }

    if (parts.length === 0) {
      return rawTTL;
    }
    return parts.join(" ");
  }

  $effect(() => {
    if (refreshTriggered || routeMode !== "view" || !viewMeta?.expires_at) {
      return;
    }

    const expiry = Date.parse(String(viewMeta.expires_at));
    if (!Number.isFinite(expiry)) {
      return;
    }

    if (nowMs >= expiry) {
      refreshTriggered = true;
      window.location.reload();
    }
  });

  function expiresInText(expiresAt, now) {
    const expiry = Date.parse(String(expiresAt ?? ""));
    if (!Number.isFinite(expiry)) {
      return "unknown";
    }
    const deltaMs = Math.max(0, expiry - now);
    if (deltaMs === 0) {
      return "expired";
    }

    const totalSeconds = Math.floor(deltaMs / 1000);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;

    if (hours > 0) {
      return `${hours}h ${minutes}m ${seconds}s`;
    }
    if (minutes > 0) {
      return `${minutes}m ${seconds}s`;
    }
    return `${seconds}s`;
  }

  function onFileChange(event) {
    selectedFile = event.currentTarget.files?.[0] ?? null;
  }

  async function copyScratchURL() {
    if (!viewMeta?.raw_url) {
      return;
    }

    copyStatusMessage = "";
    copyStatusKind = "";

    const absoluteRawURL = new URL(String(viewMeta.raw_url), window.location.origin).toString();
    try {
      await navigator.clipboard.writeText(absoluteRawURL);
      copyStatusMessage = "Copied download URL.";
      copyStatusKind = "success";
    } catch {
      copyStatusMessage = "Unable to copy URL. Copy it manually from the address bar.";
      copyStatusKind = "error";
    }
  }

  function openScratchRawURL() {
    if (!viewMeta?.raw_url) {
      return;
    }
    const absoluteRawURL = new URL(String(viewMeta.raw_url), window.location.origin).toString();
    window.open(absoluteRawURL, "_blank", "noopener,noreferrer");
  }

  function navigateTo(path) {
    const destination = String(path ?? "").trim();
    if (!destination) {
      return;
    }
    window.location.assign(destination);
  }

  async function onSubmit(event) {
    event.preventDefault();
    if (loading) {
      return;
    }
    if (!uploadAllowed) {
      errorMessage = "Uploads are not available from your network location.";
      return;
    }

    errorMessage = "";

    if (!selectedFile) {
      errorMessage = "Choose a file before creating a scratch.";
      return;
    }
    if (maxUploadSizeBytes > 0 && selectedFile.size > maxUploadSizeBytes) {
      errorMessage = `Selected file is too large (${formatBytes(selectedFile.size, true)}). Max upload size is ${maxUploadSizeDisplay}.`;
      return;
    }

    loading = true;
    try {
      const formData = new FormData();
      formData.append("file", selectedFile);
      const response = await fetch("/api/scratch", {
        method: "POST",
        body: formData
      });

      if (!response.ok) {
        const message = await response.text();
        throw new Error(message || `request failed (${response.status})`);
      }

      const created = await response.json();
      const viewURL = typeof created?.view_url === "string" ? created.view_url : "";
      if (!viewURL) {
        throw new Error("scratch created but view URL is missing");
      }
      window.location.assign(viewURL);
      return;
    } catch (err) {
      if (err instanceof TypeError) {
        errorMessage = `Upload failed before the server returned a response. Max upload size is ${maxUploadSizeDisplay}.`;
      } else {
        errorMessage = err instanceof Error ? err.message : String(err);
      }
    } finally {
      loading = false;
    }
  }
</script>

<main class="container">
  <header class="site-header">
    <div class="brand-row">
      <a class="brand" href="/">Scratchbox</a>
    </div>
    <nav class="site-nav" aria-label="Primary">
      <button class="link-button nav-button action-button" type="button" onclick={() => navigateTo("/")}>Home</button>
      {#if uploadAllowed}
        <button class="link-button nav-button action-button" type="button" onclick={() => navigateTo("/u")}>Upload</button>
      {/if}
    </nav>
  </header>

  {#if routeMode === "home"}
    <section class="panel">
      <h1>Share scratch files quickly</h1>
      <p>
        Scratchbox is a minimal temporary upload service with expiration.
      </p>
      <p>
        No accounts, no sessions, no friction. Create a scratch, grab the URL, and share it.
      </p>
      {#if !uploadAllowed}
        <p class="helper">Uploads are currently unavailable from your network location.</p>
      {/if}
    </section>
  {:else if routeMode === "view"}
    <section class="panel">
      {#if viewLoading}
        <p>Loading scratch content...</p>
      {:else if viewError}
        <section id="error" class="error">{viewError}</section>
      {:else if viewMeta}
        {#if viewIsImage}
          <img
            class="scratch-image"
            src={viewMeta.raw_url}
            alt={`Scratch ${scratchID}`}
            loading="lazy"
          />
        {:else if viewIsVideo}
          <video class="scratch-video" controls preload="metadata">
            <source src={viewMeta.raw_url} type={viewMeta.content_type} />
            Your browser does not support inline video playback.
          </video>
        {:else if !viewIsBinary}
          <pre>{viewContent}</pre>
        {/if}

        <p><strong>Expires in:</strong> {expiresInText(viewMeta.expires_at, nowMs)}</p>
        <p class="scratch-actions">
          <button class="link-button action-button" type="button" onclick={openScratchRawURL}>Download</button>
          <button class="link-button action-button" type="button" onclick={copyScratchURL}>Copy Link</button>
        </p>
        {#if copyStatusMessage}
          <p class={copyStatusKind === "error" ? "copy-status error" : "copy-status"}>{copyStatusMessage}</p>
        {/if}
      {/if}
    </section>
  {:else if routeMode === "upload"}
    <section class="panel">
      <p class="helper">Size limit: {maxUploadSizeDisplay}</p>
      <p class="helper">Expiration: {defaultTTLDisplay}</p>
      {#if configError}
        <p class="helper warning">Could not load UI config: {configError}</p>
      {/if}
      {#if !uploadAllowed}
        <section id="error" class="error">
          Uploads are disabled for your current IP based on server allowlist settings.
        </section>
      {:else}
        <form id="scratch-form" onsubmit={onSubmit}>
          <section id="file-panel" class="input-panel">
            <label for="file">Select a file to upload</label>
            <input
              id="file"
              name="file"
              type="file"
              onchange={onFileChange}
              disabled={loading}
            />
          </section>

          <button class="link-button nav-button action-button submit-button" type="submit" disabled={loading}>
            {#if loading}Creating...{:else}Create scratch{/if}
          </button>
        </form>
      {/if}

      {#if errorMessage}
        <section id="error" class="error">{errorMessage}</section>
      {/if}
    </section>
  {:else if routeMode === "expired"}
    <section class="panel">
      <h1>Scratch expired</h1>
      <p>{viewError || "The scratch you requested is no longer available because its retention time elapsed."}</p>
    </section>
  {:else}
    <section class="panel">
      <h1>Page not found</h1>
      <p>The requested path does not exist.</p>
    </section>
  {/if}

</main>

<style>
  :global(body) {
    margin: 0;
    font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
    background: #0b0e14;
    color: #e7ebf3;
  }

  .container {
    max-width: 960px;
    margin: 0 auto;
    padding: 2rem 1.25rem 3rem;
  }

  .site-header,
  .panel {
    border: 1px solid #2a3243;
    border-radius: 10px;
    background: #101726;
  }

  .site-header {
    padding: 1rem 1.1rem;
  }

  .site-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
  }

  .brand-row {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
  }

  .brand {
    font-size: 1.3rem;
    font-weight: 700;
    text-decoration: none;
    color: #f5f7fc;
  }

  .site-nav {
    display: flex;
    gap: 0.85rem;
  }

  .panel {
    margin-top: 1rem;
    padding: 1.2rem;
  }

  .helper {
    color: #b1b7c7;
    margin-top: 0.5rem;
  }

  .link-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.45rem 0.8rem;
    border: 1px solid #2f67c8;
    border-radius: 8px;
    background: #213f75;
    color: #f2f5fb;
    text-decoration: none;
    font-weight: 600;
  }

  .nav-button {
    background: #162640;
    border-color: #355487;
    font-weight: 500;
  }

  .warning {
    color: #f4c16e;
  }

  .scratch-actions {
    display: flex;
    align-items: center;
    gap: 0.6rem;
  }

  .action-button {
    cursor: pointer;
  }

  .copy-status {
    margin-top: 0.35rem;
    color: #9be8ac;
  }

  .input-panel,
  #error {
    margin-top: 1rem;
  }

  input[type="file"] {
    margin-top: 0.5rem;
    width: 100%;
    background: #121826;
    color: #e7ebf3;
    border: 1px solid #2a3243;
    border-radius: 8px;
    padding: 0.7rem;
  }

  .submit-button {
    margin-top: 1rem;
  }

  button[disabled] {
    opacity: 0.65;
    cursor: not-allowed;
  }

  pre {
    max-width: 100%;
    overflow: auto;
    white-space: pre-wrap;
    background: #101726;
    border: 1px solid #2a3243;
    border-radius: 8px;
    padding: 1rem;
  }

  .scratch-image {
    display: block;
    max-width: 100%;
    max-height: 70vh;
    border: 1px solid #2a3243;
    border-radius: 8px;
    background: #0b0e14;
  }

  .scratch-video {
    display: block;
    width: 100%;
    max-height: 70vh;
    border: 1px solid #2a3243;
    border-radius: 8px;
    background: #0b0e14;
  }

  .error {
    color: #ff9b9b;
  }
</style>
