// src/framework/shadowlisp/primitives/SlBind.ts
//
// Vision 49: Bind element - Reactive binding between Lisp and DOM
//
// Rich Hickey (Clojure creator):
//   "State is the root of all evil. But when you need it,
//    make it explicit and observable."
//
// <sl-bind var="count" to="#counter" property="textContent" />
//
// This creates a reactive binding - when the variable changes,
// the DOM property updates automatically.

import type { SExpr } from '../../../lisp/SExpr';
import { SlMatrixElement } from '../SlMatrixElement';
import { SlBootstrap, type SlEnv } from '../SlBootstrap';

type WritableTarget = Record<string, unknown>;

/**
 * SlBind - Creates reactive bindings between Lisp variables and DOM.
 *
 * When the Lisp variable changes, the DOM element updates.
 *
 * @example
 * <sl-bind var="count" to="#display" />
 * <!-- When 'count' changes, #display.textContent updates -->
 *
 * @example
 * <sl-bind var="color" to="#box" property="style.backgroundColor" />
 * <!-- When 'color' changes, the background color updates -->
 */
export class SlBind extends SlMatrixElement {
  private _targetEl: HTMLElement | null = null;
  private _currentValue: SExpr = null;

  static get observedAttributes(): string[] {
    return [...super.observedAttributes, 'var', 'to', 'property'];
  }

  /**
   * Get the variable name to watch.
   */
  get varName(): string {
    return this.getAttribute('var') ?? '';
  }

  /**
   * Get the target selector.
   */
  get targetSelector(): string {
    return this.getAttribute('to') ?? '';
  }

  /**
   * Get the property to update (default: textContent).
   */
  get targetProperty(): string {
    return this.getAttribute('property') ?? 'textContent';
  }

  connectedCallback(): void {
    super.connectedCallback();
    this._findTarget();
  }

  private _findTarget(): void {
    if (this.targetSelector) {
      this._targetEl = document.querySelector(this.targetSelector);
      if (!this._targetEl) {
        this.warnKernel(`[sl-bind] Target not found: ${this.targetSelector}`);
      }
    }
  }

  /**
   * Evaluate - looks up the variable and updates the target.
   */
  async evalSelf(env: SExpr[]): Promise<SExpr> {
    if (!this.varName) {
      throw new Error('sl-bind requires a var attribute');
    }

    // Look up the variable (cast to SExpr - closures aren't expected here)
    const value = SlBootstrap.lookup(this.varName, env as SlEnv) as SExpr;
    this._currentValue = value;

    // Update the target
    this._update(value);

    return value;
  }

  /**
   * Update the target element with a new value.
   */
  private _update(value: SExpr): void {
    if (!this._targetEl) {
      this._findTarget();
    }

    if (!this._targetEl) {
      return;
    }

    const formatted = this._format(value);
    const prop = this.targetProperty;

    // Handle nested properties like style.backgroundColor
    if (prop.includes('.')) {
      const parts = prop.split('.');
      let obj: WritableTarget = this._targetEl as unknown as WritableTarget;
      for (let i = 0; i < parts.length - 1; i++) {
        const next = obj[parts[i]!];
        if (!next || typeof next !== 'object') {
          return;
        }
        obj = next as WritableTarget;
      }
      obj[parts[parts.length - 1]!] = formatted;
    } else {
      (this._targetEl as unknown as WritableTarget)[prop] = formatted;
    }

    // Dispatch event
    this.dispatchEvent(new CustomEvent('sl-bound', {
      bubbles: true,
      detail: { var: this.varName, value, target: this.targetSelector }
    }));
  }

  /**
   * Format a value for display.
   */
  private _format(value: SExpr): string {
    if (value === null || value === undefined) return '';
    if (typeof value === 'object') {
      if (Array.isArray(value)) return value.join(' ');
      return JSON.stringify(value);
    }
    return String(value);
  }

  /**
   * Force refresh the binding.
   */
  refresh(env: SExpr[]): void {
    this.evalSelf(env);
  }

  toSExpr(): SExpr {
    return ['bind', this.varName, this.targetSelector, this.targetProperty];
  }
}

if (!customElements.get('sl-bind')) {
  customElements.define('sl-bind', SlBind);
}
