<script lang="ts">
  import "../app.postcss";
  import { syncStore, fragmentSyncCount } from "$lib/stores/stores";
  import { onMount, onDestroy } from "svelte";
  import {
    DatabaseMeta,
    getCurrentSchema,
    getLatestDbName,
    importFromDatabase,
    incrementDbName,
    initDb,
  } from "$lib/db";
  import SettingsModal from "$lib/components/SettingsModal.svelte";
  import { getSystem } from "$lib/gui";
  import classNames from "classnames";
  import { dev } from "$app/environment";
  import { Toaster } from "$lib/components/ui/sonner";
  import { assets } from "$app/paths";
  import FullScreenError from "$lib/components/FullScreenError.svelte";
  import { debounce, wrapError } from "$lib/utils";
  import DevTooling from "$lib/components/DevTooling.svelte";
  import { openAiConfig } from "$lib/stores/stores/llmProvider";
  import ActionsMenu from "$lib/components/ActionsMenu.svelte";

  const sys = getSystem();
  let startupError: Error | null = null;
  let appReady = false;

  const handleHardReset = async ({ confirm = true } = {}) => {
    const confirmed = confirm
      ? await sys.confirm(
          "Are you sure you want to reset your database? This will delete all your chat history."
        )
      : true;

    if (!confirmed) return;

    appReady = false;

    incrementDbName();

    // Reloading the window should cause handleStartup to be called again on app
    // mount, using the new db name. Using a new db name is equivalent to using
    // a new db. The old db will not be removed, but the app will have no
    // existing data.
    location.reload();
  };

  /**
   * NOTE: Teardown is not currently used because the layout only gets torn down when the tab closes
   */
  let teardown: any;

  const handleStartup = async () => {
    // throw up after a time if the app is hanging
    let _timeout = setTimeout(() => {
      throw new Error("Timed out trying to initialize");
    }, 20_000);

    const dbName = getLatestDbName();
    // @note The whole app assumes the db exists and is ready. Do not render before that
    try {
      const start = performance.now();
      console.debug("Initializing database");
      teardown = await initDb(dbName || "");
      console.debug(`Database initialized in ${performance.now() - start}ms :: ${dbName}`);
      const pendingImport = localStorage.getItem("pendingImport");
      if (pendingImport) {
        await importFromDatabase(undefined, pendingImport);
        localStorage.removeItem("pendingImport");
      }
    } catch (err: any) {
      throw wrapError(err, `There was an error initializing the database.`);
    } finally {
      clearTimeout(_timeout);
    }

    appReady = true;

    const siteId = await DatabaseMeta.getSiteId();
    console.debug(`App initialized. siteId=${siteId}`);

    const lastSyncChain = $openAiConfig.lastSyncChain;

    if (lastSyncChain) {
      console.debug(`Connecting to sync chain: ${lastSyncChain}`);

      // Not sure why, but this doesn't work if we do it immediately.
      setTimeout(() => {
        syncStore.connectTo(lastSyncChain, { autoSync: true });
      }, 1000);
    }
  };

  let isPendingImport = false;

  onMount(async () => {
    try {
      await handleStartup();
    } catch (error: any) {
      console.error("startup error", error);
      startupError = error;
      isPendingImport = true;
      localStorage.setItem("pendingImport", getLatestDbName() as string);
    }
  });

  function isExternalUrl(href: any) {
    if (typeof href !== "string") return false;

    try {
      const url = new URL(href);
      return url.origin !== location.origin;
    } catch (err) {
      console.debug("Could not parse url", err);
      return false;
    }
  }

  function handleExternalUrls(e: MouseEvent) {
    // @ts-ignore
    const href = e.target.href;
    if (isExternalUrl(href)) {
      e.preventDefault();
      sys.openExternal(href);
    }
  }

  const CI = Boolean(process.env.CI);
  let telemetryDisabled = dev || CI;
  onMount(() => {
    if (!CI) {
      telemetryDisabled = localStorage.getItem("telemetryDisabled") === "true";
    }
  });

  onMount(() => {
    // @note servie worker is not registered on desktop (no need)
    if (sys.isBrowser && "serviceWorker" in navigator) {
      const fn = () => {
        const serviceWorkerPath = `${assets}/service-worker.js`;
        console.log("%cserviceWorker/register", "color:salmon;", serviceWorkerPath);
        navigator.serviceWorker.register(serviceWorkerPath, {
          type: dev ? "module" : "classic",
        });
      };
      console.log("%cserviceWorker", "color:salmon;");
      if (window.document.readyState === "complete") {
        fn();
      } else {
        addEventListener("load", fn);
      }
    }
  });

  const siteMeta = {
    name: "Prompta",
    title: "Prompta - Open-source ChatGPT Client",
    description:
      "Prompta is an open-source UI client for talking to ChatGPT (and GPT-4). Store all your chats locally. Search them easily. Sync across devices.",
  };

  const handleSync = debounce(() => {
    if ($syncStore.connection) {
      syncStore.sync();
    }
  }, 100);

  let showDelayedMessage = false;
  let messageTimer: ReturnType<typeof setTimeout>;

  onMount(() => {
    messageTimer = setTimeout(() => {
      showDelayedMessage = true;
    }, 2000);
  });

  onDestroy(() => {
    clearTimeout(messageTimer);
  });
</script>

<svelte:window on:click={handleExternalUrls} on:focus={handleSync} />

<svelte:head>
  <title>{siteMeta.title}</title>
  <meta name="description" content={siteMeta.description} />
  <meta name="twitter:card" content="summary" />
  <meta property="og:title" content={siteMeta.title} />
  <meta property="og:description" content={siteMeta.description} />

  <link rel="icon" type="image/png" sizes="16x16" href="/icon_16x16.png" />
  <link rel="icon" type="image/png" sizes="32x32" href="/icon_16x16@2x.png" />

  <!-- NOTE: user-scalable=no is unfortunate, but safari sometimes zooms in automatically when focusing an input which breaks the layout -->
  <!-- NOTE: viewport-fit=cover may need to be revisited. the current layout doesn't account for notches at all -->
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no"
  />

  {#if !telemetryDisabled}
    <script>
      // prettier-ignore
      !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
      // prettier-ignore
      posthog.init('phc_sM0XZyv1NmCKHt481PFSGpKfuVDJ1IkNvYtrdFK7r4s',{api_host:'https://app.posthog.com'})
    </script>
  {/if}
</svelte:head>

<div
  class={classNames("overflow-hidden text-white bg-[#1B1B1B]", {
    "rounded-2xl border": sys.isTauri,
  })}
>
  {#if startupError}
    <FullScreenError
      title={isPendingImport ? "Database migration required" : "The app could not be initialized"}
      error={isPendingImport ? undefined : startupError}
      level={isPendingImport ? "info" : "error"}
    >
      <div class="prose prose-invert">
        <div class="mt-3">
          <p>Database Path: <code>{getLatestDbName()}</code></p>
          {#await getCurrentSchema() then schema}
            <p>Schema Name: <code>{schema.name}</code></p>
          {:catch}
            <p>Schema: <code>unknown</code></p>
          {/await}
        </div>
        {#if isPendingImport}
          <h3>
            <em>Manual migration</em>
          </h3>
          <p>
            Use the button below to begin migration. Please do not close the app while a migration
            is in progress.
          </p>
          <div class="flex flex-col space-y-2 sm:flex-row sm:space-x-6 sm:space-y-0">
            <button
              on:click={() => handleHardReset({ confirm: false })}
              class="block bg-yellow-600 rounded px-4 py-2 cursor-pointer"
            >
              Begin Migration
            </button>
          </div>
        {:else}
          <h3>
            <em>What can you do?</em>
          </h3>
          <ul>
            <li>
              <strong> Reset your database </strong>. This will delete all your data, but it might
              resolve the startup issue. This way you can continue using the app immediately.
            </li>
            <li>
              <strong
                >Check the{" "}
                <a class="" target="_blank" href="https://github.com/iansinnott/prompta/issues">
                  Github Issues
                </a></strong
              > to see if someone has solved this problem.
            </li>
          </ul>
          <div class="flex flex-col space-y-2 sm:flex-row sm:space-x-6 sm:space-y-0">
            <button
              on:click={() => handleHardReset()}
              class="block bg-red-600 rounded px-4 py-2 cursor-pointer"
            >
              Reset Database
            </button>
            <a
              class="text-center block bg-gray-600 rounded px-4 py-2"
              target="_blank"
              href="https://github.com/iansinnott/prompta/issues/10"
            >
              Check the Github Issues
            </a>
          </div>
        {/if}
      </div>
    </FullScreenError>
  {:else if appReady}
    <slot />
    <SettingsModal />
  {:else}
    <!-- Loading Spinner before initialization -->
    <!-- adding both heights for fallback. not every browser likes svh -->
    <div class="flex flex-col items-center justify-center h-screen" style="height: 100svh;">
      <div class="relative">
        <div class="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-zinc-700" />
        {#if $fragmentSyncCount > 0}
          <div class="absolute inset-0 flex items-center justify-center">
            <span class="text-zinc-400">{$fragmentSyncCount}</span>
          </div>
        {/if}
      </div>
      {#if showDelayedMessage}
        <p class="mt-4 text-zinc-400 text-sm">
          This may take a while as we process your chat history...
        </p>
      {/if}
    </div>
  {/if}
</div>

<!-- Offset here is just for the y dimension. See style tag for the X dimension -->
<Toaster richColors closeButton position="top-right" offset={84} visibleToasts={5} />

{#if appReady}
  <!--  This is just the value that happens to line up  -->
  <div
    class={classNames("absolute right-4 flex space-x-4 items-center", {
      "bottom-14": sys.isPWAInstalled,
      "bottom-[14px]": !sys.isPWAInstalled,
    })}
  >
    <ActionsMenu class="text-xs uppercase leading-[22px]" />
  </div>
  <DevTooling />
{/if}

<style>
  /*  Fix the offset of the toaster. It has no API for separately setting the x and y offset  */
  :global([data-sonner-toaster][data-x-position="right"]) {
    right: 16px !important;
  }
</style>
