<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { useClipboard } from '@vueuse/core';
import { codeToHtml } from 'shiki';
import { cn } from '@/lib/utils';
import { UiButton } from '@/components/UiButton';
import { UiIcon } from '@/components/UiIcon';
import type { UiCodeBlockProps } from './types';

defineOptions({
  name: 'UiCodeBlock',
  inheritAttrs: false,
});

const props = withDefaults(defineProps<UiCodeBlockProps>(), {
  language: 'text',
  filename: undefined,
  showLineNumbers: false,
  showCopyButton: true,
  maxHeight: undefined,
  copyLabel: 'Copy code',
  copiedLabel: 'Copied',
});

const highlightedHtml = ref('');
const isHighlighted = ref(false);
const isCopied = ref(false);

const { copy } = useClipboard();

const TOKEN_COLOR_CONTENT_ON_SURFACE_PRIMARY =
  'oklch(var(--content-on-surface-primary))';
const shikiTokenColorOverrides = {
  '#E36209': TOKEN_COLOR_CONTENT_ON_SURFACE_PRIMARY,
  '#FFAB70': TOKEN_COLOR_CONTENT_ON_SURFACE_PRIMARY,
} as const;

const headerLabel = computed(() => props.filename || props.language);
const showHeader = computed(
  () => headerLabel.value !== 'text' || props.showCopyButton,
);

let highlightRequestId = 0;

function normalizeShikiTokenColors(html: string) {
  return Object.entries(shikiTokenColorOverrides).reduce(
    (nextHtml, [from, to]) => nextHtml.replaceAll(from, to),
    html,
  );
}

async function highlightCode() {
  const requestId = ++highlightRequestId;
  isHighlighted.value = false;
  try {
    const html = await codeToHtml(props.code, {
      lang: props.language as Parameters<typeof codeToHtml>[1]['lang'],
      themes: {
        light: 'github-light',
        dark: 'github-dark',
      },
    });
    if (requestId !== highlightRequestId) return;
    highlightedHtml.value = normalizeShikiTokenColors(html);
    isHighlighted.value = true;
  } catch {
    if (requestId !== highlightRequestId) return;
    try {
      const html = await codeToHtml(props.code, {
        lang: 'text',
        themes: {
          light: 'github-light',
          dark: 'github-dark',
        },
      });
      if (requestId !== highlightRequestId) return;
      highlightedHtml.value = normalizeShikiTokenColors(html);
      isHighlighted.value = true;
    } catch {
      if (requestId !== highlightRequestId) return;
      highlightedHtml.value = '';
    }
  }
}

watch(() => [props.code, props.language], highlightCode);
onMounted(highlightCode);

let copyTimerId: ReturnType<typeof setTimeout> | undefined;

async function handleCopy() {
  try {
    await copy(props.code);
    isCopied.value = true;
    clearTimeout(copyTimerId);
    copyTimerId = setTimeout(() => {
      isCopied.value = false;
    }, 2000);
  } catch {
    // Clipboard access denied or unsupported - silently ignore
  }
}

onUnmounted(() => {
  highlightRequestId++;
  clearTimeout(copyTimerId);
});
</script>

<template>
  <div
    v-bind="$attrs"
    :class="
      cn(
        'ui-code-block overflow-hidden rounded-lg border border-border',
        $attrs.class ?? '',
      )
    "
  >
    <div
      v-if="showHeader"
      class="flex items-center justify-between border-b border-border bg-background-surface-muted px-4 py-2"
    >
      <span class="font-mono text-sm text-content-on-surface-secondary">
        {{ headerLabel !== 'text' ? headerLabel : '' }}
      </span>
      <UiButton
        v-if="showCopyButton"
        variant="ghost"
        size="icon-sm"
        :aria-label="isCopied ? copiedLabel : copyLabel"
        @click="handleCopy"
      >
        <UiIcon :name="isCopied ? 'check' : 'copy'" :size="14" />
      </UiButton>
    </div>

    <div
      class="overflow-auto"
      :style="maxHeight ? { maxHeight } : undefined"
      :tabindex="maxHeight ? 0 : undefined"
      :aria-label="maxHeight ? 'Scrollable code block' : undefined"
    >
      <!--
        `highlightedHtml` is produced by Shiki, which emits a sanitized
        `<pre><code>` tree from the raw `code` string. No user HTML is ever
        interpreted, so `v-html` is safe here.
      -->
      <!-- eslint-disable vue/no-v-html, sonarjs/no-vue-bypass-sanitization -->
      <div
        v-if="isHighlighted && highlightedHtml"
        :class="
          cn(
            'ui-code-block__content',
            showLineNumbers && 'ui-code-block__line-numbers',
          )
        "
        v-html="highlightedHtml"
      />
      <!-- eslint-enable vue/no-v-html, sonarjs/no-vue-bypass-sanitization -->
      <pre
        v-else
        class="m-0 bg-background p-4 font-mono text-sm leading-relaxed text-foreground"
      ><code>{{ code }}</code></pre>
    </div>
  </div>
</template>

<style scoped>
/* Dark mode: :global() needed because html.dark is outside component scope */
:global(html.dark .ui-code-block .shiki),
:global(html.dark .ui-code-block .shiki span) {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
}

/* :deep() needed to style v-html content */
.ui-code-block__content :deep(.shiki) {
  margin: 0;
  padding: 1rem;
  font-size: 0.875rem;
  line-height: 1.625;
  overflow-x: auto;
}

.ui-code-block__content :deep(.shiki code) {
  display: block;
  font-family:
    ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono',
    monospace;
}

/* Line numbers via CSS counter */
.ui-code-block__line-numbers :deep(.shiki code) {
  counter-reset: line;
}

.ui-code-block__line-numbers :deep(.shiki code .line::before) {
  counter-increment: line;
  content: counter(line);
  display: inline-block;
  width: 2rem;
  margin-right: 1rem;
  text-align: right;
  opacity: 0.4;
}
</style>
