<script lang="ts">
  import { onMount, onDestroy } from "svelte";
  import { push } from "svelte-spa-router";
  import { session } from "../lib/store.svelte.js";
  import { sendMessage } from "../lib/messages.js";
  import { lookup, ApiError } from "../lib/api.js";
  import { inboxService } from "../lib/inbox-service.svelte.js";
  import EmojiButton from "../lib/EmojiButton.svelte";
  import {
    appendOutbound,
    ensureConversation,
    listConversations,
    type Conversation,
  } from "../lib/conversations-store.js";
  import type { Identity } from "../lib/kez.js";

  let conversations = $state<Conversation[]>([]);
  let activePrimary = $state<Identity | null>(null);
  let activeConv = $derived(
    activePrimary
      ? conversations.find((c) => c.peer_primary === activePrimary) ?? null
      : null,
  );
  let composeText = $state("");
  let composing = $state(false);
  let composeEl: HTMLInputElement | null = $state(null);

  /**
   * Insert an emoji at the current cursor position in the compose input
   * (or append if the input isn't focused). Keeps the cursor right after
   * the inserted character so you can keep typing.
   */
  function insertEmoji(emoji: string) {
    if (!composeEl) {
      composeText += emoji;
      return;
    }
    const start = composeEl.selectionStart ?? composeText.length;
    const end = composeEl.selectionEnd ?? composeText.length;
    composeText = composeText.slice(0, start) + emoji + composeText.slice(end);
    // Restore focus + position the caret after the inserted emoji
    // (need a tick for the value to flush through the binding).
    queueMicrotask(() => {
      if (!composeEl) return;
      composeEl.focus();
      const pos = start + emoji.length;
      composeEl.setSelectionRange(pos, pos);
    });
  }

  // Lazy grapheme segmenter — reused across messages.
  const graphemeSeg =
    typeof Intl !== "undefined" && "Segmenter" in Intl
      ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
      : null;

  /**
   * iMessage-style: if the whole message is just a handful of emoji and
   * nothing else, drop the bubble chrome and render it big. We count
   * grapheme clusters (so "👨‍👩‍👧" reads as 1, not 5 code points).
   *
   * "Emoji-only" = the string contains no letters, digits, whitespace,
   * or punctuation. That leaves Symbol_Other + emoji bits.
   */
  function emojiOnlyBoost(body: string): "lg" | "xl" | "2xl" | null {
    const trimmed = body.trim();
    if (!trimmed) return null;
    if (/[\p{L}\p{N}\p{P}\p{Zs}]/u.test(trimmed)) return null;
    const count = graphemeSeg
      ? [...graphemeSeg.segment(trimmed)].length
      : [...trimmed].length;
    if (count === 1) return "2xl";
    if (count <= 3) return "xl";
    if (count <= 6) return "lg";
    return null;
  }
  /** Unsubscribe handle for the inbox-service "new message" listener. */
  let unsubscribe: (() => void) | null = null;

  /** Scroll container for the thread view — bound for auto-scroll. */
  let scrollEl: HTMLDivElement | null = $state(null);
  /** Tracks the previously-rendered conversation so we know when the
   *  user just OPENED a different thread (always scroll to bottom) vs.
   *  when a new message landed in the current one (scroll only if they
   *  were near the bottom already — don't yank them out of history). */
  let prevPrimary: string | null = null;
  let prevMessageCount = 0;

  /** Distance (px) from the bottom of the thread within which we still
   *  count "near bottom" for the follow-along rule. Roughly two short
   *  bubble heights — generous enough that small misses don't sticky. */
  const SCROLL_FOLLOW_THRESHOLD_PX = 120;

  // Auto-scroll effect — runs whenever activeConv.messages.length or
  // activeConv.peer_primary changes (both are reactive reads).
  $effect(() => {
    if (!activeConv || !scrollEl) {
      prevPrimary = null;
      prevMessageCount = 0;
      return;
    }
    const el = scrollEl;
    const count = activeConv.messages.length;
    const conversationChanged = activeConv.peer_primary !== prevPrimary;
    if (conversationChanged) {
      prevPrimary = activeConv.peer_primary;
      prevMessageCount = count;
      // Wait a microtask so Svelte's DOM updates have flushed.
      queueMicrotask(() => (el.scrollTop = el.scrollHeight));
      return;
    }
    if (count > prevMessageCount) {
      prevMessageCount = count;
      const distFromBottom =
        el.scrollHeight - el.scrollTop - el.clientHeight;
      if (distFromBottom < SCROLL_FOLLOW_THRESHOLD_PX) {
        queueMicrotask(() => (el.scrollTop = el.scrollHeight));
      }
    } else {
      prevMessageCount = count;
    }
  });

  // "Start chat with" lookup state.
  let newPeerInput = $state("");
  let resolving = $state(false);
  let resolveError = $state<string | null>(null);

  // Toast for the share-link copy action.
  let copied = $state(false);

  onMount(async () => {
    if (!session.unlocked) {
      push("/unlock");
      return;
    }
    await refresh();
    // Subscribe to the always-on inbox service — re-render whenever a
    // new message lands. The service is already running (it started on
    // session unlock in store.svelte.ts) regardless of which route the
    // user was on.
    unsubscribe = inboxService.onMessage(() => void refresh());
    // Landing here = the user has seen new messages; reset the badge.
    inboxService.markAllRead();
  });

  onDestroy(() => {
    unsubscribe?.();
  });

  async function refresh() {
    conversations = await listConversations();
  }

  /** "Start chat with handle" — resolve, ensure conversation, open it. */
  async function startConversation() {
    if (!session.unlocked || !newPeerInput.trim()) return;
    resolving = true;
    resolveError = null;
    try {
      // Accept "alice" or "alice@kez.lat". v0.1 ignores the server part —
      // cross-server lookup lands in v0.2 along with sigchain push.
      const local = newPeerInput.trim().split("@")[0];
      if (local === session.unlocked.handle) {
        resolveError = "That's you — can't chat with yourself.";
        return;
      }
      const record = await lookup(local);
      await ensureConversation(record.primary as Identity, record.fqhn);
      activePrimary = record.primary as Identity;
      newPeerInput = "";
      await refresh();
    } catch (e) {
      if (e instanceof ApiError && e.status === 404) {
        resolveError = `No one with handle "${newPeerInput}" is registered on this server.`;
      } else {
        resolveError = (e as Error).message;
      }
    } finally {
      resolving = false;
    }
  }

  async function send() {
    if (!session.unlocked || !activeConv || !composeText.trim()) return;
    composing = true;
    try {
      const body = composeText;
      composeText = "";
      await sendMessage({
        senderHandle: session.unlocked.handle,
        senderSeed: session.unlocked.seed,
        senderPrimary: session.unlocked.primary,
        recipient: activeConv.peer_handle || activeConv.peer_primary,
        body,
      });
      await appendOutbound({
        peer_primary: activeConv.peer_primary,
        peer_handle: activeConv.peer_handle,
        from: session.unlocked.primary,
        body,
      });
      await refresh();
    } catch (e) {
      alert(`Send failed: ${(e as Error).message}`);
      composeText = composeText; // no-op, keep linter happy
    } finally {
      composing = false;
    }
  }

  function shortKey(primary: Identity): string {
    if (primary.startsWith("ed25519:")) {
      const hex = primary.slice("ed25519:".length);
      return `${hex.slice(0, 8)}…${hex.slice(-4)}`;
    }
    return primary;
  }

  /** What we show as the conversation title. */
  function displayName(c: Conversation): string {
    return c.peer_handle || shortKey(c.peer_primary);
  }

  function formatTime(iso: string): string {
    const d = new Date(iso);
    const today = new Date();
    if (
      d.getFullYear() === today.getFullYear() &&
      d.getMonth() === today.getMonth() &&
      d.getDate() === today.getDate()
    ) {
      return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
    }
    return d.toLocaleString([], { dateStyle: "short", timeStyle: "short" });
  }

  async function copyMyKez() {
    if (!session.unlocked) return;
    await navigator.clipboard.writeText(
      `${session.unlocked.handle}@${session.unlocked.server}`,
    );
    copied = true;
    setTimeout(() => (copied = false), 1500);
  }
</script>

<div class="flex h-[calc(100vh-8rem)] gap-4">
  <!-- Sidebar -->
  <aside class="w-80 shrink-0 border border-gray-200 rounded-lg bg-white flex flex-col">
    <!-- Your KEZ (so users can share it to start chats) -->
    <div class="p-3 border-b border-gray-200 bg-gray-50">
      <p class="text-xs text-gray-500 uppercase tracking-wide mb-1">Your KEZ</p>
      {#if session.unlocked}
        <div class="flex items-center justify-between gap-2">
          <code class="font-mono text-sm text-gray-900 truncate">
            {session.unlocked.handle}@{session.unlocked.server}
          </code>
          <button
            class="text-xs px-2 py-0.5 border border-gray-300 rounded text-gray-700 hover:bg-white shrink-0"
            onclick={copyMyKez}
            title="Copy your KEZ — share it with someone so they can message you"
          >
            {copied ? "✓" : "copy"}
          </button>
        </div>
      {/if}
    </div>

    <!-- Start a new conversation -->
    <div class="p-3 border-b border-gray-200">
      <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">
        Start a chat
      </p>
      <form
        class="flex gap-2"
        onsubmit={(e) => {
          e.preventDefault();
          startConversation();
        }}
      >
        <input
          type="text"
          bind:value={newPeerInput}
          placeholder="alice  or  alice@kez.lat"
          class="flex-1 min-w-0 px-2 py-1 text-sm border border-gray-300 rounded font-mono"
          autocomplete="off"
          disabled={resolving}
        />
        <button
          type="submit"
          class="px-3 py-1 text-sm bg-gray-900 text-white rounded hover:bg-gray-700 disabled:opacity-50"
          disabled={!newPeerInput.trim() || resolving}
        >
          {resolving ? "…" : "+"}
        </button>
      </form>
      {#if resolveError}
        <p class="mt-2 text-xs text-red-700">{resolveError}</p>
      {:else}
        <p class="mt-2 text-xs text-gray-500">
          Enter the KEZ of someone you want to message. Ask them to share it
          with you (the button above does the same for yours).
        </p>
      {/if}
    </div>

    <!-- Conversation list -->
    <div class="flex-1 overflow-y-auto">
      {#if conversations.length === 0}
        <p class="p-4 text-sm text-gray-500 italic">
          No conversations yet.
        </p>
      {:else}
        <ul>
          {#each conversations as c (c.peer_primary)}
            {@const last = c.messages[c.messages.length - 1]}
            <li>
              <button
                class={`w-full text-left p-3 border-b border-gray-100 hover:bg-gray-50 ${activePrimary === c.peer_primary ? "bg-gray-100" : ""}`}
                onclick={() => (activePrimary = c.peer_primary)}
              >
                <p class="font-mono text-sm font-semibold text-gray-900 truncate">
                  {displayName(c)}
                </p>
                {#if last}
                  <p class="text-xs text-gray-500 truncate">
                    {last.direction === "out" ? "→ " : "← "}{last.body}
                  </p>
                  <p class="text-xs text-gray-400 mt-0.5">{formatTime(last.ts)}</p>
                {:else}
                  <p class="text-xs text-gray-400 italic">No messages yet</p>
                {/if}
              </button>
            </li>
          {/each}
        </ul>
      {/if}
    </div>

    <!-- Footer status — reads from the global inbox service so it
         reflects the SAME connection that's running everywhere else. -->
    <div class="p-2 border-t border-gray-200 text-xs text-gray-500 space-y-0.5">
      <p>
        {#if inboxService.status === "live"}
          <span class="text-green-700">● live</span>
        {:else if inboxService.status === "reconnecting"}
          <span class="text-amber-700">● reconnecting…</span>
        {:else if inboxService.status === "connecting"}
          <span class="text-gray-500">○ connecting…</span>
        {:else}
          <span class="text-gray-400">○ off</span>
        {/if}
      </p>
      {#if inboxService.lastError}
        <p class="text-red-700">⚠ {inboxService.lastError}</p>
      {/if}
    </div>
  </aside>

  <!-- Main: thread or empty state -->
  <main class="flex-1 min-w-0 border border-gray-200 rounded-lg bg-white flex flex-col">
    {#if !activeConv}
      <div class="flex-1 flex items-center justify-center p-8">
        <div class="max-w-md text-center space-y-4">
          <div class="text-4xl">🔒</div>
          <h2 class="text-lg font-semibold text-gray-900">
            End-to-end encrypted chat
          </h2>
          <p class="text-sm text-gray-700">
            Messages on kez-chat are encrypted between you and the recipient.
            Even kez.lat (the server) can't read them — it just relays
            opaque ciphertext.
          </p>
          <p class="text-sm text-gray-700">
            To start, ask someone for their <strong>KEZ</strong> — their
            <code class="bg-gray-100 px-1 rounded text-xs">handle@server</code>
            (like an email address). Enter it on the left under
            <strong>Start a chat</strong>. Or share yours with them using the
            copy button.
          </p>
          <p class="text-xs text-gray-500 pt-2 border-t border-gray-200">
            Don't know anyone yet? Open kez.lat in a second browser window,
            create a different account, and message yourself between the two.
          </p>
        </div>
      </div>
    {:else}
      <div class="p-3 border-b border-gray-200">
        <p class="font-mono text-sm font-semibold text-gray-900">
          {displayName(activeConv)}
        </p>
        <p class="text-xs text-gray-500 break-all font-mono">
          {activeConv.peer_primary}
        </p>
      </div>

      <div class="flex-1 overflow-y-auto p-4 space-y-2" bind:this={scrollEl}>
        {#each activeConv.messages as m (m.seq + ":" + m.direction)}
          {@const boost = emojiOnlyBoost(m.body)}
          <div class={`max-w-md ${m.direction === "out" ? "ml-auto" : ""}`}>
            {#if boost}
              <!-- Emoji-only message: drop the bubble chrome, render big. -->
              <div
                class={`whitespace-pre-wrap break-words leading-none ${boost === "2xl" ? "text-5xl" : boost === "xl" ? "text-4xl" : "text-3xl"} ${m.direction === "out" ? "text-right" : ""}`}
              >
                {m.body}
              </div>
            {:else}
              <div
                class={`px-3 py-2 rounded-lg text-sm whitespace-pre-wrap break-words ${m.direction === "out" ? "bg-gray-900 text-white" : "bg-gray-100 text-gray-900"}`}
              >
                {m.body}
              </div>
            {/if}
            <p class={`mt-1 text-xs text-gray-400 ${m.direction === "out" ? "text-right" : ""}`}>
              {formatTime(m.ts)}
            </p>
          </div>
        {/each}
        {#if activeConv.messages.length === 0}
          <p class="text-gray-400 text-sm italic text-center mt-8">
            No messages yet. Say hi — it's encrypted before it leaves your
            browser.
          </p>
        {/if}
      </div>

      <form
        class="p-3 border-t border-gray-200 flex gap-2 items-center"
        onsubmit={(e) => {
          e.preventDefault();
          send();
        }}
      >
        <EmojiButton onpick={insertEmoji} />
        <input
          type="text"
          bind:value={composeText}
          bind:this={composeEl}
          placeholder="Type a message…"
          class="flex-1 min-w-0 px-3 py-2 text-sm border border-gray-300 rounded"
          autocomplete="off"
          disabled={composing}
        />
        <button
          type="submit"
          class="px-4 py-2 text-sm bg-gray-900 text-white rounded hover:bg-gray-700 disabled:opacity-50"
          disabled={composing || !composeText.trim()}
        >
          {composing ? "Sending…" : "Send"}
        </button>
      </form>
    {/if}
  </main>
</div>
