<script lang="ts">
  import { marked } from "$lib/markdown-safe";
  import { getBlockLayoutClasses } from "$lib/block-layout";
  import type {
    SearchableTextBlockData,
    SearchableTextFragment,
  } from "$lib/block-types";
  import "$lib/iconify-offline";
  import Icon from "@iconify/svelte";
  import Tag from "../Tag.svelte";
  import Tags from "../Tags.svelte";
  import Tooltip from "$lib/ui/Tooltip.svelte";
  import { t as tStatic, T } from "$lib/translations";
  import type { Translations } from "$lib/translations";

  let {
    block,
    class: className = "",
    translations = {},
  }: {
    block: SearchableTextBlockData;
    class?: string;
    translations?: Translations | null;
  } = $props();

  function t(key: string, replacements?: Record<string, string | number>) {
    return tStatic(translations ?? null, key, replacements);
  }
  const helpTooltipText = $derived(t(T.searchable_text_help));

  const layoutClasses = $derived(getBlockLayoutClasses(block.layout));

  const fragments = $derived.by(() => {
    const raw = block.textFragments ?? [];
    return raw
      .filter(
        (f): f is SearchableTextFragment =>
          typeof f === "object" && f !== null && ("title" in f || "text" in f),
      )
      .map((f) => {
        const tagsRaw = f.tags ?? [];
        const tags = tagsRaw
          .map((t) =>
            typeof t === "string" ? t : ((t as { name?: string })?.name ?? ""),
          )
          .filter(Boolean);
        return {
          title: (f.title ?? "").trim(),
          text: (f.text ?? "").trim(),
          tags,
          textHtml: (f.text ?? "").trim()
            ? (marked.parse((f.text ?? "").trim()) as string)
            : "",
        };
      });
  });

  const allTags = $derived(
    [...new Set(fragments.flatMap((f) => f.tags))].sort((a, b) =>
      a.localeCompare(b),
    ),
  );

  let searchQuery = $state("");
  let selectedTag = $state("all");
  let copiedIndex = $state<number | null>(null);

  async function copyFragmentContent(
    fragment: { title: string; text: string },
    index: number,
  ) {
    const plain = [fragment.title, fragment.text].filter(Boolean).join("\n\n");
    try {
      await navigator.clipboard.writeText(plain);
      copiedIndex = index;
      setTimeout(() => (copiedIndex = null), 2000);
    } catch {
      try {
        const ta = document.createElement("textarea");
        ta.value = plain;
        ta.setAttribute("readonly", "");
        ta.style.position = "fixed";
        ta.style.opacity = "0";
        document.body.appendChild(ta);
        ta.select();
        document.execCommand("copy");
        document.body.removeChild(ta);
        copiedIndex = index;
        setTimeout(() => (copiedIndex = null), 2000);
      } catch {
        /* ignore */
      }
    }
  }

  function getVisibleFragments() {
    const q = searchQuery.toLowerCase().trim().replace(/\s+/g, " ");
    const tag = selectedTag;
    return fragments.filter((f) => {
      const tagMatch = tag === "all" || f.tags.includes(tag);
      if (!tagMatch) return false;
      if (q === "") return true;
      const titleNorm = (f.title ?? "").toLowerCase().replace(/\s+/g, " ");
      const textNorm = (f.text ?? "")
        .toLowerCase()
        .replace(/<[^>]+>/g, " ")
        .replace(/\s+/g, " ");
      return titleNorm.includes(q) || textNorm.includes(q);
    });
  }

  const visibleFragments = $derived(getVisibleFragments());

  const HIGHLIGHT_CLASS = "bg-wald-200 text-wald-900 rounded px-0.5";

  function highlightInText(text: string, term: string): string {
    if (!term.trim()) return escapeHtml(text);
    const escaped = escapeRegex(term);
    const re = new RegExp(escaped, "gi");
    return escapeHtml(text).replace(
      re,
      (match) => `<mark class="${HIGHLIGHT_CLASS}">${match}</mark>`,
    );
  }

  function highlightInHtml(html: string, term: string): string {
    if (!term.trim()) return html;
    const escaped = escapeRegex(term);
    const re = new RegExp(escaped, "gi");
    const parts = html.split(/(<[^>]+>)/g);
    return parts
      .map((part) => {
        if (part.startsWith("<")) return part;
        return part.replace(
          re,
          (match) => `<mark class="${HIGHLIGHT_CLASS}">${match}</mark>`,
        );
      })
      .join("");
  }

  function escapeHtml(s: string): string {
    return s
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  function escapeRegex(s: string): string {
    return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  const descriptionHtml = $derived(
    block.description && typeof block.description === "string"
      ? (marked.parse(block.description) as string)
      : "",
  );
</script>

<div
  class="searchable-text flex flex-col gap-0 border border-stein-200 bg-stein-50 {layoutClasses} {className}"
  data-block="SearchableText" data-block-type="searchable_text"
  data-block-slug={block._slug}
>
  <div class="p-4 md:p-6">
    {#if block.title}
      <h2 class="text-xl md:text-2xl font-bold text-stein-900 mb-2">
        {block.title}
      </h2>
    {/if}
    {#if descriptionHtml}
      <div class="markdown text-stein-600 max-w-none prose prose-zinc prose-sm">
        {@html descriptionHtml}
      </div>
    {/if}
  </div>

  <header
    class="sticky top-14 z-10 px-4 md:px-6 py-3 border-b border-stein-200 bg-stein-0/95 backdrop-blur-sm md:static md:bg-stein-100 md:backdrop-blur-none shadow-sm md:shadow-none"
  >
    <div class="flex flex-col gap-2">
      <div class="relative flex items-center gap-2">
        <Tooltip content={helpTooltipText} placement="top">
          <button
            type="button"
            class="shrink-0 p-1.5 rounded-xs text-himmel-500 hover:text-himmel-700 hover:bg-himmel-50 transition-colors focus:outline-none focus:ring-2 focus:ring-wald-500 focus:ring-offset-1"
            aria-label={t(T.searchable_text_help_aria)}
          >
            <Icon
              icon="mdi:help-circle-outline"
              class="size-5"
              aria-hidden="true"
            />
          </button>
        </Tooltip>
        <input
          type="search"
          placeholder={t(T.searchable_text_placeholder)}
          class="w-full text-sm px-4 py-2.5 pr-10 border border-stein-200 rounded-xs focus:outline-none focus:ring-2 focus:ring-wald-500 focus:border-wald-400 bg-stein-0 text-stein-900 placeholder:text-stein-400"
          bind:value={searchQuery}
          oninput={(e) => (searchQuery = (e.target as HTMLInputElement).value)}
          aria-label={t(T.searchable_text_search_aria)}
        />
        {#if searchQuery}
          <button
            type="button"
            class="absolute right-9 top-1/2 -translate-y-1/2 p-1.5 rounded-xs hover:bg-stein-200 text-stein-500 hover:text-stein-700 transition-colors"
            onclick={() => (searchQuery = "")}
            aria-label={t(T.searchable_text_clear_search)}
          >
            ✕
          </button>
        {/if}
        <Icon
          icon="mdi:magnify"
          class="absolute right-3 top-1/2 -translate-y-1/2 text-stein-400 pointer-events-none size-5"
          aria-hidden="true"
        />
      </div>
      {#if allTags.length > 0}
        <div class="overflow-x-auto -mx-1 px-1 pt-2 pb-2">
          <div class="flex gap-2 flex-wrap min-w-max md:flex-wrap md:w-auto">
            <Tag
              label={t(T.searchable_text_tag_all)}
              variant={selectedTag === "all" ? "green" : "white"}
              active={selectedTag === "all"}
              onclick={() => (selectedTag = "all")}
            />
            {#each allTags as tag}
              <Tag
                label={tag}
                variant={selectedTag === tag ? "green" : "white"}
                active={selectedTag === tag}
                onclick={() => (selectedTag = tag)}
              />
            {/each}
          </div>
        </div>
      {/if}
    </div>
  </header>

  <div
    class="text-xs text-stein-600 px-4 md:px-6 py-2.5 bg-stein-100 border-b border-stein-200"
    aria-live="polite"
  >
    {#if visibleFragments.length === 0}
      <span>{t(T.searchable_text_no_results)}</span>
    {:else}
      <span>
        {visibleFragments.length === fragments.length
          ? t(T.searchable_text_count_all, { count: visibleFragments.length })
          : t(T.searchable_text_count_filtered, {
              visible: visibleFragments.length,
              total: fragments.length,
            })}
      </span>
    {/if}
  </div>

  <div class="flex flex-col gap-0 bg-stein-100">
    {#each visibleFragments as fragment, i}
      <details class="group border-b border-stein-200 last:border-b-0">
        <summary
          class="cursor-pointer list-none px-4 md:px-6 py-3.5 bg-stein-50 hover:bg-wald-50 text-sm font-medium text-stein-800 flex flex-col gap-1.5 transition-colors"
        >
          <div class="flex items-start gap-2 min-w-0">
            <div class="grow min-w-0">
              {@html highlightInText(
                fragment.title || t(T.searchable_text_untitled),
                searchQuery.trim(),
              )}
            </div>
            <Icon
              icon="mdi:chevron-down"
              class="shrink-0 text-stein-500 group-open:rotate-180 transition-transform"
              aria-hidden="true"
            />
          </div>
          {#if fragment.tags.length > 0}
            <span class="flex flex-wrap gap-1">
              <Tags tags={fragment.tags} variant="white" />
            </span>
          {/if}
        </summary>
        <div
          class="px-4 md:px-6 py-4 bg-stein-0 markdown prose prose-zinc prose-sm max-w-none text-xs"
        >
          <div class="flex justify-end mb-3 -mt-1">
            <button
              type="button"
              class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xs border border-stein-200 bg-stein-50 text-stein-700 hover:bg-wald-50 hover:border-wald-200 hover:text-wald-800 transition-colors"
              onclick={(e) => {
                e.preventDefault();
                copyFragmentContent(fragment, i);
              }}
              aria-label={t(T.searchable_text_copy_aria)}
            >
              {#if copiedIndex === i}
                <Icon
                  icon="mdi:check"
                  class="size-4 text-wald-600"
                  aria-hidden="true"
                />
                <span>{t(T.searchable_text_copied)}</span>
              {:else}
                <Icon
                  icon="mdi:content-copy"
                  class="size-4"
                  aria-hidden="true"
                />
                <span>{t(T.searchable_text_copy_button)}</span>
              {/if}
            </button>
          </div>
          {@html highlightInHtml(fragment.textHtml, searchQuery.trim())}
        </div>
      </details>
    {/each}
  </div>
</div>
