import { customElement, GenesisElement, html, observable } from '@genesislcap/web-core';
import { agenticActivityBus } from '../../channel/ai-activity-bus';

/**
 * Contract for pages that support docking the AI assistant.
 * Register an implementation with {@link getAiPopoutManager} on mount and deregister on unmount.
 *
 * @beta
 */
export interface AiDockProvider {
  /**
   * Called when the user expands the bubble. The provider **moves the given
   * element** into its layout — e.g. via a layout factory that re-parents it into
   * a pane. It is the ONE live assistant element, so it must be moved by reference,
   * **not cloned** (a clone would be a second, dead instance). Session state
   * survives the move because it lives in module-level registries, not the element.
   */
  onDock(element: HTMLElement): Promise<void>;
  /**
   * Called when the user collapses the panel or before navigation. By the time
   * this runs the manager has already moved the assistant element back into the
   * bubble, so the provider only needs to tear down its now-empty layout slot.
   */
  onUndock(): Promise<void>;
  /**
   * Called by `collapseIfDocked` before navigation. The provider is responsible for
   * finding the docked assistant element and calling `handlePopout()` on it, which
   * publishes `chat-popin` and returns the assistant to the bubble.
   */
  initiateCollapse(): void;
}

/**
 * App-shell component that owns the pop-out/pop-in lifecycle for the AI assistant bubble.
 *
 * @remarks
 * Place this in the persistent app shell, wrapping a `foundation-ai-chat-bubble` that
 * contains an AI assistant element in the `dialog-content` slot.
 *
 * GENC-1351 (Option A — single element): rather than cloning a second assistant to
 * dock, this manager **moves the one real element** between the bubble and the page
 * layout. One connected instance means no dual-view coordination (no shared-driver
 * unwire/rewire, host election, or duplicated side-effects); the session survives the
 * move because the driver/store/persister live in module-level registries keyed by the
 * element's identity.
 *
 * Pages that support docking call `aiPopoutManager.registerDockProvider()` on mount
 * and `aiPopoutManager.deregisterDockProvider()` on unmount.
 *
 * @example
 * ```html
 * <foundation-ai-popout-manager>
 *   <foundation-ai-chat-bubble title="My Assistant">
 *     <my-assistant slot="dialog-content"></my-assistant>
 *   </foundation-ai-chat-bubble>
 * </foundation-ai-popout-manager>
 * ```
 *
 * @beta
 */
@customElement({
  name: 'foundation-ai-popout-manager',
  template: html`
    <slot></slot>
  `,
})
export class FoundationAiPopoutManager extends GenesisElement {
  /** True when a dock provider is registered — controls expand button visibility. */
  @observable canDock = false;

  /**
   * The single assistant element. Captured while it's in the bubble slot and held
   * across the move (it's the same instance whether docked or in the bubble), so
   * pop-in can re-home it even though it's no longer a child of this manager.
   */
  private assistantEl: HTMLElement | null = null;
  private dockProvider: AiDockProvider | null = null;
  private isDocked = false;
  private unsubBus?: () => void;

  connectedCallback() {
    super.connectedCallback();
    // oxlint-disable-next-line typescript/no-this-alias -- module-level singleton, not a local alias
    _aiPopoutManager = this;

    this.assistantEl = this.findAssistant();

    const unsubPopout = agenticActivityBus.subscribe('chat-popout', async () => {
      if (this.isDocked || !this.dockProvider) return;
      const el = (this.assistantEl ??= this.findAssistant());
      if (!el) return;
      this.isDocked = true;
      _aiAssistantDocked = true;
      this.syncBubbleVisibility();
      // Reflect the new location on the assistant so its header shows the "collapse"
      // affordance. This is now purely cosmetic — the element stays wired across the
      // move, so an async mode-forward by a wrapper element can't break it.
      el.setAttribute('popout-mode', 'collapse');
      // Hand the ONE real element to the page; the provider moves it into its layout.
      await this.dockProvider.onDock(el);
    });

    const unsubPopin = agenticActivityBus.subscribe('chat-popin', async () => {
      if (!this.isDocked) return;
      this.isDocked = false;
      _aiAssistantDocked = false;
      // Re-home the assistant into the bubble BEFORE the provider tears down its
      // layout slot — its removeItems (or equivalent) would otherwise destroy the
      // moved element.
      const bubble = this.querySelector('foundation-ai-chat-bubble');
      if (this.assistantEl && bubble) {
        this.assistantEl.setAttribute('popout-mode', 'expand');
        this.assistantEl.setAttribute('slot', 'dialog-content');
        bubble.appendChild(this.assistantEl);
      }
      this.syncBubbleVisibility();
      await this.dockProvider?.onUndock();
    });

    this.unsubBus = () => {
      unsubPopout();
      unsubPopin();
    };
    this.syncBubbleVisibility();
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.unsubBus?.();
    this.unsubBus = undefined;
    _aiPopoutManager = undefined;
  }

  /** Locate the assistant element currently slotted into the bubble. */
  private findAssistant(): HTMLElement | null {
    const bubble = this.querySelector('foundation-ai-chat-bubble');
    return (bubble?.querySelector('[slot="dialog-content"]') as HTMLElement | null) ?? null;
  }

  /**
   * Register an externally-created assistant element and its docked state. For
   * single-element hosts that create the assistant directly in a layout pane (rather
   * than statically in the bubble), so the manager tracks the one element, whether it's
   * currently docked (for pop-in re-homing), reflects the right `popout-mode` on it
   * (drives the header button), and hides the empty bubble.
   */
  adoptAssistant(el: HTMLElement, docked: boolean): void {
    this.assistantEl = el;
    this.isDocked = docked;
    _aiAssistantDocked = docked;
    el.setAttribute('popout-mode', docked ? 'collapse' : 'expand');
    this.syncBubbleVisibility();
  }

  /** Hide the floating bubble unless it currently contains the assistant. */
  private syncBubbleVisibility(): void {
    const bubble = this.querySelector('foundation-ai-chat-bubble') as HTMLElement | null;
    if (!bubble) return;
    const hasAssistant = !!bubble.querySelector('[slot="dialog-content"]');
    bubble.style.display = this.isDocked || !hasAssistant ? 'none' : '';
  }

  canDockChanged() {
    const el = (this.assistantEl ??= this.findAssistant());
    if (this.canDock) {
      el?.setAttribute('popout-mode', this.isDocked ? 'collapse' : 'expand');
    } else {
      el?.removeAttribute('popout-mode');
    }
  }

  registerDockProvider(provider: AiDockProvider): void {
    this.dockProvider = provider;
    this.canDock = true;
  }

  deregisterDockProvider(): void {
    this.isDocked = false;
    _aiAssistantDocked = false;
    this.dockProvider = null;
    this.canDock = false;
  }

  /**
   * If the assistant is currently docked, collapses it back into the bubble.
   * Await this in `onBeforeNavButtonClick` to ensure cleanup before navigation.
   */
  collapseIfDocked(): Promise<void> {
    if (!this.isDocked || !this.dockProvider) return Promise.resolve();
    this.dockProvider.initiateCollapse();
    return Promise.resolve();
  }
}

let _aiPopoutManager: FoundationAiPopoutManager | undefined;
/** Whether the assistant is currently docked in the layout panel (vs bubble-only). */
let _aiAssistantDocked = false;

/** True when the assistant is active in the layout panel (docked) rather than the bubble. */
export function isAiAssistantDocked(): boolean {
  return _aiAssistantDocked;
}

/**
 * Returns the active `FoundationAiPopoutManager` instance, or `undefined` if none is mounted.
 * Import this in pages to call `registerDockProvider` / `deregisterDockProvider`.
 *
 * @beta
 */
export function getAiPopoutManager(): FoundationAiPopoutManager | undefined {
  return _aiPopoutManager;
}
