/**
 * Non-side-effect class export for `<stream-ui>`.
 *
 * Importing this file gives you the class(es) without auto-registering the tag.
 * Useful for test isolation, subclassing with tag-name override, or selective
 * composition.
 *
 * The auto-register path stays at `@adia-ai/web-components/components/stream`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <stream-ui></stream-ui>
 * el.tokens = asyncIterable; // starts streaming
 *
 * Renders an AsyncIterable<string> as streaming text.
 * Used for agent prose, LLM output, real-time logs.
 *
 * Props (attributes):
 *   pace:        ms between character renders (default: 0, real-time)
 *   hide-cursor: hide the blinking cursor during stream (default: false — cursor visible)
 *
 * Props (JS only):
 *   .tokens: AsyncIterable<string> — the stream source
 *
 * Events:
 *   stream-start  — streaming begins
 *   stream-end    — stream completed
 *   stream-error  — stream error (detail: { error })
 */

import { UIElement } from '../../core/element.js';

export class UIStream extends UIElement {
  static properties = {
    pace:       { type: Number,  default: 0,     reflect: true },
    hideCursor: { type: Boolean, default: false, reflect: true, attribute: 'hide-cursor' },
    streaming:  { type: Boolean, default: false, reflect: true },
  };

  #tokens = null;
  #abortCtrl = null;
  #buffer = '';

  get tokens() { return this.#tokens; }
  set tokens(iterable) {
    this.#tokens = iterable;
    if (iterable) this.#startStream(iterable);
  }

  static template = () => null;

  async #startStream(iterable) {
    this.#abortCtrl?.abort();
    this.#abortCtrl = new AbortController();
    const signal = this.#abortCtrl.signal;

    this.#buffer = '';
    this.textContent = '';
    this.streaming = true;
    this.dispatchEvent(new Event('stream-start', { bubbles: true }));

    try {
      for await (const token of iterable) {
        if (signal.aborted) break;
        this.#buffer += token;

        if (this.pace > 0) {
          for (const char of token) {
            if (signal.aborted) break;
            this.textContent += char;
            await new Promise(r => setTimeout(r, this.pace));
          }
        } else {
          this.textContent = this.#buffer;
        }
      }
    } catch (err) {
      if (!signal.aborted) {
        this.dispatchEvent(new CustomEvent('stream-error', {
          bubbles: true,
          detail: { error: err },
        }));
      }
    }

    if (!signal.aborted) {
      this.streaming = false;
      this.dispatchEvent(new Event('stream-end', { bubbles: true }));
    }
  }

  stop() {
    this.#abortCtrl?.abort();
    this.streaming = false;
  }

  clear() {
    this.stop();
    this.#buffer = '';
    this.textContent = '';
  }

  disconnected() {
    this.stop();
  }
}
