/**
 * Renderer creates and updates message DOM elements from templates.
 * Handles streaming bubbles, attachments, and play-button state.
 * Takes the conversation tree as a dependency.
 */

import { extractText } from './conversation.js';

const ICONS_URL = '/static/icons.svg';

export class Renderer {
  /**
   * @param {Object} options
   * @param {Document} options.document
   * @param {import('./conversation.js').ConversationTree} options.conversation
   */
  constructor({ document, conversation }) {
    this.document = document;
    this.conversation = conversation;

    // DOM Elements
    this.$chat = document.getElementById('chat');
    this.$chatLoading = document.getElementById('chat-loading');
    this.$attachments = document.getElementById('attachments');
    this.$scrollDown = document.getElementById('scroll-down');
    this.$voice = document.getElementById('voice');

    // Auto-scroll follow state. Following is opt-in, so it starts off; the
    // user engages it via the scroll button.
    this.isAutoScrollEnabled = false;

    // Templates
    this.$tplAssistantMessage = document.getElementById(
      'tpl-assistant-message',
    );
    this.$tplAttachmentChip = document.getElementById('tpl-attachment-chip');
    this.$tplImagePill = document.getElementById('tpl-image-pill');
    this.$tplDebugRow = document.getElementById('tpl-debug-row');
    this.$tplErrorMessage = document.getElementById('tpl-error-message');
    this.$tplUserMessage = document.getElementById('tpl-user-message');
    this.$tplReasoningMessage = document.getElementById(
      'tpl-reasoning-message',
    );
    this.$tplToolMessage = document.getElementById('tpl-tool-message');
    this.$tplSystemMessage = document.getElementById('tpl-system-message');
  }

  // --- Full / incremental render ---

  /**
   * renderMessages renders messages to the chat container.
   * Messages already in the DOM (matched by data-id) are skipped.
   * @param {Object[]} messages
   * @param {Object} [meta]
   */
  renderMessages(messages, meta = {}) {
    const toolResultMap = new Map();
    for (const msg of messages) {
      if (msg.role === 'tool' && msg.tool_call_id) {
        toolResultMap.set(msg.tool_call_id, msg);
      }
    }

    for (const msg of messages) {
      if (msg.role === 'user') {
        if (msg.id && this.$chat.querySelector(`[data-id="${msg.id}"]`)) {
          continue;
        }
        this._renderUserMessage(msg, msg.meta ?? meta);
        continue;
      }
      if (msg.role !== 'assistant') continue;

      const hasContent =
        msg.content || msg.reasoning_content || msg.tool_calls?.length;
      if (!hasContent) continue;

      const assistantId = msg.id;
      const bubbleMeta = msg.meta ?? meta;

      if (msg.reasoning_content) {
        const reasoningId = `${assistantId}/reasoning`;
        if (!this.$chat.querySelector(`[data-id="${reasoningId}"]`)) {
          this._renderReasoningMessage(msg, bubbleMeta);
        }
      }

      const textContent = extractText(msg.content);
      if (textContent) {
        if (
          !assistantId ||
          !this.$chat.querySelector(`[data-id="${assistantId}"]`)
        ) {
          this._renderAssistantMessage(msg, bubbleMeta);
        }
      }

      if (msg.tool_calls?.length > 0) {
        for (const toolCall of msg.tool_calls) {
          const result = toolResultMap.get(toolCall.id);
          const toolBubbleId = result?.id || toolCall.id;
          if (!this.$chat.querySelector(`[data-id="${toolBubbleId}"]`)) {
            this._renderToolMessage(msg, toolCall, result, bubbleMeta);
          }
        }
      }
    }
  }

  /**
   * reRenderActivePath re-renders the entire active conversation path.
   */
  reRenderActivePath() {
    const $loading = this.$chatLoading;
    this.$chat.innerHTML = '';
    this.$chat.appendChild($loading);
    this._renderTree(this.conversation.rootId);
    this.scrollToBottom();
  }

  /**
   * reRenderMessage replaces the DOM element(s) for a message.
   * @param {Object} message
   */
  reRenderMessage(message) {
    if (message.role === 'user') {
      const $old = this.$chat.querySelector(`[data-id="${message.id}"]`);
      if (!$old) return;
      const $msg =
        this.$tplUserMessage.content.cloneNode(true).firstElementChild;
      if (message.id) $msg.dataset.id = message.id;

      const content = extractText(message.content);
      $msg.querySelector('.message__content').textContent = content;

      this._renderImagePills($msg, message);

      const $dl = $msg.querySelector('.message__debug-list');
      if (message.meta?.language)
        this._appendDebugRow($dl, 'Language', message.meta.language);
      if (message.meta?.voice)
        this._appendDebugRow($dl, 'Voice', message.meta.voice);
      if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

      $old.replaceWith($msg);
    } else if (message.role === 'assistant') {
      const $textBubble = this.$chat.querySelector(`[data-id="${message.id}"]`);
      const $reasoningBubble = this.$chat.querySelector(
        `[data-id="${message.id}/reasoning"]`,
      );
      const $toolBubbles = this.$chat.querySelectorAll(
        `[data-parent-id="${message.id}"]`,
      );

      let $reference = $reasoningBubble || $textBubble;
      if (!$reference && $toolBubbles.length > 0) {
        $reference = $toolBubbles[0];
      }
      if (!$reference) return;

      const $toRemove = [];
      if ($reasoningBubble) $toRemove.push($reasoningBubble);
      if ($textBubble) $toRemove.push($textBubble);
      for (const $tool of $toolBubbles) {
        $toRemove.push($tool);
      }

      const toolResultMap = new Map();
      if (message.tool_calls?.length > 0) {
        const toolCallIds = new Set(message.tool_calls.map((tc) => tc.id));
        for (const result of this.conversation.messagesMap.values()) {
          if (result.role === 'tool' && toolCallIds.has(result.tool_call_id)) {
            toolResultMap.set(result.tool_call_id, result);
          }
        }
      }

      const bubbleMeta = message.meta ?? {};

      for (const $el of $toRemove) {
        $el.remove();
      }

      if (message.reasoning_content) {
        this._renderReasoningMessage(message, bubbleMeta);
      }

      const textContent = extractText(message.content);
      if (textContent) {
        this._renderAssistantMessage(message, bubbleMeta);
      }

      if (message.tool_calls?.length > 0) {
        for (const toolCall of message.tool_calls) {
          const result = toolResultMap.get(toolCall.id);
          this._renderToolMessage(message, toolCall, result, bubbleMeta);
        }
      }
    }
  }

  // --- Streaming bubbles ---

  /**
   * createStreamingReasoning creates a streaming reasoning bubble.
   * @param {string} assistantId
   * @param {Object} [meta]
   * @returns {HTMLElement}
   */
  createStreamingReasoning(assistantId, meta = null) {
    const $msg =
      this.$tplReasoningMessage.content.cloneNode(true).firstElementChild;
    $msg.dataset.id = `${assistantId}/reasoning`;
    $msg.dataset.parentId = assistantId;
    $msg.dataset.field = 'reasoning';
    $msg.classList.add('message--streaming');

    $msg.querySelector('.message__collapsible').open = true;

    const $dl = $msg.querySelector('.message__debug-list');
    if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice);
    if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

    // Remove action buttons during streaming.
    $msg.querySelector('[data-action="delete"]').remove();
    $msg.querySelector('[data-action="edit"]').remove();
    $msg.querySelector('[data-action="copy"]').remove();

    this.$chat.insertBefore($msg, this.$chatLoading);
    this.scrollToBottom();
    return $msg;
  }

  /**
   * createStreamingText creates a streaming text content bubble.
   * @param {Object} message
   * @param {Object} [meta]
   * @returns {HTMLElement}
   */
  createStreamingText(message, meta = null) {
    const $msg =
      this.$tplAssistantMessage.content.cloneNode(true).firstElementChild;
    if (message.id) $msg.dataset.id = message.id;
    $msg.classList.add('message--streaming');

    const $dl = $msg.querySelector('.message__debug-list');
    if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice);
    if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

    // Remove action buttons during streaming.
    $msg.querySelector('[data-action="play"]').remove();
    $msg.querySelector('[data-action="copy"]').remove();
    $msg.querySelector('[data-action="delete"]').remove();
    $msg.querySelector('[data-action="edit"]').remove();

    this.$chat.insertBefore($msg, this.$chatLoading);
    this.scrollToBottom();
    return $msg;
  }

  /**
   * appendDelta appends text to a streaming assistant message.
   * @param {HTMLElement} $msg
   * @param {string} delta
   */
  appendDelta($msg, delta) {
    const $content = $msg.querySelector('.message__content');
    $content.textContent += delta;
    this.scrollToBottom();
  }

  /**
   * appendReasoningDelta appends reasoning text to a streaming bubble.
   * @param {HTMLElement} $msg
   * @param {string} delta
   */
  appendReasoningDelta($msg, delta) {
    const $content = $msg.querySelector('.message__content');
    $content.textContent += delta;
    this.scrollToBottom();
  }

  /**
   * finalizeStreamingReasoning replaces a streaming reasoning bubble
   * with a fully bound one.
   * @param {HTMLElement} $streaming
   * @param {Object} message
   * @param {Object} [meta]
   * @returns {HTMLElement|null}
   */
  finalizeStreamingReasoning($streaming, message, meta = null) {
    if (!$streaming) return null;

    const $msg =
      this.$tplReasoningMessage.content.cloneNode(true).firstElementChild;
    $msg.dataset.id = `${message.id}/reasoning`;
    $msg.dataset.parentId = message.id;
    $msg.dataset.field = 'reasoning';
    $msg.dataset.messageId = message.id;

    $msg.querySelector('.message__content').textContent =
      message.reasoning_content || '';
    $msg.querySelector('.message__collapsible').open = false;

    const $dl = $msg.querySelector('.message__debug-list');
    if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice);
    if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

    $msg.dataset.copyText = message.reasoning_content || '';

    $streaming.replaceWith($msg);
    this.scrollToBottom();
    return $msg;
  }

  /**
   * finalizeStreamingText replaces a streaming text bubble with a
   * fully bound one.
   * @param {HTMLElement} $streaming
   * @param {Object} message
   * @param {Object} [meta]
   * @returns {HTMLElement|null}
   */
  finalizeStreamingText($streaming, message, meta = null) {
    if (!$streaming) return null;

    const $msg =
      this.$tplAssistantMessage.content.cloneNode(true).firstElementChild;
    if (message.id) $msg.dataset.id = message.id;

    const $content = $msg.querySelector('.message__content');
    const text = extractText(message.content);
    if (text) {
      $content.textContent = text;
    } else {
      $content.remove();
    }

    const $dl = $msg.querySelector('.message__debug-list');
    this._populateDebugPanel($dl, meta);
    if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

    const voice = meta?.voice || this.$voice.value || '';
    $msg.dataset.voice = voice;
    $msg.dataset.copyText = text || '';

    $streaming.replaceWith($msg);
    this.scrollToBottom();
    return $msg;
  }

  // --- Attachments ---

  /**
   * renderAttachments renders the attachment chips in the footer.
   * @param {File[]} attachments
   * @param {number} attachments.length
   * @param {Function} onRemove - Called with the index to remove.
   */
  renderAttachments(attachments, onRemove) {
    this.$attachments.innerHTML = '';

    for (let i = 0; i < attachments.length; i++) {
      const file = attachments[i];
      const $chip =
        this.$tplAttachmentChip.content.cloneNode(true).firstElementChild;

      const $name = $chip.querySelector('.compose__attachment-name');
      $name.textContent = file.name;
      $name.title = file.name;

      const $remove = $chip.querySelector('.compose__attachment-remove');
      $remove.setAttribute('aria-label', `Remove ${file.name}`);
      $remove.addEventListener('click', () => onRemove(i));

      this.$attachments.appendChild($chip);
    }
  }

  // --- Play button ---

  /**
   * setPlayButton updates the play/stop button icon for a message.
   * @param {string} messageId
   * @param {boolean} playing
   */
  setPlayButton(messageId, playing) {
    const $msg = this.$chat.querySelector(`[data-id="${messageId}"]`);
    if (!$msg) return;
    const $btn = $msg.querySelector('[data-action="play"]');
    if (!$btn) return;

    if (playing) {
      $btn.classList.add('playing');
      $btn.innerHTML = `<svg class="icon"><use href="${ICONS_URL}#stop"></use></svg>`;
      $btn.setAttribute('aria-label', 'Stop');
    } else {
      $btn.classList.remove('playing');
      $btn.innerHTML = `<svg class="icon"><use href="${ICONS_URL}#volume"></use></svg>`;
      $btn.setAttribute('aria-label', 'Play');
    }
  }

  // --- Error ---

  /**
   * renderError renders an error message in the chat.
   * @param {string} message
   */
  renderError(message, requestId = '') {
    const $msg =
      this.$tplErrorMessage.content.cloneNode(true).firstElementChild;
    const $content = $msg.querySelector('.message__content');
    if (requestId) {
      $content.textContent = `Request: ${requestId}\n${message}`;
      $msg.dataset.copyText = `Request: ${requestId}\n${message}`;
    } else {
      $content.textContent = message;
      $msg.dataset.copyText = message;
    }
    this.$chat.insertBefore($msg, this.$chatLoading);
    this.scrollToBottom();
  }

  // --- Scroll ---

  scrollToBottom() {
    if (this.isAutoScrollEnabled) {
      this.$chat.scrollTop = this.$chat.scrollHeight;
    }
  }

  // Anchor the last user message to the top of the chat viewport. Uses direct
  // scrollTop math (not scrollIntoView) to avoid moving ancestors/footer on
  // iOS. No-op if the message is already fully visible.
  scrollToLastUserMessage() {
    const messages = this.$chat.querySelectorAll('.message--user');
    const $last = messages[messages.length - 1];
    if (!$last) return;

    const { scrollTop, clientHeight } = this.$chat;
    const top = $last.offsetTop - this.$chat.offsetTop;
    const bottom = top + $last.offsetHeight;
    const fullyVisible = top >= scrollTop && bottom <= scrollTop + clientHeight;
    if (fullyVisible) return;

    this.$chat.scrollTop = top;
  }

  // Icon and label are driven by follow state, not scroll position, so they
  // never drift from isAutoScrollEnabled.
  updateFollowIndicator() {
    this.$scrollDown.classList.toggle('following', this.isAutoScrollEnabled);
  }

  // Position-based, disable-only. A user scroll-up past the threshold stops
  // following. Never re-enables (a programmatic scroll-to-bottom lands at ~0px,
  // so it cannot false-trigger; being near the bottom cannot re-enable).
  handleChatScroll() {
    const { scrollHeight, scrollTop, clientHeight } = this.$chat;
    if (scrollHeight - scrollTop - clientHeight > 100) {
      this.setAutoScroll(false);
    }
  }

  // Set the flag and sync the icon so the two never drift.
  setAutoScroll(enabled) {
    this.isAutoScrollEnabled = enabled;
    this.updateFollowIndicator();
  }

  clearChat() {
    const $loading = this.$chatLoading;
    this.$chat.innerHTML = '';
    this.$chat.appendChild($loading);
  }

  // --- Private ---

  _renderTree(messageId) {
    const messages = [];
    this._collectMessages(messageId, messages);
    this.renderMessages(messages);
  }

  _collectMessages(messageId, messages) {
    if (!messageId) return;
    const msg = this.conversation.get(messageId);
    if (!msg) return;
    messages.push(msg);
    if (msg.children) {
      for (const childId of msg.children) {
        this._collectMessages(childId, messages);
      }
    }
  }

  _renderUserMessage(message, meta = null) {
    const content = (() => {
      if (typeof message.content === 'string') {
        return message.content;
      }
      if (Array.isArray(message.content)) {
        return message.content
          .filter((p) => p.type === 'text')
          .map((p) => p.text)
          .join('\n');
      }
      return '';
    })();

    const $msg = this.$tplUserMessage.content.cloneNode(true).firstElementChild;
    if (message.id) $msg.dataset.id = message.id;

    $msg.querySelector('.message__content').textContent = content;
    $msg.dataset.copyText = content;

    this._renderImagePills($msg, message);

    const $dl = $msg.querySelector('.message__debug-list');
    if (meta?.language) this._appendDebugRow($dl, 'Language', meta.language);
    if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice);
    if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

    this.$chat.insertBefore($msg, this.$chatLoading);
    this.scrollToBottom();
  }

  _renderAssistantMessage(message, meta = null) {
    const $msg =
      this.$tplAssistantMessage.content.cloneNode(true).firstElementChild;
    if (message.id) $msg.dataset.id = message.id;

    const $content = $msg.querySelector('.message__content');
    const text = extractText(message.content);
    if (text) {
      $content.textContent = text;
    } else {
      $content.remove();
    }

    const $dl = $msg.querySelector('.message__debug-list');
    this._populateDebugPanel($dl, meta);
    if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

    const voice = meta?.voice || this.$voice.value || '';
    $msg.dataset.voice = voice;
    $msg.dataset.copyText = text || '';

    this.$chat.insertBefore($msg, this.$chatLoading);
    this.scrollToBottom();
  }

  _renderReasoningMessage(message, meta = null) {
    const $msg =
      this.$tplReasoningMessage.content.cloneNode(true).firstElementChild;
    $msg.dataset.id = `${message.id}/reasoning`;
    $msg.dataset.parentId = message.id;
    $msg.dataset.field = 'reasoning';
    $msg.dataset.messageId = message.id;

    $msg.querySelector('.message__content').textContent =
      message.reasoning_content || '';
    $msg.dataset.copyText = message.reasoning_content || '';

    const $dl = $msg.querySelector('.message__debug-list');
    if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice);
    if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

    this.$chat.insertBefore($msg, this.$chatLoading);
    this.scrollToBottom();
  }

  _renderToolMessage(assistantMessage, toolCall, result = null, meta = null) {
    const $msg = this.$tplToolMessage.content.cloneNode(true).firstElementChild;
    const toolBubbleId = result?.id || toolCall.id;
    $msg.dataset.id = toolBubbleId;
    $msg.dataset.parentId = assistantMessage.id;
    $msg.dataset.field = 'tool';
    $msg.dataset.messageId = assistantMessage.id;
    $msg.dataset.toolCallId = toolCall.id;

    $msg.querySelector('.message__collapsible-label').textContent =
      toolCall.function?.name || 'Tool Call';

    const $args = $msg.querySelector('[data-args]');
    try {
      const args = JSON.parse(toolCall.function?.arguments || '{}');
      $args.textContent = JSON.stringify(args, null, 2);
    } catch {
      $args.textContent = toolCall.function?.arguments || '';
    }

    if (result) {
      const $outputSection = $msg.querySelector(
        '.message__tool-section--output',
      );
      $outputSection.hidden = false;

      const $output = $msg.querySelector('[data-output]');
      try {
        const output = JSON.parse(extractText(result.content) || '{}');
        $output.textContent = JSON.stringify(output, null, 2);
      } catch {
        $output.textContent = extractText(result.content) || '';
      }
    }

    // Set copy text.
    const copyParts = [];
    const name = toolCall.function?.name || 'unknown';
    const args = toolCall.function?.arguments || '{}';
    copyParts.push(`${name}(${args})`);
    if (result) {
      copyParts.push(extractText(result.content) || '');
    }
    $msg.dataset.copyText = copyParts.join('\n\n');

    const $dl = $msg.querySelector('.message__debug-list');
    if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice);
    if ($dl.children.length === 0) this._appendDebugRow($dl, 'No data', '');

    this.$chat.insertBefore($msg, this.$chatLoading);
    this.scrollToBottom();
  }

  /**
   * renderSystemMessage renders the system message block.
   * @param {string} content
   */
  renderSystemMessage(content) {
    this._renderSystemMessage(content);
  }

  /**
   * reRenderSystemMessage replaces the system message block in the DOM.
   * @param {string} content
   */
  reRenderSystemMessage(content) {
    const $old = this.$chat.querySelector('[data-id="system"]');
    if (!$old) return;

    const $msg =
      this.$tplSystemMessage.content.cloneNode(true).firstElementChild;
    $msg.dataset.id = 'system';

    const $content = $msg.querySelector('.message__content');
    if (content?.trim()) {
      $content.textContent = content;
      $msg.dataset.copyText = content;
    } else {
      $content.innerHTML =
        '<em class="message__placeholder">System Default</em>';
      $msg.dataset.copyText = '';
    }

    $old.replaceWith($msg);
  }

  _renderSystemMessage(content, _meta = null) {
    const $msg =
      this.$tplSystemMessage.content.cloneNode(true).firstElementChild;
    $msg.dataset.id = 'system';

    const $content = $msg.querySelector('.message__content');
    if (content?.trim()) {
      $content.textContent = content;
      $msg.dataset.copyText = content;
    } else {
      $content.innerHTML =
        '<em class="message__placeholder">System Default</em>';
      $msg.dataset.copyText = '';
    }

    this.$chat.insertBefore($msg, this.$chatLoading);
  }

  _renderImagePills($msg, message) {
    if (!Array.isArray(message.content)) return;
    const imageParts = message.content.filter((p) => p.type === 'image_url');
    if (imageParts.length === 0) return;

    const $wrapper = document.createElement('div');
    $wrapper.className = 'message__image-pills';
    for (const part of imageParts) {
      const $pill = this.$tplImagePill.content.cloneNode(true);
      $pill.querySelector('.message__image-pill-name').textContent =
        part.filename || 'Image';
      $wrapper.appendChild($pill.firstElementChild);
    }
    $msg
      .querySelector('.message__body')
      .insertBefore($wrapper, $msg.querySelector('.message__actions'));
  }

  _populateDebugPanel($dl, meta) {
    if (meta?.voice) this._appendDebugRow($dl, 'Voice', meta.voice);
    if (meta?.usage) {
      const u = meta.usage;
      this._appendDebugRow($dl, 'Prompt tokens', String(u.prompt_tokens));
      this._appendDebugRow(
        $dl,
        'Completion tokens',
        String(u.completion_tokens),
      );
      this._appendDebugRow($dl, 'Total tokens', String(u.total_tokens));
      if (u.completion_tokens_details?.reasoning_tokens > 0)
        this._appendDebugRow(
          $dl,
          'Reasoning tokens',
          String(u.completion_tokens_details.reasoning_tokens),
        );
      if (u.prompt_tokens_details?.cached_tokens > 0)
        this._appendDebugRow(
          $dl,
          'Cached tokens',
          String(u.prompt_tokens_details.cached_tokens),
        );
    }
    if (meta?.timings) {
      const t = meta.timings;
      if (t.prompt_ms > 0)
        this._appendDebugRow(
          $dl,
          'Prompt eval',
          `${t.prompt_ms.toFixed(1)} ms (${t.prompt_per_second.toFixed(0)} tok/s)`,
        );
      if (t.predicted_ms > 0)
        this._appendDebugRow(
          $dl,
          'Predicted eval',
          `${t.predicted_ms.toFixed(1)} ms (${t.predicted_per_second.toFixed(0)} tok/s)`,
        );
    }
  }

  _appendDebugRow($dl, label, value) {
    const $row = this.$tplDebugRow.content.cloneNode(true);
    $row.querySelector('dt').textContent = label;
    $row.querySelector('dd').textContent = value;
    $dl.appendChild($row);
  }

  _icon(name) {
    const svg = this.document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    svg.setAttribute('class', 'icon');

    const use = this.document.createElementNS(
      'http://www.w3.org/2000/svg',
      'use',
    );
    use.setAttribute('href', `${ICONS_URL}#${name}`);

    svg.appendChild(use);
    return svg;
  }

  copyToClipboard($button, text) {
    navigator.clipboard.writeText(text).then(
      () => {
        $button.replaceChildren(this._icon('check'));
        $button.classList.add('message__action-btn--success');
        setTimeout(() => {
          $button.replaceChildren(this._icon('copy'));
          $button.classList.remove('message__action-btn--success');
        }, 1500);
      },
      (e) => console.error('failed to copy:', e),
    );
  }
}
