<script lang="ts">
  import Icon from "@iconify/svelte";
  import "$lib/iconify-offline";
  import { env } from "$env/dynamic/public";
  import { useTranslate, T } from "$lib/translations";

  let {
    pageId,
    environment,
    class: cls = "",
    bodyMax = 4096,
  }: {
    pageId: string;
    environment?: string;
    class?: string;
    bodyMax?: number;
  } = $props();

  const t = useTranslate();

  type Status = "approved" | "pending" | "spam";

  type PublicComment = {
    id: string;
    parent_id?: string;
    author: string;
    body: string;
    created_at: string;
    updated_at?: string;
    status: Status;
    can_edit: boolean;
  };

  type ListResponse = {
    comments: PublicComment[];
    session_id: string;
    require_approval: boolean;
  };

  const cmsBase = (
    env.PUBLIC_CMS_URL ||
    import.meta.env.PUBLIC_CMS_URL ||
    "http://localhost:3000"
  ).replace(/\/$/, "");

  const apiUrl = (suffix = "") =>
    `${cmsBase}/api/comments/${encodeURIComponent(pageId)}${suffix}${
      environment ? `?_environment=${encodeURIComponent(environment)}` : ""
    }`;

  const AUTHOR_KEY = "ww_comment_author";

  let comments = $state<PublicComment[]>([]);
  let requireApproval = $state(false);
  let loading = $state(true);
  let loadError = $state<string | null>(null);

  // Anti-instant-bot: Backend rejects POSTs with `client_age_ms` below a
  // threshold (default 3000ms). Captured once when the component mounts.
  const mountedAt = Date.now();

  let author = $state("");
  let body = $state("");
  let website = $state(""); // honeypot
  let submitting = $state(false);
  let submitError = $state<string | null>(null);

  let replyTo = $state<string | null>(null);
  let replyAuthor = $state("");
  let replyBody = $state("");
  let replyError = $state<string | null>(null);

  let editingId = $state<string | null>(null);
  let editBody = $state("");
  let editError = $state<string | null>(null);

  // Restore name from localStorage on first mount.
  $effect(() => {
    if (typeof localStorage !== "undefined") {
      const saved = localStorage.getItem(AUTHOR_KEY);
      if (saved && !author) author = saved;
      if (saved && !replyAuthor) replyAuthor = saved;
    }
    loadComments();
  });

  function rememberAuthor(name: string) {
    if (typeof localStorage !== "undefined" && name.trim()) {
      localStorage.setItem(AUTHOR_KEY, name.trim());
    }
  }

  async function loadComments() {
    loadError = null;
    try {
      const r = await fetch(apiUrl(), { credentials: "include" });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      const data = (await r.json()) as ListResponse;
      comments = data.comments;
      requireApproval = data.require_approval;
    } catch (e) {
      loadError = e instanceof Error ? e.message : "Load failed";
    } finally {
      loading = false;
    }
  }

  async function postComment(authorName: string, text: string, parentId: string | null) {
    const r = await fetch(apiUrl(), {
      method: "POST",
      credentials: "include",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        author: authorName,
        body: text,
        parent_id: parentId,
        website,
        client_age_ms: Date.now() - mountedAt,
      }),
    });
    if (!r.ok) {
      const msg = await r.text().catch(() => "");
      throw new Error(msg || `HTTP ${r.status}`);
    }
    return (await r.json()) as PublicComment;
  }

  async function onSubmit(e: SubmitEvent) {
    e.preventDefault();
    if (!author.trim() || !body.trim() || submitting) return;
    submitting = true;
    submitError = null;
    try {
      await postComment(author, body, null);
      rememberAuthor(author);
      body = "";
      await loadComments();
    } catch (e) {
      submitError = e instanceof Error ? e.message : "Send failed";
    } finally {
      submitting = false;
    }
  }

  async function onReply(e: SubmitEvent, parentId: string) {
    e.preventDefault();
    if (!replyAuthor.trim() || !replyBody.trim() || submitting) return;
    submitting = true;
    replyError = null;
    try {
      await postComment(replyAuthor, replyBody, parentId);
      rememberAuthor(replyAuthor);
      replyBody = "";
      replyTo = null;
      await loadComments();
    } catch (e) {
      replyError = e instanceof Error ? e.message : "Send failed";
    } finally {
      submitting = false;
    }
  }

  function startEdit(c: PublicComment) {
    editingId = c.id;
    editBody = c.body;
    editError = null;
  }

  function cancelEdit() {
    editingId = null;
    editBody = "";
    editError = null;
  }

  async function saveEdit(id: string) {
    if (!editBody.trim()) return;
    try {
      const r = await fetch(`${apiUrl()}`.replace(`/${pageId}`, `/${pageId}/${id}`), {
        method: "PATCH",
        credentials: "include",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ body: editBody }),
      });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      await loadComments();
      cancelEdit();
    } catch (e) {
      editError = e instanceof Error ? e.message : "Edit failed";
    }
  }

  async function deleteComment(id: string) {
    if (!confirm(t(T.comments_confirm_delete))) return;
    try {
      const r = await fetch(`${apiUrl()}`.replace(`/${pageId}`, `/${pageId}/${id}`), {
        method: "DELETE",
        credentials: "include",
      });
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      await loadComments();
    } catch (e) {
      loadError = e instanceof Error ? e.message : "Delete failed";
    }
  }

  // Auto-resize a textarea on input.
  function autosize(node: HTMLTextAreaElement) {
    const fit = () => {
      node.style.height = "auto";
      node.style.height = `${node.scrollHeight}px`;
    };
    fit();
    node.addEventListener("input", fit);
    return {
      destroy() {
        node.removeEventListener("input", fit);
      },
    };
  }

  // Cmd/Ctrl+Enter on a textarea submits the closest form.
  function submitOnCmdEnter(e: KeyboardEvent) {
    if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
      const form = (e.currentTarget as HTMLElement).closest("form");
      if (form) {
        e.preventDefault();
        form.requestSubmit();
      }
    }
  }

  // Stable hue from name → consistent avatar colour per author.
  function hueFromName(name: string): number {
    let h = 0;
    for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
    return h % 360;
  }

  function initialsFromName(name: string): string {
    const parts = name.trim().split(/\s+/);
    if (parts.length === 0 || !parts[0]) return "?";
    if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
    return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
  }

  function fmtAbs(iso: string): string {
    try {
      return new Date(iso).toLocaleString("de-DE", {
        dateStyle: "medium",
        timeStyle: "short",
      });
    } catch {
      return iso;
    }
  }

  function fmtRel(iso: string): string {
    try {
      const then = new Date(iso).getTime();
      const now = Date.now();
      const diff = Math.round((then - now) / 1000); // seconds, negative = past
      const abs = Math.abs(diff);
      const fmt = new Intl.RelativeTimeFormat("de", { numeric: "auto" });
      if (abs < 60) return fmt.format(diff, "second");
      if (abs < 3600) return fmt.format(Math.round(diff / 60), "minute");
      if (abs < 86_400) return fmt.format(Math.round(diff / 3600), "hour");
      if (abs < 604_800) return fmt.format(Math.round(diff / 86_400), "day");
      return fmtAbs(iso);
    } catch {
      return iso;
    }
  }

  const topLevel = $derived(comments.filter((c) => !c.parent_id));
  function repliesOf(parentId: string) {
    return comments.filter((c) => c.parent_id === parentId);
  }

  const totalPending = $derived(comments.filter((c) => c.status === "pending").length);
</script>

{#snippet avatar(name: string, size: "sm" | "md" = "md")}
  {@const hue = hueFromName(name || "?")}
  <span
    class="inline-flex shrink-0 items-center justify-center rounded-full font-semibold text-white select-none {size === 'sm'
      ? 'size-7 text-[11px]'
      : 'size-9 text-xs'}"
    style="background: hsl({hue} 55% 45%);"
    aria-hidden="true"
  >
    {initialsFromName(name || "?")}
  </span>
{/snippet}

{#snippet commentBody(c: PublicComment)}
  {#if editingId === c.id}
    <form
      onsubmit={(e) => {
        e.preventDefault();
        saveEdit(c.id);
      }}
      class="mt-2"
    >
      <textarea
        bind:value={editBody}
        use:autosize
        onkeydown={submitOnCmdEnter}
        class="w-full p-2 border border-stein-300 rounded-xs text-sm focus:outline-none focus:ring-2 focus:ring-stein-400"
        rows="2"
        maxlength={bodyMax}
      ></textarea>
      <div class="mt-1 flex items-center gap-3 text-xs">
        <button
          type="submit"
          class="rounded-xs bg-stein-800 px-3 py-1 font-medium text-white hover:bg-stein-700"
        >{t(T.comments_save)}</button>
        <button
          type="button"
          onclick={cancelEdit}
          class="text-stein-500 hover:text-stein-900"
        >{t(T.comments_cancel)}</button>
        <span class="ml-auto text-stein-400 tabular-nums">{editBody.length} / {bodyMax}</span>
      </div>
      {#if editError}<p class="text-fire-700 text-xs mt-1">{editError}</p>{/if}
    </form>
  {:else}
    <p class="mt-1 whitespace-pre-wrap text-sm leading-relaxed text-stein-800">{c.body}</p>
    {#if c.can_edit}
      <div class="mt-1 flex gap-3 opacity-60 group-hover:opacity-100 transition-opacity">
        <button
          type="button"
          onclick={() => startEdit(c)}
          class="text-xs text-stein-600 hover:text-stein-900 inline-flex items-center gap-1"
        >
          <Icon icon="mdi:pencil-outline" class="size-3" />
          {t(T.comments_edit)}
        </button>
        <button
          type="button"
          onclick={() => deleteComment(c.id)}
          class="text-xs text-stein-600 hover:text-fire-700 inline-flex items-center gap-1"
        >
          <Icon icon="mdi:trash-can-outline" class="size-3" />
          {t(T.comments_delete)}
        </button>
      </div>
    {/if}
  {/if}
{/snippet}

{#snippet meta(c: PublicComment)}
  <div class="flex flex-wrap items-baseline gap-2 text-sm">
    <strong class="font-medium text-stein-900">{c.author}</strong>
    {#if c.can_edit}
      <span class="text-[10px] uppercase tracking-wide text-stein-400">{t(T.comments_self_marker)}</span>
    {/if}
    <time
      class="text-stein-500 text-xs"
      datetime={c.created_at}
      title={fmtAbs(c.created_at)}
    >{fmtRel(c.created_at)}</time>
    {#if c.updated_at}
      <span class="text-stein-400 text-xs" title={fmtAbs(c.updated_at)}>{t(T.comments_edited)}</span>
    {/if}
    {#if c.status === "pending"}
      <span class="ml-auto inline-flex items-center gap-1 rounded-full bg-erde-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-erde-800">
        <Icon icon="mdi:clock-outline" class="size-3" />
        {t(T.comments_pending_short)}
      </span>
    {/if}
  </div>
{/snippet}

<div class="comments {cls}">
  {#if loading && comments.length === 0}
    <ul class="space-y-4 list-none p-0" aria-busy="true" aria-label={t(T.comments_loading)}>
      {#each Array(2) as _, i (i)}
        <li class="flex gap-3 list-none!">
          <div class="size-9 shrink-0 rounded-full bg-stein-200 animate-pulse"></div>
          <div class="flex-1 space-y-2">
            <div class="h-3 w-32 bg-stein-200 rounded-xs animate-pulse"></div>
            <div class="h-3 w-full bg-stein-200 rounded-xs animate-pulse"></div>
            <div class="h-3 w-3/4 bg-stein-200 rounded-xs animate-pulse"></div>
          </div>
        </li>
      {/each}
    </ul>
  {:else if loadError}
    <div class="flex items-start gap-2 rounded-xs border border-fire-200 bg-fire-50 p-3 text-sm text-fire-800">
      <Icon icon="mdi:alert-circle-outline" class="size-5 shrink-0 mt-0.5" />
      <div class="flex-1">
        <p class="font-medium">{t(T.comments_load_error_title)}</p>
        <p class="mt-1 text-xs text-fire-700">{loadError}</p>
      </div>
      <button
        type="button"
        onclick={loadComments}
        class="text-xs text-fire-800 underline hover:no-underline"
      >{t(T.comments_retry)}</button>
    </div>
  {:else}
    {#if topLevel.length === 0}
      <div class="flex flex-col items-center gap-2 py-8 text-center sm:flex-row sm:items-center sm:text-left sm:py-4 sm:gap-3">
        <Icon icon="mdi:comment-text-outline" class="size-10 text-stein-300 shrink-0" />
        <p class="text-sm text-stein-500">{t(T.comments_empty)}</p>
      </div>
    {:else}
      <ul class="space-y-5 mb-8 list-none p-0">
        {#each topLevel as c (c.id)}
          {@const replies = repliesOf(c.id)}
          <li
            class="group flex gap-3 list-none! {c.status === 'pending'
              ? 'rounded-xs border border-erde-200 bg-erde-50/50 p-3'
              : ''}"
          >
            {@render avatar(c.author)}
            <div class="min-w-0 flex-1">
              {@render meta(c)}
              {@render commentBody(c)}

              <div class="mt-2">
                <button
                  type="button"
                  onclick={() => (replyTo = replyTo === c.id ? null : c.id)}
                  class="text-xs text-stein-600 hover:text-stein-900 inline-flex items-center gap-1"
                >
                  <Icon icon="mdi:reply" class="size-3" />
                  {replyTo === c.id
                    ? t(T.comments_cancel)
                    : replies.length === 0
                      ? t(T.comments_reply)
                      : replies.length === 1
                        ? t(T.comments_reply_count_one)
                        : t(T.comments_reply_count_other, { count: replies.length })}
                </button>
              </div>

              {#if replyTo === c.id}
                <form onsubmit={(e) => onReply(e, c.id)} class="mt-3 space-y-2">
                  <input
                    type="text"
                    bind:value={replyAuthor}
                    placeholder={t(T.comments_name_placeholder)}
                    required
                    maxlength="80"
                    class="w-full p-2 border border-stein-300 rounded-xs text-sm focus:outline-none focus:ring-2 focus:ring-stein-400"
                  />
                  <textarea
                    bind:value={replyBody}
                    use:autosize
                    onkeydown={submitOnCmdEnter}
                    placeholder={t(T.comments_reply_placeholder)}
                    required
                    rows="2"
                    maxlength={bodyMax}
                    class="w-full p-2 border border-stein-300 rounded-xs text-sm focus:outline-none focus:ring-2 focus:ring-stein-400"
                  ></textarea>
                  {#if replyError}<p class="text-fire-700 text-xs">{replyError}</p>{/if}
                  <div class="flex items-center gap-2">
                    <button
                      type="submit"
                      disabled={submitting}
                      class="px-3 py-1.5 bg-stein-800 text-white rounded-xs text-sm font-medium hover:bg-stein-700 disabled:opacity-50"
                    >{t(T.comments_reply_send)}</button>
                    <span class="ml-auto text-xs text-stein-400 tabular-nums">{replyBody.length} / {bodyMax}</span>
                  </div>
                </form>
              {/if}

              {#if replies.length > 0}
                <ul class="mt-4 space-y-4 list-none p-0 border-l border-stein-200 pl-4">
                  {#each replies as r (r.id)}
                    <li
                      class="group flex gap-3 list-none! {r.status === 'pending'
                        ? 'rounded-xs border border-erde-200 bg-erde-50/50 p-2'
                        : ''}"
                    >
                      {@render avatar(r.author, "sm")}
                      <div class="min-w-0 flex-1">
                        {@render meta(r)}
                        {@render commentBody(r)}
                      </div>
                    </li>
                  {/each}
                </ul>
              {/if}
            </div>
          </li>
        {/each}
      </ul>
    {/if}

    <form onsubmit={onSubmit} class="border-t border-stein-200 pt-5 space-y-3">
      <h3 class="font-medium text-sm text-stein-900">{t(T.comments_form_title)}</h3>
      <input
        type="text"
        bind:value={author}
        placeholder={t(T.comments_name_placeholder)}
        required
        maxlength="80"
        class="w-full p-2 border border-stein-300 rounded-xs text-sm focus:outline-none focus:ring-2 focus:ring-stein-400"
      />
      <textarea
        bind:value={body}
        use:autosize
        onkeydown={submitOnCmdEnter}
        placeholder={t(T.comments_body_placeholder)}
        required
        rows="3"
        maxlength={bodyMax}
        class="w-full p-2 border border-stein-300 rounded-xs text-sm focus:outline-none focus:ring-2 focus:ring-stein-400"
      ></textarea>
      <input
        type="text"
        bind:value={website}
        tabindex="-1"
        autocomplete="off"
        aria-hidden="true"
        class="absolute left-[-9999px]"
      />
      {#if submitError}
        <p class="text-fire-700 text-xs">{submitError}</p>
      {/if}
      <div class="flex flex-wrap items-center gap-3 text-xs">
        <button
          type="submit"
          disabled={submitting || !body.trim() || !author.trim()}
          class="px-4 py-2 bg-stein-800 text-white rounded-xs text-sm font-medium hover:bg-stein-700 disabled:opacity-50 disabled:cursor-not-allowed"
        >
          {submitting ? t(T.comments_sending) : t(T.comments_send)}
        </button>
        {#if requireApproval}
          <span class="text-stein-500 inline-flex items-center gap-1">
            <Icon icon="mdi:shield-check-outline" class="size-3.5" />
            {t(T.comments_approval_hint)}
          </span>
        {/if}
        <span class="ml-auto text-stein-400 tabular-nums">{body.length} / {bodyMax}</span>
      </div>
      {#if totalPending > 0}
        <p class="text-xs text-stein-500 italic">
          {totalPending === 1
            ? t(T.comments_pending_summary_one)
            : t(T.comments_pending_summary_other, { count: totalPending })}
        </p>
      {/if}
    </form>
  {/if}
</div>
