// src/core/children/ChildManager.ts

import type { IMatrixContext } from '../../engine/core/IMatrixContext';
import type { IComponentDescriptor } from '../../engine/core/IComponentDescriptor';
import type { Position } from '../composite/Position';
import {
  COMPONENT_RUNTIME_METADATA,
  type IComponentRuntimeMetadata,
} from '../composite/IComponentRuntimeMetadata';

/**
 * Component class constructor interface.
 */
export interface IComponentClass {
  new(): unknown;
  readonly name: string;
}

/**
 * Descriptor for recreating a child (Blueprint).
 * Used by Supervisor to restart crashed actors.
 */
export interface IChildBlueprint {
  componentClass: IComponentClass;
  props?: Record<string, unknown>;
}

/**
 * Child entry - ADDRESS ONLY, no component reference.
 */
export interface IChildEntry {
  /** Unique identifier within this parent */
  readonly id: string;
  /** Mount path (address) of the child */
  readonly mount: string;
  /** Slot the child belongs to */
  readonly slot: string;
  /** Component class name for introspection */
  readonly componentClass: string;
  /** When the child was added */
  readonly createdAt: number;
  /** Ops this child accepts (captured at registration) */
  readonly accepts: string[];
  /** Events this child emits (captured at registration) */
  readonly emits: string[];
  /** Sibling tracks this child listens to (captured at registration) */
  readonly tracks: string[];
  /** Runtime-detectable provenance for this child mount. */
  readonly metadata?: IComponentRuntimeMetadata;
}

/**
 * Options for adding a child.
 */
export interface IAddChildOptions {
  /** Slot to add child to (default: 'default') */
  slot?: string;
  /** Initial properties to pass to child */
  props?: Record<string, unknown>;
  /** Position within the slot */
  position?: Position;
}

/**
 * Internal entry with cleanup tracking and supervision metadata.
 */
interface IInternalChildEntry extends IChildEntry {
  unsubs: Set<() => void>;
  /** Blueprint for restarting */
  descriptor?: IChildBlueprint;
}

/**
 * ChildManager - Manages child actors by address.
 *
 * Provides address-based tracking and metadata storage for supervision.
 * DOES NOT hold references to child component instances.
 */
export class ChildManager {
  private readonly children = new Map<string, IInternalChildEntry>();
  private readonly order = new Map<string, string[]>();
  private readonly context: IMatrixContext;
  private inventoryVersion = 0;

  constructor(context: IMatrixContext) {
    this.context = context;
  }

  /**
   * Add a child actor.
   */
  async addChild(
    id: string,
    componentClass: IComponentClass,
    options: IAddChildOptions = {}
  ): Promise<string> {
    const { slot = 'default', props, position = 'append' } = options;

    if (this.children.has(id)) {
      // eslint-disable-next-line no-console -- duplicate child creation warning in kernel child manager
      console.warn(`[ChildManager] Child '${id}' already exists at ${this.context.mount}`);
      return this.children.get(id)!.mount;
    }

    // Ask runtime to create the child
    const mount = await this.context.runtime.createChild(
      this.context,
      id,
      { componentClass, props } as IComponentDescriptor
    );

    // Register with metadata for supervision
    this.registerChild(id, mount, slot, componentClass.name, position, componentClass, props);

    return mount;
  }

  /**
   * Register a child that was created externally.
   */
  registerChild(
    id: string,
    mount: string,
    slot: string = 'default',
    componentClassName: string = 'unknown',
    position: Position = 'append',
    classCtor?: IComponentClass,
    initialProps?: Record<string, unknown>
  ): void {
    if (this.children.has(id)) {
      // eslint-disable-next-line no-console -- duplicate child registration warning in kernel child manager
      console.warn(`[ChildManager] Child '${id}' already registered at ${this.context.mount}`);
      return;
    }

    // Capture accepts/emits/tracks from the class constructor for introspection
    const ctorAny = classCtor as unknown as Record<string, unknown> | undefined;
    const childAccepts = ctorAny?.accepts ? Object.keys(ctorAny.accepts as Record<string, unknown>) : [];
    const childEmits = ctorAny?.emits ? Object.keys(ctorAny.emits as Record<string, unknown>) : [];
    const childTracks = Array.isArray(ctorAny?.tracks) ? (ctorAny.tracks as string[]) : [];

    const entry: IInternalChildEntry = {
      id,
      mount,
      slot,
      componentClass: componentClassName,
      createdAt: Date.now(),
      accepts: childAccepts,
      emits: childEmits,
      tracks: childTracks,
      metadata: this.context.getService<IComponentRuntimeMetadata>(COMPONENT_RUNTIME_METADATA),
      unsubs: new Set(),
      // Store the blueprint if provided (preferred) or constructed from legacy args
      descriptor: (classCtor) ? { componentClass: classCtor, props: initialProps } : undefined
    };

    this.children.set(id, entry);
    this.inventoryVersion += 1;

    if (!this.order.has(slot)) {
      this.order.set(slot, []);
    }
    this._insertIdAtPosition(slot, id, position);
  }

  private _insertIdAtPosition(slot: string, id: string, position: Position): void {
    const order = this.order.get(slot);
    if (!order) return;

    if (position === 'append') {
      order.push(id);
    } else if (position === 'prepend') {
      order.unshift(id);
    } else if (typeof position === 'object' && 'before' in position) {
      const idx = order.indexOf(position.before);
      if (idx >= 0) order.splice(idx, 0, id);
      else order.push(id);
    } else if (typeof position === 'object' && 'after' in position) {
      const idx = order.indexOf(position.after);
      if (idx >= 0) order.splice(idx + 1, 0, id);
      else order.push(id);
    } else if (typeof position === 'object' && 'index' in position) {
      if (position.index >= order.length) order.push(id);
      else if (position.index <= 0) order.unshift(id);
      else order.splice(position.index, 0, id);
    }
  }

  async removeChild(id: string): Promise<void> {
    const entry = this.children.get(id);
    if (!entry) return;

    for (const unsub of entry.unsubs) {
      try { unsub(); } catch { }
    }

    // Runtime disposal
    await this.context.runtime.remove(entry.mount);

    this.unregisterChild(id);
  }

  unregisterChild(id: string): void {
    const entry = this.children.get(id);
    if (!entry) return;

    for (const unsub of entry.unsubs) {
      try { unsub(); } catch { }
    }

    this.children.delete(id);
    this.inventoryVersion += 1;

    const slotOrder = this.order.get(entry.slot);
    if (slotOrder) {
      const idx = slotOrder.indexOf(id);
      if (idx >= 0) slotOrder.splice(idx, 1);
    }
  }

  getChildSlot(id: string): string | undefined {
    return this.children.get(id)?.slot;
  }

  updateChildMount(id: string, newMount: string): void {
    const entry = this.children.get(id);
    if (!entry) return;
    // Don't lose the metadata on update
    // We update the existing entry object or create a copy (safer)
    const newEntry = { ...entry, mount: newMount };
    this.children.set(id, newEntry);
    this.inventoryVersion += 1;
  }

  moveChild(id: string, position: Position): void {
    const entry = this.children.get(id);
    if (!entry) return;
    const slotOrder = this.order.get(entry.slot);
    if (!slotOrder) return;
    const currentIdx = slotOrder.indexOf(id);
    if (currentIdx >= 0) slotOrder.splice(currentIdx, 1);
    this._insertIdAtPosition(entry.slot, id, position);
  }

  getChildMount(id: string): string | undefined {
    return this.children.get(id)?.mount;
  }

  getChild(id: string): IChildEntry | undefined {
    const entry = this.children.get(id);
    if (!entry) return undefined;

    // Strict separation: Public IChildEntry does NOT include descriptor
    const { unsubs, descriptor, ...rest } = entry;
    return rest;
  }

  /**
   * Get internal child entry including supervision metadata.
   * Used by SupervisionBehavior to restart children.
   */
  getInternalChild(id: string): IInternalChildEntry | undefined {
    return this.children.get(id);
  }

  hasChild(id: string): boolean {
    return this.children.has(id);
  }

  /**
   * Get component class name for a child.
   * Used for introspection/serialization.
   */
  getChildClass(id: string): string | undefined {
    return this.children.get(id)?.componentClass;
  }

  getChildIds(slot?: string): string[] {
    if (slot) return [...(this.order.get(slot) ?? [])];
    const result: string[] = [];
    for (const ids of this.order.values()) result.push(...ids);
    return result;
  }

  getAllChildren(): IChildEntry[] {
    return Array.from(this.children.values()).map(({ unsubs, descriptor, ...rest }) => rest);
  }

  getChildrenInSlot(slot: string): IChildEntry[] {
    const ids = this.order.get(slot) ?? [];
    return ids
      .map(id => this.getChild(id))
      .filter((c): c is IChildEntry => c !== undefined);
  }

  get childCount(): number {
    return this.children.size;
  }

  get version(): number {
    return this.inventoryVersion;
  }

  get slots(): string[] {
    return Array.from(this.order.keys());
  }

  addChildUnsub(id: string, unsub: () => void): void {
    this.children.get(id)?.unsubs.add(unsub);
  }

  sendToChild(id: string, command: string, payload?: unknown): void {
    const mount = this.getChildMount(id);
    if (!mount) {
      // eslint-disable-next-line no-console -- missing child send target should remain visible in kernel logs
      console.warn(`[ChildManager] Child '${id}' not found at ${this.context.mount}`);
      return;
    }
    this.context.transport.publish(`${mount}.accepts.${command}`, payload);
  }

  onChildEvent(id: string, event: string, handler: (payload: unknown) => void): () => void {
    const mount = this.getChildMount(id);
    if (!mount) {
      // eslint-disable-next-line no-console -- missing child event target should remain visible in kernel logs
      console.warn(`[ChildManager] Child '${id}' not found at ${this.context.mount}`);
      return () => { };
    }
    const unsub = this.context.transport.subscribe(
      `${mount}.emits.${event}`,
      handler
    );
    this.addChildUnsub(id, unsub);
    return unsub;
  }

  broadcastToChildren(command: string, payload?: unknown, slot?: string): void {
    const ids = this.getChildIds(slot);
    for (const id of ids) {
      this.sendToChild(id, command, payload);
    }
  }

  async disposeAll(): Promise<void> {
    const ids = [...this.children.keys()];
    for (const id of ids) {
      await this.removeChild(id);
    }
  }
}
