import { __decorate } from "tslib";
import { attr, customElement, GenesisElement, html, observable } from '@genesislcap/web-core';
import { agenticActivityBus } from '../../channel/ai-activity-bus';
import { styles } from './chat-bubble.styles';
import { AiChatBubbleTemplate } from './chat-bubble.template';
/** Default dimensions of the dialog panel */
const DIALOG_DEFAULT_WIDTH = 400;
const DIALOG_DEFAULT_HEIGHT = 520;
/** Initial position: bubble starts this far from the right/bottom viewport edges */
const BUBBLE_INITIAL_RIGHT_OFFSET = 90;
/** Initial position: dialog starts this far from the right/bottom viewport edges */
const DIALOG_INITIAL_RIGHT_OFFSET = 440;
const DIALOG_INITIAL_BOTTOM_OFFSET = 570;
/** Minimum distance the dialog is allowed from any viewport edge */
const VIEWPORT_EDGE_MARGIN = 20;
/** Minimum pointer movement (px) to classify an interaction as a drag rather than a click */
const DRAG_THRESHOLD = 4;
/** Keeps the bubble fully inside the viewport when dragged (accounts for bubble size + border) */
const BUBBLE_DRAG_MARGIN = 64;
/** Horizontal offset between bubble center and dialog left edge when toggling open/close */
const DIALOG_BUBBLE_HORIZONTAL_OFFSET = 340;
/** How far above the bubble the dialog opens */
const DIALOG_OPEN_TOP_OFFSET = 470;
/** How far below the dialog top the bubble snaps when closing via the X button */
const BUBBLE_SNAP_BOTTOM_OFFSET = 458;
/** Fallback tooltip width used before the element has been measured in the DOM */
const TOOLTIP_FALLBACK_WIDTH = 155;
/**
 * Floating chat bubble component that provides a draggable orb and dialog panel
 * for embedding an AI assistant without a full-page layout.
 *
 * @remarks
 * This component is optional — apps can use `foundation-ai-assistant` directly
 * in a layout panel instead, depending on their AI interaction flow.
 *
 * @beta
 */
let AiChatBubble = class AiChatBubble extends GenesisElement {
    constructor() {
        super(...arguments);
        this.title = 'AI Assistant';
        this.designSystemPrefix = 'rapid';
        /** @internal */
        this.dialogContent = [];
        /** @internal */
        this.tooltipMeasuredWidth = 0;
        /** Current left position of the bubble orb (px from viewport left). @internal */
        this.bubbleLeft = 0;
        /** Current top position of the bubble orb (px from viewport top). @internal */
        this.bubbleTop = 0;
        /** Current left position of the dialog panel (px from viewport left). @internal */
        this.dialogLeft = 0;
        /** Current top position of the dialog panel (px from viewport top). @internal */
        this.dialogTop = 0;
        /** Current rendered width of the dialog panel (px). @internal */
        this.dialogWidth = DIALOG_DEFAULT_WIDTH;
        /** Current rendered height of the dialog panel (px). @internal */
        this.dialogHeight = DIALOG_DEFAULT_HEIGHT;
        /** Whether the dialog panel is currently open. @internal */
        this.dialogOpen = false;
        /** Whether the bubble orb is currently hovered. @internal */
        this.hovered = false;
        this.dragging = false;
        this.dragTarget = null;
        this.startMouseX = 0;
        this.startMouseY = 0;
        this.startElemLeft = 0;
        this.startElemTop = 0;
        this.hasDragged = false;
        this.latestMouseX = 0;
        this.latestMouseY = 0;
        this.onCloseBtnClick = () => this.onCloseDialog();
        // Capture the latest pointer coords and schedule one rAF per display frame.
        // Pointer events can fire far faster than the display refreshes (up to 1000Hz on
        // gaming mice), so we only store the coords here and let applyDragFrame do the
        // actual work. If a frame is already scheduled we just overwrite the coords —
        // the pending frame will pick up the freshest values when it runs.
        this.onDocMouseMove = (e) => {
            if (!this.dragging)
                return;
            this.latestMouseX = e.clientX;
            this.latestMouseY = e.clientY;
            if (this.rafHandle === undefined) {
                this.rafHandle = requestAnimationFrame(this.applyDragFrame);
            }
        };
        // Runs once per display frame during a drag. Writes the new position directly to
        // the element's style.translate rather than updating FAST observables, which keeps
        // the compositor thread doing the work instead of triggering FAST's reactivity
        // pipeline on every frame. This means the element's visual position is temporarily
        // ahead of the observables — they are only synced back on mouseup (see onDocMouseUp).
        // Note: style.translate is an independent CSS property and does not conflict with
        // the transform used for the bubble's hover scale or the dialog's open animation.
        this.applyDragFrame = () => {
            this.rafHandle = undefined;
            const dx = this.latestMouseX - this.startMouseX;
            const dy = this.latestMouseY - this.startMouseY;
            if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)
                this.hasDragged = true;
            if (this.dragTarget === 'bubble') {
                const left = Math.max(0, Math.min(window.innerWidth - BUBBLE_DRAG_MARGIN, this.startElemLeft + dx));
                const top = Math.max(0, Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.startElemTop + dy));
                if (this.bubbleEl)
                    this.bubbleEl.style.translate = `${left}px ${top}px`;
            }
            else if (this.dragTarget === 'dialog') {
                const left = Math.max(0, Math.min(window.innerWidth - DIALOG_DEFAULT_WIDTH, this.startElemLeft + dx));
                const top = Math.max(0, Math.min(window.innerHeight - 100, this.startElemTop + dy));
                if (this.dialogEl)
                    this.dialogEl.style.translate = `${left}px ${top}px`;
            }
        };
        // Commits the final drag position back to the FAST observables, bringing them back
        // in sync with what the user sees. This triggers a single FAST re-render which writes
        // the same value already on the element — no visible jump. Any pending rAF is
        // cancelled first; its work is redundant since we recompute from latestMouseX/Y here.
        this.onDocMouseUp = () => {
            if (this.rafHandle !== undefined) {
                cancelAnimationFrame(this.rafHandle);
                this.rafHandle = undefined;
            }
            if (this.dragging) {
                const dx = this.latestMouseX - this.startMouseX;
                const dy = this.latestMouseY - this.startMouseY;
                if (this.dragTarget === 'bubble') {
                    this.bubbleLeft = Math.max(0, Math.min(window.innerWidth - BUBBLE_DRAG_MARGIN, this.startElemLeft + dx));
                    this.bubbleTop = Math.max(0, Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.startElemTop + dy));
                    if (!this.hasDragged) {
                        if (this.dialogOpen) {
                            this.startCloseDialog();
                        }
                        else {
                            this.dialogLeft = Math.max(VIEWPORT_EDGE_MARGIN, this.bubbleLeft - DIALOG_BUBBLE_HORIZONTAL_OFFSET);
                            this.dialogTop = Math.max(VIEWPORT_EDGE_MARGIN, this.bubbleTop - DIALOG_OPEN_TOP_OFFSET);
                            this.dialogOpen = true;
                        }
                    }
                }
                else if (this.dragTarget === 'dialog') {
                    this.dialogLeft = Math.max(0, Math.min(window.innerWidth - DIALOG_DEFAULT_WIDTH, this.startElemLeft + dx));
                    this.dialogTop = Math.max(0, Math.min(window.innerHeight - 100, this.startElemTop + dy));
                }
            }
            this.dragging = false;
            this.dragTarget = null;
        };
    }
    connectedCallback() {
        super.connectedCallback();
        this.bubbleLeft = window.innerWidth - BUBBLE_INITIAL_RIGHT_OFFSET;
        this.bubbleTop = window.innerHeight - 100;
        this.dialogLeft = Math.max(VIEWPORT_EDGE_MARGIN, window.innerWidth - DIALOG_INITIAL_RIGHT_OFFSET);
        this.dialogTop = Math.max(VIEWPORT_EDGE_MARGIN, window.innerHeight - DIALOG_INITIAL_BOTTOM_OFFSET);
        document.addEventListener('mousemove', this.onDocMouseMove);
        document.addEventListener('mouseup', this.onDocMouseUp);
        const unsubPopout = agenticActivityBus.subscribe('chat-popout', () => {
            this.startCloseDialog();
            this.hidden = true;
        });
        const unsubPopin = agenticActivityBus.subscribe('chat-popin', () => {
            this.cancelClose();
            this.hidden = false;
            this.dialogOpen = true;
        });
        this.unsubBus = () => {
            unsubPopout();
            unsubPopin();
        };
        // Measure tooltip width once after first render. The bubble is visible on connect
        // (dialogOpen starts false), so the element is in the DOM by the time rAF fires.
        // This value persists through open/close cycles — no re-measurement needed unless
        // the title attribute changes.
        requestAnimationFrame(() => {
            if (this.tooltipEl)
                this.tooltipMeasuredWidth = this.tooltipEl.offsetWidth;
        });
    }
    onBubbleMouseEnter() {
        this.hovered = true;
        // Ensure we have a real measured width even on first-ever hover.
        // The ref may not be available yet during connectedCallback's first rAF.
        if (!this.tooltipMeasuredWidth) {
            requestAnimationFrame(() => {
                if (this.tooltipEl)
                    this.tooltipMeasuredWidth = this.tooltipEl.offsetWidth;
            });
        }
    }
    onBubbleMouseLeave() {
        this.hovered = false;
    }
    disconnectedCallback() {
        var _a, _b, _c, _d, _e;
        super.disconnectedCallback();
        document.removeEventListener('mousemove', this.onDocMouseMove);
        document.removeEventListener('mouseup', this.onDocMouseUp);
        (_a = this.unsubBus) === null || _a === void 0 ? void 0 : _a.call(this);
        this.unsubBus = undefined;
        (_b = this.injectedCloseBtn) === null || _b === void 0 ? void 0 : _b.removeEventListener('click', this.onCloseBtnClick);
        (_c = this.injectedCloseBtn) === null || _c === void 0 ? void 0 : _c.remove();
        this.injectedCloseBtn = undefined;
        (_d = this.resizeObserver) === null || _d === void 0 ? void 0 : _d.disconnect();
        this.resizeObserver = undefined;
        if (this.rafHandle !== undefined) {
            cancelAnimationFrame(this.rafHandle);
            this.rafHandle = undefined;
        }
        (_e = this.closeAbort) === null || _e === void 0 ? void 0 : _e.abort();
        this.closeAbort = undefined;
    }
    dialogOpenChanged(_, next) {
        var _a, _b;
        if (next) {
            // Ensure we never observe a stale element from a prior open.
            (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
            this.resizeObserver = undefined;
            requestAnimationFrame(() => {
                const el = this.dialogEl;
                if (!el || !el.isConnected)
                    return;
                // Immediately sync actual size on open. This prevents cases where a stale
                // ResizeObserver entry (or a not-yet-laid-out element) would temporarily
                // drive the observables to 0 and shrink the dialog.
                const w = el.offsetWidth;
                const h = el.offsetHeight;
                if (w > 0)
                    this.dialogWidth = w;
                if (h > 0)
                    this.dialogHeight = h;
                this.resizeObserver = new ResizeObserver((entries) => {
                    var _a, _b, _c;
                    const entry = entries[0];
                    if (!entry)
                        return;
                    const borderBox = (_a = entry.borderBoxSize) === null || _a === void 0 ? void 0 : _a[0];
                    const nextW = Math.round((_b = borderBox === null || borderBox === void 0 ? void 0 : borderBox.inlineSize) !== null && _b !== void 0 ? _b : entry.contentRect.width);
                    const nextH = Math.round((_c = borderBox === null || borderBox === void 0 ? void 0 : borderBox.blockSize) !== null && _c !== void 0 ? _c : entry.contentRect.height);
                    if (nextW > 0)
                        this.dialogWidth = nextW;
                    if (nextH > 0)
                        this.dialogHeight = nextH;
                });
                this.resizeObserver.observe(el);
            });
        }
        else {
            (_b = this.resizeObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
            this.resizeObserver = undefined;
        }
    }
    dialogContentChanged(_, next) {
        var _a;
        if (!(next === null || next === void 0 ? void 0 : next.length) || this.injectedCloseBtn)
            return;
        for (const el of next) {
            const assistant = el.tagName.toLowerCase() === 'foundation-ai-assistant'
                ? el
                : (_a = el.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('foundation-ai-assistant');
            if (assistant) {
                this.injectCloseButton(assistant);
                break;
            }
        }
    }
    injectCloseButton(assistant) {
        const prefix = this.designSystemPrefix;
        const btn = document.createElement(`${prefix}-button`);
        btn.setAttribute('slot', 'header-actions-extra');
        btn.setAttribute('appearance', 'stealth');
        btn.setAttribute('title', 'Close');
        const icon = document.createElement(`${prefix}-icon`);
        icon.setAttribute('name', 'times');
        btn.appendChild(icon);
        btn.addEventListener('click', this.onCloseBtnClick);
        assistant.appendChild(btn);
        this.injectedCloseBtn = btn;
    }
    onBubbleMouseDown(e) {
        e.preventDefault();
        this.dragging = true;
        this.dragTarget = 'bubble';
        this.startMouseX = e.clientX;
        this.startMouseY = e.clientY;
        this.latestMouseX = e.clientX;
        this.latestMouseY = e.clientY;
        this.startElemLeft = this.bubbleLeft;
        this.startElemTop = this.bubbleTop;
        this.hasDragged = false;
    }
    onDialogHeaderMouseDown(e) {
        // Sync actual rendered size before dragging — the browser's CSS resize handle
        // updates the element's size but our observables may not have caught up yet.
        if (this.dialogEl) {
            this.dialogWidth = this.dialogEl.offsetWidth;
            this.dialogHeight = this.dialogEl.offsetHeight;
        }
        this.dragging = true;
        this.dragTarget = 'dialog';
        this.startMouseX = e.detail.clientX;
        this.startMouseY = e.detail.clientY;
        this.latestMouseX = e.detail.clientX;
        this.latestMouseY = e.detail.clientY;
        this.startElemLeft = this.dialogLeft;
        this.startElemTop = this.dialogTop;
        this.hasDragged = false;
    }
    // Cancels an in-flight close animation and restores the dialog to its open state.
    // Needed for the chat-popin case: startCloseDialog() may have been called (adding
    // .closing and an animationend listener) but the host was hidden before the animation
    // finished. When the host reappears via chat-popin, we cancel the pending close so
    // the dialog stays open rather than disappearing once animationend eventually fires.
    cancelClose() {
        var _a, _b;
        (_a = this.closeAbort) === null || _a === void 0 ? void 0 : _a.abort();
        this.closeAbort = undefined;
        (_b = this.dialogEl) === null || _b === void 0 ? void 0 : _b.classList.remove('closing');
    }
    // Plays the closing animation before removing the dialog from the DOM.
    // dialogOpen must stay true while the animation runs — setting it to false
    // immediately would cause the repeat directive to remove the element before
    // the animation has a chance to play. We only set dialogOpen = false inside
    // the animationend callback, at which point FAST removes the element cleanly.
    // If the user prefers reduced motion we skip the animation and close immediately.
    startCloseDialog() {
        const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        if (!this.dialogEl || reducedMotion) {
            this.dialogOpen = false;
            return;
        }
        this.closeAbort = new AbortController();
        this.dialogEl.classList.add('closing');
        this.dialogEl.addEventListener('animationend', () => {
            this.dialogOpen = false;
        }, {
            once: true,
            signal: this.closeAbort.signal,
        });
    }
    onPopout() {
        this.startCloseDialog();
    }
    onCloseDialog() {
        // Snap the bubble orb to the bottom-right of wherever the dialog currently is,
        // so it reappears near where the user last positioned the window.
        this.bubbleLeft = Math.max(0, Math.min(window.innerWidth - BUBBLE_DRAG_MARGIN, this.dialogLeft + DIALOG_BUBBLE_HORIZONTAL_OFFSET));
        this.bubbleTop = Math.max(0, Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.dialogTop + BUBBLE_SNAP_BOTTOM_OFFSET));
        this.startCloseDialog();
    }
    get tooltipStyle() {
        const tooltipWidth = this.tooltipMeasuredWidth || TOOLTIP_FALLBACK_WIDTH;
        const bubbleWidth = 62; // Fixed in CSS
        const margin = 8;
        const naturalLeft = (bubbleWidth - tooltipWidth) / 2;
        const vpLeft = this.bubbleLeft + naturalLeft;
        const vpRight = vpLeft + tooltipWidth;
        let shift = 0;
        if (vpLeft < margin)
            shift = margin - vpLeft;
        else if (vpRight > window.innerWidth - margin)
            shift = window.innerWidth - margin - vpRight;
        const tooltipLeft = naturalLeft + shift;
        const arrowLeft = bubbleWidth / 2 - tooltipLeft;
        return `left: ${tooltipLeft}px; --arrow-left: ${arrowLeft}px`;
    }
};
__decorate([
    attr({ attribute: 'title' })
], AiChatBubble.prototype, "title", void 0);
__decorate([
    attr({ attribute: 'image-src' })
], AiChatBubble.prototype, "imageSrc", void 0);
__decorate([
    attr({ attribute: 'design-system-prefix' })
], AiChatBubble.prototype, "designSystemPrefix", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "dialogContent", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "tooltipMeasuredWidth", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "bubbleLeft", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "bubbleTop", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "dialogLeft", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "dialogTop", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "dialogWidth", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "dialogHeight", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "dialogOpen", void 0);
__decorate([
    observable
], AiChatBubble.prototype, "hovered", void 0);
AiChatBubble = __decorate([
    customElement({
        name: 'foundation-ai-chat-bubble',
        template: html `
    ${() => AiChatBubbleTemplate()}
  `,
        styles,
    })
], AiChatBubble);
export { AiChatBubble };
