/**
 * PackageRootActor — base class for package root actors.
 *
 * Like a NestJS Module: owns a DI scope, declares children (providers),
 * manages lifecycle, provides shared resources (storage, config).
 *
 * The root actor mounts at the package namespace (e.g., system.observability).
 * Children mount under it (e.g., system.observability.logging). Children
 * inherit the root's context via cascade — if root registers a store,
 * children find it automatically.
 *
 * See: ARCHITECTURE/ARCHITECTURE-PACKAGE-ACTORS.md
 */

import { MatrixActor, type ActorStatusSnapshotV1 } from './MatrixActor';

// ── Types ────────────────────────────────────────────────────────────────

export interface PackageManifestProps {
  /** Components to spawn as children. */
  components?: Array<{
    type: string;
    mount: string;
    props?: Record<string, unknown>;
  }>;
  /** Storage configuration from manifest. */
  storage?: {
    sqlite?: {
      domain: string;
      schema?: string;
    };
  };
  /** Package metadata. */
  packageName?: string;
  packageVersion?: string;
}

// ── Base Class ───────────────────────────────────────────────────────────

export class PackageRootActor extends MatrixActor {
  /** Override in subclass: npm package name. */
  static package = '';
  /** Override in subclass: bus namespace (root mount path). */
  static namespace = '';

  static override accepts = {
    '$children':  { description: 'List child actors in this package' },
    '$restart':   { description: 'Restart all children' },
    '$status':    { description: 'Package health + storage stats' },
  };

  protected _manifestProps: PackageManifestProps = {};
  private _childMounts: string[] = [];

  // ── Lifecycle ────────────────────────────────────────────────────────

  override async onBootstrap(): Promise<void> {
    // Props are passed by PackageLoader from the manifest
    this._manifestProps = ((this as unknown as { _props?: PackageManifestProps })._props) ?? {};

    // 1. Initialize package-scoped storage (if declared)
    await this._initStorage();

    // 2. Spawn children declared in manifest
    await this._spawnChildren();
  }

  override onDispose(): void {
    // Children are auto-disposed by the actor tree (ChildManager)
    this._childMounts = [];
  }

  // ── Storage ──────────────────────────────────────────────────────────

  /**
   * Create package-scoped storage and register on this context.
   * Children find it via cascade — they call context.getService(RECORD_STORE_KEY)
   * and walk up to THIS context, finding the package's store (not the global one).
   */
  private async _initStorage(): Promise<void> {
    const storage = this._manifestProps.storage?.sqlite;
    if (!storage) return;

    // Ask system.sqlite to ensure the domain's database exists
    try {
      const { RequestReply } = await import('../engine/remoting/RequestReply');
      await RequestReply.execute(
        this._context!, 'system.sqlite', 'sqlite.init-schema',
        {
          domain: storage.domain,
          schema: storage.schema || '',
          packageName: this._manifestProps.packageName,
        },
        { timeoutMs: 10000 },
      );
    } catch {
      // system.sqlite may not be available yet — storage init deferred
    }

    // Create in-process per-package store via the registered provider seam
    // (RECORD_STORE_PROVIDER_KEY) and register the result on THIS context
    // so children inherit via cascade. The runtime owns provider registration;
    // core never reaches into a sibling implementation package.
    try {
      const { RECORD_STORE_PROVIDER_KEY } = await import('./IRecordStoreProvider');
      const { RECORD_STORE_KEY } = await import('../framework/InteractionRecord');

      const provider = this._context!.getService(RECORD_STORE_PROVIDER_KEY) as
        | { provide(matrixHome: string, domain: string): Promise<unknown | null> }
        | undefined;
      if (!provider || typeof provider.provide !== 'function') {
        // No provider registered — storage deferred (matches pre-seam behavior
        // where missing sqlite was swallowed by the surrounding try/catch).
        return;
      }

      const { join } = await import('node:path');
      const matrixHome = process.env.MATRIX_HOME || join(process.cwd(), '.matrix');

      const store = await provider.provide(matrixHome, storage.domain);
      if (store == null) {
        // Provider declined (e.g., no SQLite backend on this host) — defer.
        return;
      }

      this._context!.setService(RECORD_STORE_KEY, store);
    } catch {
      // Provider lookup or invocation failed — storage deferred (same as
      // the pre-seam swallow on dynamic-import failure).
    }
  }

  // ── Child Spawning ───────────────────────────────────────────────────

  /**
   * Spawn children declared in the manifest. Each child mounts as
   * namespace.childMount (e.g., system.observability.logging).
   */
  private async _spawnChildren(): Promise<void> {
    const components = this._manifestProps.components ?? [];
    const registry = this._context?.runtime as unknown as {
      _componentRegistry?: { resolve(type: string): (new () => MatrixActor) | undefined };
    };

    for (const component of components) {
      try {
        // Resolve the component class from the registry
        const ActorClass = registry?._componentRegistry?.resolve(component.type);
        if (!ActorClass) {
          continue; // Type not registered — skip silently
        }

        // Mount as child: component.mount is relative (e.g., "logging")
        const childMount = await this.addChild(
          component.mount,
          ActorClass as new () => MatrixActor,
          { props: component.props },
        );
        this._childMounts.push(childMount);
      } catch {
        // Child failed to mount — continue with others
      }
    }
  }

  // ── Handlers ─────────────────────────────────────────────────────────

  /** $children — list package contents (like Assembly.GetTypes()). */
  on$children() {
    const ctor = this.constructor as typeof PackageRootActor;
    return {
      ok: true,
      package: ctor.package,
      namespace: ctor.namespace,
      children: this._childMounts,
      count: this._childMounts.length,
    };
  }

  /** $restart — dispose all children and re-spawn from manifest. */
  async on$restart() {
    // Dispose existing children
    if (this._children) {
      for (const mount of [...this._childMounts]) {
        try {
          const childId = mount.split('.').pop() || mount;
          this._children.removeChild(childId);
        } catch { /* best effort */ }
      }
    }
    this._childMounts = [];

    // Re-spawn
    await this._spawnChildren();

    return {
      ok: true,
      restarted: true,
      children: this._childMounts,
      count: this._childMounts.length,
    };
  }

  /** $status — package health + storage stats. */
  on$status(): ActorStatusSnapshotV1 & {
    ok: true;
    package: string;
    namespace: string;
    version: string | undefined;
    children: number;
    storage: string | null;
  } {
    const ctor = this.constructor as typeof PackageRootActor;
    const base = super.on$status();
    return {
      ...base,
      ok: true,
      kind: 'package-root',
      package: ctor.package,
      namespace: ctor.namespace,
      version: this._manifestProps.packageVersion,
      children: this._childMounts.length,
      storage: this._manifestProps.storage?.sqlite?.domain ?? null,
    };
  }
}
