import type { InteractionResult } from '@genesislcap/foundation-ai';
import { customElement, GenesisElement, observable } from '@genesislcap/web-core';
import { createInteractionContext } from '../../state/interaction-context';
import type { AiChatInteraction, AiChatWidget } from '../../types/ai-chat-widget';
import type { InteractionContext } from '../../types/interaction-context';
import { logger } from '../../utils/logger';
import { AiChatInteractionWrapperStyles } from './chat-interaction-wrapper.styles';
import { AiChatInteractionWrapperTemplate } from './chat-interaction-wrapper.template';

/** Structural view of the assistant host, pierced across the wrapper's shadow boundary. */
type InteractionHost = {
  isEngaged?: boolean;
  getInteractionContext?: (id: string) => InteractionContext | undefined;
};

export type ChatInteractionEventsMap = {
  'interaction-completed': InteractionResult<unknown> & { interactionId: string };
};

/**
 * Detail payload for the `interaction-completed` event.
 * @public
 */
export type InteractionCompletedEventDetail = ChatInteractionEventsMap['interaction-completed'];

/**
 * Hosts an interactive AI chat sub-component by tag name.
 * @public
 *
 * @fires interaction-completed - Fired when the interaction finishes. detail: `InteractionCompletedEventDetail`
 */
@customElement({
  name: 'ai-chat-interaction-wrapper',
  template: AiChatInteractionWrapperTemplate,
  styles: AiChatInteractionWrapperStyles,
})
export class AiChatInteractionWrapper extends GenesisElement {
  /** @internal */
  @observable componentName: string = '';
  /** @internal */
  @observable data: any;
  /** @internal */
  @observable interactionId: string = '';
  /** The resolved result once the interaction has completed. Forwarded to the rendered component. @internal */
  @observable resolved: InteractionResult<unknown> | undefined = undefined;
  /** @internal */
  container!: HTMLElement;

  /** Observes the rendered widget for mid-lifecycle size changes — see {@link observeWidgetResize}. */
  private _resizeObserver?: ResizeObserver;

  connectedCallback() {
    super.connectedCallback();
    this.renderComponent();
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this._resizeObserver?.disconnect();
    this._resizeObserver = undefined;
  }

  componentNameChanged() {
    this.renderComponent();
  }

  /**
   * Re-render the hosted widget when the interaction identity changes. This
   * ensures widgets don't go stale with them being rendered with `recycle: true`
   * when messages are dynamically filtered, showing/hiding thinking/tool calls
   *
   * `interactionId` is a per-interaction UUID, so it changes exactly when the
   * hosted interaction does. Property bindings apply in template order
   * (componentName, data, interactionId), so `data` is already rebound by the
   * time this fires and the fresh widget is built with the correct data.
   */
  interactionIdChanged() {
    this.renderComponent();
  }

  resolvedChanged() {
    const element = this.container?.firstElementChild as (AiChatWidget & HTMLElement) | null;
    // The interaction just resolved: forward the resolved descriptor to the LIVE widget
    // in place, preserving its DOM/state (no re-create). If no matching element exists
    // yet (e.g. a history re-render straight into the resolved state), render it fresh.
    if (element && element.tagName?.toLowerCase() === this.componentName) {
      if (this.resolved) {
        element.interaction = { status: 'resolved', data: this.data, result: this.resolved };
      }
    } else if (this.resolved) {
      this.renderComponent();
    }
  }

  /**
   * Watch the rendered widget element for box-size changes. When the widget
   * grows or shrinks mid-lifecycle (e.g. expanding a "More info" panel,
   * appending an SSE status row, revealing generated code), bubble a
   * `request-scroll-to-bottom` event so `FoundationAiAssistant` re-snaps to
   * the bottom. `scrollToBottom` respects `_userScrolledAway`, so a user
   * reading old messages is not yanked back.
   *
   * The first ResizeObserver callback fires synchronously with the initial
   * size when `observe()` is called; we skip it so freshly-rendered widgets
   * don't double up with the existing `handleMessagesUpdate` auto-scroll for
   * new messages.
   */
  private observeWidgetResize(element: Element) {
    this._resizeObserver?.disconnect();
    let initialFireSkipped = false;
    this._resizeObserver = new ResizeObserver(() => {
      if (!initialFireSkipped) {
        initialFireSkipped = true;
        return;
      }
      this.dispatchEvent(
        new CustomEvent('request-scroll-to-bottom', { bubbles: true, composed: true }),
      );
    });
    this._resizeObserver.observe(element);
  }

  private renderComponent() {
    if (!this.container || !this.componentName) return;

    // Clear children without using innerHTML
    this._resizeObserver?.disconnect();
    this._resizeObserver = undefined;
    this.container.replaceChildren();

    if (!customElements.get(this.componentName)) {
      logger.warn(
        `Interactive component "${this.componentName}" is not registered in the customElements registry.`,
      );
      const errorDiv = document.createElement('div');
      errorDiv.textContent = `Error: Component "${this.componentName}" is not available in this application.`;
      this.container.appendChild(errorDiv);
      return;
    }

    try {
      // Pierce the wrapper's shadow boundary to read host state: `isEngaged` (gates
      // autofocus) and the driver-owned interaction context. Single-view model
      // (GENC-1351): one assistant view always renders the live widget.
      const root = this.getRootNode();
      const host = root instanceof ShadowRoot ? (root.host as InteractionHost) : null;

      const element = document.createElement(this.componentName) as AiChatWidget & HTMLElement;

      // GENC-1390: the widget's whole input surface is the discriminated interaction
      // descriptor + its stable id. Rebuilt on every (re)mount; the pending variant's
      // `interactionContext` is the SAME driver-owned object across remounts, which is
      // what lets a widget's live state survive the element teardown/recreate.
      element.interactionId = this.interactionId;
      element.interaction = this.buildInteraction(host);

      // Handle completion from the inner component. Guard against re-emission if the
      // interaction was already resolved (auto-resolve widgets fire on connectedCallback
      // when rendering history).
      element.addEventListener('interaction-completed', (e: CustomEvent) => {
        e.stopPropagation();
        if (this.resolved) return;

        this.$emit('interaction-completed', {
          interactionId: this.interactionId,
          ...e.detail,
        });
      });

      this.container.appendChild(element as unknown as Node);
      this.observeWidgetResize(element as unknown as Element);
    } catch (e) {
      logger.error(`Failed to create interactive component: ${this.componentName}`, e);
      const errorDiv = document.createElement('div');
      errorDiv.textContent = `Error: Could not load component ${this.componentName}`;
      this.container.appendChild(errorDiv);
    }
  }

  /**
   * Project the wrapper's current `data`/`resolved`/`interactionId` into the
   * discriminated {@link AiChatInteraction} the widget consumes (GENC-1390).
   */
  private buildInteraction(host: InteractionHost | null): AiChatInteraction {
    if (this.resolved) {
      return { status: 'resolved', data: this.data, result: this.resolved };
    }
    // Pending. The driver creates a live context for EVERY pending interaction BEFORE the widget
    // mounts, returns the SAME instance on every remount, and drops it the instant the interaction
    // resolves (see `ChatDriver.resolveInteraction`). So a MISSING context is not "pending, context
    // lost" — it means this interaction is no longer live: it has resolved/evicted (the `resolved`
    // binding above can lag a render behind that during a layout rebuild), or the widget was rendered
    // outside the normal host flow (e.g. a unit test). In none of those cases may a `once`-guarded
    // side effect run — re-firing one re-opened a just-closed side panel, whose layout rebuild
    // re-mounted the widget, which re-opened it… a tab-locking loop (GENC-1390). Fall back to an
    // INERT context (disposed up front, so `once` refuses and `state`/subscribers are inert) rather
    // than a live throwaway, so the widget degrades safely instead of re-running its open-once effect.
    let interactionContext = host?.getInteractionContext?.(this.interactionId);
    if (!interactionContext) {
      logger.warn(
        `No live InteractionContext for interaction "${this.interactionId}"; using an inert context (once-guarded effects will not run).`,
      );
      const handle = createInteractionContext(this.interactionId);
      handle.dispose();
      interactionContext = handle.context;
    }
    return {
      status: 'pending',
      data: this.data,
      shouldAutoFocus: !!host?.isEngaged,
      interactionContext,
    };
  }
}
