/**
 * mx run — package-first standalone runner.
 *
 * Preferred mode:
 *   mx run . --env dev
 *
 * Actor-file mode:
 *   mx run ./MyActor.ts --mount my-svc --root MY-ROOT
 */

import { pathToFileURL } from 'node:url';
import * as path from 'node:path';
import * as fs from 'node:fs';
import * as net from 'node:net';
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
import type { NatsConnection, Subscription } from 'nats';
import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
import {
  buildMatrixHttpAssetMount,
  MatrixHttpAssetEndpointActor,
} from '@open-matrix/system-gateway-http';
import { HostAuthStateStore, HostCredentialStore } from '@open-matrix/system-auth';
import {
  serviceCommand,
  startServiceCommand,
  type IServiceCommandOptions,
  type IStartedServiceCommand,
} from './service.js';
import { loadPackageEnvironment, loadPackageEnvironmentFromPath } from '../utils/package-environment.js';
import {
  createRunnerRuntimeHandle,
  formatRunnerRuntimeClosed,
  type IRunnerRuntimeHandle,
} from '../utils/runner-runtime.js';
import { resolveNatsBinary } from '../utils/runner-transport.js';
import { resolveRunnerTransportPlan, type IResolvedRunnerTransportPlan } from '../utils/runner-transport.js';
import { startStandaloneWebappServer, type IStandaloneWebappServerHandle } from '../utils/standalone-webapp-server.js';
import { loadMatrixWebappManifest } from '../utils/webapp-manifest.js';
import {
  deregisterRunnerRuntime,
  heartbeatRunnerRuntime,
  registerRunnerRuntime,
  type IRunnerRuntimeRegistration,
  type IRunnerWebappRouteMetadata,
} from '../utils/runner-control-plane.js';
import {
  packageDeclaresStandaloneFactory,
  type ILoadedMatrixServiceManifest,
} from '../utils/service-manifest.js';
import {
  defaultRunnerControlMount,
  defaultRunnerRuntimeMount,
  mountRunnerRuntimeControl,
  type RunnerRuntimeLifecycleState,
} from '../utils/runner-runtime-control.js';
import type { ILoadedMatrixWebappManifest } from '../utils/webapp-manifest.js';

export interface IRunCommandOptions {
  readonly check?: boolean;
  readonly json?: boolean;
  readonly env?: string;
  readonly envFile?: string;
  readonly serve?: boolean;
  readonly entry?: string;
  readonly mount?: string;
  readonly root?: string;
  readonly export?: string;
  readonly config?: string;
  readonly secrets?: string;
  readonly overrides?: string;
  readonly broker?: boolean;
  readonly brokerPort?: number;
  readonly federation?: boolean;
  readonly backboneUrl?: string;
  readonly props?: string;
}

type ActorConstructor = {
  new (): MatrixActor;
  readonly accepts?: Record<string, unknown>;
  readonly name: string;
};

type BrokerLike = {
  publish(topic: string, payload: unknown): void;
};

type BrokerConstructor = new () => BrokerLike;
type TransportConstructor = new (broker: BrokerLike, options: { readonly name: string }) => unknown;
type RuntimeConstructor = new () => unknown;
type MatrixContextFactory = {
  create(mount: string, transport: unknown, runtime: unknown): Parameters<MatrixActor['initialize']>[0];
};
type FederationPeerConstructor = new (options: {
  readonly localRoot: string;
  readonly localBus: unknown;
  readonly instanceResolver: unknown;
}) => {
  setBackbone(transport: unknown): Promise<void>;
  start(): Promise<void>;
};
type ExactInstanceResolverConstructor = new () => unknown;
type CreateNatsTransport = (client: NatsConnection) => unknown;

interface IActorFileRunCommandOptions {
  readonly mount: string;
  readonly root?: string;
  readonly export?: string;
  readonly broker?: boolean;
  readonly brokerPort?: number;
  readonly federation?: boolean;
  readonly backboneUrl?: string;
  readonly props?: string;
}

interface IRunningNatsServer {
  readonly port: number;
  stop(): Promise<void>;
}

// P1.65 — set process.title for ps/tasklist visibility. Sourcing order:
//   MATRIX_RUNTIME_KEY env → packageRef shortname → packageDir basename
//   → MATRIX_RUNTIME_ID stripped → default 'runtime'.
function setRuntimeProcessTitle(input: {
  readonly runtimeKey?: string;
  readonly runtimeId?: string;
  readonly packageDir?: string;
}): void {
  const fromKey = readTrimmed(input.runtimeKey);
  const fromPackageRef = readPackageRefShortName(input.packageDir);
  const fromDir = readPackageDirBasename(input.packageDir);
  const fromRuntimeId = readRuntimeIdShortName(input.runtimeId);
  const name = fromKey ?? fromPackageRef ?? fromDir ?? fromRuntimeId ?? 'runtime';
  process.title = `hivecast-runtime:${normalizeRuntimeTitleName(name)}`;
}

function readTrimmed(value: string | undefined): string | undefined {
  if (typeof value !== 'string') return undefined;
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function readPackageRefShortName(packageDir: string | undefined): string | undefined {
  if (!packageDir) return undefined;
  try {
    const manifestPath = path.join(packageDir, 'package.json');
    if (!fs.existsSync(manifestPath)) return undefined;
    const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { name?: unknown };
    const name = typeof parsed.name === 'string' ? parsed.name : undefined;
    if (!name) return undefined;
    return name.split('/').pop();
  } catch {
    return undefined;
  }
}

function readPackageDirBasename(packageDir: string | undefined): string | undefined {
  if (!packageDir) return undefined;
  const base = path.basename(packageDir);
  return base.length > 0 ? base : undefined;
}

function readRuntimeIdShortName(runtimeId: string | undefined): string | undefined {
  const value = readTrimmed(runtimeId);
  if (!value) return undefined;
  return value
    .replace(/^RUNTIME-/i, '')
    .replace(/^HOST-/i, '')
    .replace(/^LOCAL-/i, '');
}

function normalizeRuntimeTitleName(value: string): string {
  return value
    .trim()
    .toLowerCase()
    .replace(/^@[^/]+\//, '')
    .replace(/@[^@/]+$/, '')
    .replace(/[^a-z0-9_-]+/g, '-')
    .replace(/-+/g, '-')
    .replace(/^-|-$/g, '') || 'runtime';
}

async function waitForPort(
  port: number,
  timeoutMs: number,
  child: ChildProcessWithoutNullStreams,
  getLogs: () => string,
  getSpawnError?: () => Error | null,
): Promise<void> {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const spawnError = getSpawnError?.();
    if (spawnError) {
      throw new Error(`Failed to spawn nats-server: ${spawnError.message}`);
    }
    if (child.exitCode != null) {
      throw new Error(`nats-server exited early (${child.exitCode}): ${getLogs()}`);
    }

    const listening = await new Promise<boolean>((resolve) => {
      const socket = net.connect({ host: '127.0.0.1', port });
      socket.once('connect', () => {
        socket.destroy();
        resolve(true);
      });
      socket.once('error', () => {
        socket.destroy();
        resolve(false);
      });
      socket.setTimeout(500, () => {
        socket.destroy();
        resolve(false);
      });
    });

    if (listening) {
      return;
    }

    await new Promise((resolve) => setTimeout(resolve, 100));
  }

  throw new Error(`Timed out waiting for nats-server on port ${port}: ${getLogs()}`);
}

async function stopChildProcess(child: ChildProcessWithoutNullStreams): Promise<void> {
  if (child.exitCode != null) {
    return;
  }

  await new Promise<void>((resolve) => {
    const timeout = setTimeout(() => {
      if (child.exitCode == null) {
        child.kill('SIGKILL');
      }
    }, 2000);

    child.once('exit', () => {
      clearTimeout(timeout);
      resolve();
    });

    child.kill('SIGTERM');
  });
}

async function startNatsServer(port: number, baseDir: string): Promise<IRunningNatsServer> {
  const binaryPath = resolveNatsBinary(baseDir);
  const child = spawn(binaryPath, ['-a', '127.0.0.1', '-p', String(port), '-js'], {
    stdio: 'pipe',
    env: process.env,
    windowsHide: true,
  });

  let spawnError: Error | null = null;
  let stderr = '';
  child.once('error', (error: Error) => {
    spawnError = error;
  });
  child.stderr.on('data', (chunk: Buffer) => {
    stderr += chunk.toString('utf8');
  });

  await waitForPort(port, 10_000, child, () => {
    if (spawnError) {
      return `${spawnError.message}${stderr ? ` | ${stderr.trim()}` : ''}`;
    }
    return stderr.trim();
  }, () => spawnError);

  return {
    port,
    async stop(): Promise<void> {
      await stopChildProcess(child);
    },
  };
}

function localTopicToNatsSubject(topic: string, root: string, mount: string): string | null {
  if (topic === `${mount}/$events`) {
    return `${root}.${mount}.$events`;
  }
  if (topic.startsWith('$replies.')) {
    return `${root}.$reply.${topic.slice('$replies.'.length)}`;
  }
  if (topic.startsWith(`${root}.$reply.`)) {
    return topic;
  }
  return null;
}

function decodeNatsPayload(
  data: Uint8Array,
  jsonCodec: { decode(payload: Uint8Array): unknown },
  stringCodec: { decode(payload: Uint8Array): string },
): unknown {
  try {
    return jsonCodec.decode(data);
  } catch {
    const text = stringCodec.decode(data);
    try {
      return JSON.parse(text);
    } catch {
      return text;
    }
  }
}

function pumpSubscription(
  subscription: Subscription,
  label: string,
  onMessage: (subject: string, payload: unknown) => void,
  jsonCodec: { decode(payload: Uint8Array): unknown },
  stringCodec: { decode(payload: Uint8Array): string },
): void {
  void (async () => {
    for await (const message of subscription) {
      const payload = decodeNatsPayload(message.data, jsonCodec, stringCodec);
      onMessage(message.subject, payload);
    }
  })().catch((error) => {
    console.error(`[mx run] ${label} subscription failed: ${error}`);
  });
}

export async function runCommand(
  targetPath: string,
  options: IRunCommandOptions,
): Promise<void> {
  const resolvedPath = path.resolve(targetPath);
  if (!fs.existsSync(resolvedPath)) {
    throw new Error(`Run target not found: ${resolvedPath}`);
  }

  const targetStats = fs.statSync(resolvedPath);
  if (targetStats.isDirectory()) {
    await runPackageCommand(resolvedPath, options);
    return;
  }

  await runActorFileCommand(resolvedPath, assertActorFileRunOptions(options));
}

async function runPackageCommand(
  packageDir: string,
  options: IRunCommandOptions,
): Promise<void> {
  // P1.65 — name the runtime process for ps/tasklist. Sourcing order:
  // MATRIX_RUNTIME_KEY env → packageRef shortname → packageDir basename
  // → MATRIX_RUNTIME_ID → 'runtime'.
  setRuntimeProcessTitle({
    runtimeKey: process.env.MATRIX_RUNTIME_KEY,
    runtimeId: process.env.MATRIX_RUNTIME_ID,
    packageDir,
  });
  const hasServiceManifest = packageDeclaresStandaloneFactory(packageDir);
  const webapp = loadMatrixWebappManifest(packageDir);

  if (!options.serve && !hasServiceManifest && !webapp) {
    throw new Error(
      `Package run target ${packageDir} does not declare a matrix.json runtime.factory. `
      + 'Use a package directory with runtime.factory metadata, or pass an actor file path with --mount.',
    );
  }

  if (options.serve || (!hasServiceManifest && webapp)) {
    await runStandaloneWebappPackage(packageDir, options, {
      hasServiceManifest,
      webapp,
      serve: options.serve === true,
    });
    return;
  }

  const result = await serviceCommand(packageDir, {
    check: options.check,
    json: options.json,
    env: options.env,
    envFile: options.envFile,
    entry: options.entry,
    export: options.export,
    root: options.root,
    mount: options.mount,
    config: options.config,
    secrets: options.secrets,
    overrides: options.overrides,
  } satisfies IServiceCommandOptions);

  if (!options.check) {
    return;
  }

  if (options.json) {
    console.log(JSON.stringify(result, null, 2));
    return;
  }

  console.log(`Validated run-in-place bootstrap for ${result.packageName}`);
  console.log(`manifest: ${result.manifestPath}`);
  console.log(`entry: ${result.entryPath}`);
  console.log(`export: ${result.exportName}`);
}

async function runStandaloneWebappPackage(
  packageDir: string,
  options: IRunCommandOptions,
  state: {
    readonly hasServiceManifest: boolean;
    readonly webapp: ReturnType<typeof loadMatrixWebappManifest>;
    readonly serve: boolean;
  },
): Promise<void> {
  if (!state.webapp) {
    throw new Error(`Package run target ${packageDir} does not declare matrix.json webapp metadata`);
  }
  const envName = options.env?.trim();
  const envFile = options.envFile?.trim();
  if (!envName) {
    throw new Error('Package webapp runtime mode requires --env <name>');
  }

  const loadedEnvironment = envFile
    ? loadPackageEnvironmentFromPath(packageDir, envFile, envName)
    : loadPackageEnvironment(packageDir, envName);
  const root = options.root?.trim() || loadedEnvironment.environment.runtime.root;
  const runnerTransport = resolveRunnerTransportPlan(packageDir, loadedEnvironment, root);

  let serviceHandle: IStartedServiceCommand | undefined;
  let runnerRuntimeHandle: IRunnerRuntimeHandle | undefined;
  let webappServer: IStandaloneWebappServerHandle | undefined;
  let runtimeRegistration: IRunnerRuntimeRegistration | undefined;
  let webappAssetMount: string | undefined;
  let heartbeatTimer: NodeJS.Timeout | undefined;
  let runtimeReclaimAttempts = 0;
  let lifecycleState: RunnerRuntimeLifecycleState = 'starting';
  let requestRuntimeShutdown: (reason: string) => void = () => undefined;
  const shutdownRequested = new Promise<string>((resolve) => {
    requestRuntimeShutdown = resolve;
  });

  try {
    if (state.hasServiceManifest) {
      serviceHandle = await startServiceCommand(packageDir, {
        env: envName,
        envFile,
        entry: options.entry,
        export: options.export,
        root: options.root,
        mount: options.mount,
        config: options.config,
        secrets: options.secrets,
        overrides: options.overrides,
      } satisfies IServiceCommandOptions);
    } else {
      runnerRuntimeHandle = await createRunnerRuntimeHandle(root, runnerTransport);
      registerHostHomeService(runnerRuntimeHandle.runtime, loadedEnvironment.environment.host?.matrixDir);
      const runtimeId = resolveWebappRuntimeId(state.webapp, loadedEnvironment);
      const runtimeMount = loadedEnvironment.environment.runtime.runtimeMount
        ?? process.env.MATRIX_RUNTIME_MOUNT
        ?? defaultRunnerRuntimeMount(runtimeId);
      const controlMount = loadedEnvironment.environment.runtime.controlMount
        ?? process.env.MATRIX_RUNTIME_CONTROL_MOUNT
        ?? defaultRunnerControlMount(runtimeId);
      await mountRunnerRuntimeControl({
        runtime: runnerRuntimeHandle.runtime,
        runtimeId,
        runtimeMount,
        controlMount,
        startedAt: new Date().toISOString(),
        getState: () => lifecycleState,
        requestShutdown: (reason) => requestRuntimeShutdown(reason),
        loadedPackage: {
          packageRef: state.webapp.packageName,
        },
      });
      if (state.serve) {
        webappServer = await startStandaloneWebappServer({
          loadedEnvironment,
          webapp: state.webapp,
          runnerTransport,
        });
      }
      webappAssetMount = await mountMatrixHttpAssetEndpoint(runnerRuntimeHandle.runtime, state.webapp, runtimeId);
      runtimeRegistration = await registerRunnerRuntime(
        runnerRuntimeHandle.runtime,
        buildWebappRuntimeManifest(state.webapp),
        loadedEnvironment,
        runnerTransport,
        undefined,
        runtimeMount,
        controlMount,
        buildRunnerWebappRouteMetadata(state.webapp, webappAssetMount, webappServer),
      );
      if (!options.check) {
        const heartbeatRuntime = runnerRuntimeHandle.runtime;
        heartbeatTimer = setInterval(() => {
          void (async () => {
            const heartbeat = await heartbeatRunnerRuntime(heartbeatRuntime, runtimeRegistration);
            if (!heartbeat || heartbeat.known) {
              runtimeReclaimAttempts = 0;
              return;
            }
            runtimeReclaimAttempts += 1;
            if (runtimeReclaimAttempts === 1 || runtimeReclaimAttempts % 10 === 0) {
              console.error(
                `[mx run] registry forgot runtime claim for ${heartbeat.runtimeId} (renewed=0); reclaiming`,
              );
            }
            const reclaimed = await runtimeRegistration?.reclaim();
            if (reclaimed) {
              console.error(
                `[mx run] runtime reclaim recovered for ${heartbeat.runtimeId} after ${runtimeReclaimAttempts} attempt(s): ${reclaimed.inventoryClaimCount} inventory claims re-issued`,
              );
              runtimeReclaimAttempts = 0;
            }
          })().catch((error: unknown) => {
            if (runtimeReclaimAttempts === 1 || runtimeReclaimAttempts % 10 === 0) {
              console.error(
                `[mx run] runtime reclaim failed: ${error instanceof Error ? error.message : String(error)}`,
              );
            }
          });
        }, 10_000);
      }
    }

    if (state.serve && serviceHandle) {
      webappServer = await startStandaloneWebappServer({
        loadedEnvironment,
        webapp: state.webapp,
        runnerTransport: serviceHandle?.result.runnerTransport ?? runnerTransport,
      });
    }
    lifecycleState = 'live';

    const buildServeResult = () => ({
      packageName: state.webapp?.packageName,
      packageDir,
      environmentName: loadedEnvironment.envName,
      environmentPath: loadedEnvironment.environmentPath,
      root,
      mode: state.serve ? 'standalone-webapp' : 'webapp-assets',
      ...(webappAssetMount ? { assetMount: webappAssetMount } : {}),
      ...(webappServer ? {
        baseUrl: webappServer.baseUrl,
        httpPort: webappServer.port,
        bootstrapPath: webappServer.bootstrapPath,
        natsWsPath: webappServer.natsWsPath,
      } : {}),
      runnerTransport: serviceHandle?.result.runnerTransport ?? runnerTransport,
      ...(serviceHandle ? { service: serviceHandle.result } : {}),
    });

    const shutdownAll = async (): Promise<void> => {
      lifecycleState = 'stopping';
      if (heartbeatTimer) {
        clearInterval(heartbeatTimer);
        heartbeatTimer = undefined;
      }
      if (webappServer) {
        const current = webappServer;
        webappServer = undefined;
        await current.shutdown();
      }
      if (serviceHandle) {
        const current = serviceHandle;
        serviceHandle = undefined;
        await current.shutdown();
      }
      if (runnerRuntimeHandle) {
        const current = runnerRuntimeHandle;
        runnerRuntimeHandle = undefined;
        await deregisterRunnerRuntime(current.runtime, runtimeRegistration);
        await current.shutdown();
      }
      lifecycleState = 'stopped';
    };

    if (options.check) {
      const result = buildServeResult();
      await shutdownAll();
      if (options.json) {
        console.log(JSON.stringify(result, null, 2));
      } else if (state.serve) {
        console.log(`Validated standalone webapp serve for ${state.webapp.packageName}`);
        console.log(`base: ${result.baseUrl}`);
      } else {
        console.log(`Validated webapp asset runtime for ${state.webapp.packageName}`);
        console.log(`asset mount: ${result.assetMount ?? '(service runtime)'}`);
      }
      return;
    }

    if (options.json) {
      console.log(JSON.stringify(buildServeResult(), null, 2));
    } else if (state.serve && webappServer) {
      console.log(`[mx run] serving ${state.webapp.packageName} at ${webappServer.baseUrl}`);
      console.log(`[mx run] bootstrap: ${webappServer.baseUrl}${webappServer.bootstrapPath}`);
      console.log(`[mx run] nats-ws: ${webappServer.baseUrl}${webappServer.natsWsPath}`);
    } else {
      console.log(`[mx run] running ${state.webapp.packageName} web assets at ${webappAssetMount ?? '(service runtime)'}`);
    }

    const stopTriggers: Promise<void>[] = [
      waitForShutdownSignal(shutdownAll),
      shutdownRequested.then(async () => {
        await shutdownAll();
      }),
    ];
    if (serviceHandle) {
      stopTriggers.push(serviceHandle.shutdownRequested.then(async () => {
        await shutdownAll();
      }));
      if (serviceHandle.transportClosed) {
        stopTriggers.push(serviceHandle.transportClosed.then((closed) => {
          throw new Error(formatRunnerRuntimeClosed(closed));
        }));
      }
    }
    if (runnerRuntimeHandle?.closed) {
      stopTriggers.push(runnerRuntimeHandle.closed.then((closed) => {
        throw new Error(formatRunnerRuntimeClosed(closed));
      }));
    }

    await Promise.race(stopTriggers);
    return;
  } catch (error) {
    lifecycleState = 'failed';
    if (webappServer) {
      await webappServer.shutdown().catch(() => undefined);
    }
    if (serviceHandle) {
      await serviceHandle.shutdown().catch(() => undefined);
    }
    if (runnerRuntimeHandle) {
      await deregisterRunnerRuntime(runnerRuntimeHandle.runtime, runtimeRegistration).catch(() => undefined);
      await runnerRuntimeHandle.shutdown().catch(() => undefined);
    }
    throw error;
  }
}

function registerHostHomeService(
  runtime: IRunnerRuntimeHandle['runtime'],
  matrixDir: string | undefined,
): void {
  const hostHome = matrixDir?.trim();
  if (hostHome) {
    runtime.registerService('matrix-host-home', hostHome);
    runtime.registerService('credential-store', new HostCredentialStore(new HostAuthStateStore(hostHome)));
  }
}

async function mountMatrixHttpAssetEndpoint(
  runtime: IRunnerRuntimeHandle['runtime'],
  webapp: ILoadedMatrixWebappManifest,
  runtimeId?: string,
): Promise<string> {
  const assetMount = buildMatrixHttpAssetMount(webapp.appName, runtimeId);
  if (!runtime.hasComponent(assetMount)) {
    await runtime.createSupervised(MatrixHttpAssetEndpointActor as unknown as new () => MatrixActor, assetMount, {
      assetEndpoint: {
        packageName: webapp.packageName,
        appName: webapp.appName,
        distDir: webapp.distDir,
        entryFile: webapp.entryFile,
      },
    });
  }
  return assetMount;
}

function buildRunnerWebappRouteMetadata(
  webapp: ILoadedMatrixWebappManifest,
  assetMount: string,
  webappServer?: IStandaloneWebappServerHandle,
): IRunnerWebappRouteMetadata {
  return {
    appName: webapp.appName,
    routePrefix: `/apps/${webapp.appName}/`,
    ...(webapp.displayName ? { displayName: webapp.displayName } : {}),
    ...(webapp.icon ? { icon: webapp.icon } : {}),
    ...(typeof webapp.navOrder === 'number' ? { navOrder: webapp.navOrder } : {}),
    ...(webapp.description ? { description: webapp.description } : {}),
    ...(webapp.shells && webapp.shells.length > 0 ? { shells: webapp.shells } : {}),
    assetMount,
    ...(webappServer ? {
      origin: webappServer.baseUrl,
      port: webappServer.port,
    } : {}),
  };
}

function buildWebappRuntimeManifest(
  webapp: NonNullable<ReturnType<typeof loadMatrixWebappManifest>>,
): ILoadedMatrixServiceManifest {
  return {
    packageDir: webapp.packageDir,
    packageName: webapp.packageName,
    manifestPath: webapp.manifestPath,
    entryPath: path.join(webapp.distDir, webapp.entryFile),
    exportName: 'webapp',
    kind: 'factory',
    overrides: {},
    packageComponents: [],
    serviceInstance: {
      id: webapp.packageName,
      packageName: webapp.packageName,
      class: 'webapp',
      rootKind: 'package-root',
      autoStart: true,
      registrationSource: 'matrix-service',
    },
  };
}

function resolveWebappRuntimeId(
  webapp: NonNullable<ReturnType<typeof loadMatrixWebappManifest>>,
  loadedEnvironment: ReturnType<typeof loadPackageEnvironmentFromPath>,
): string {
  const runtimeId = loadedEnvironment.environment.runtime.runtimeId?.trim();
  if (runtimeId) {
    return runtimeId;
  }
  return webapp.packageName;
}

function actorConstructorFromExport(value: unknown): ActorConstructor | null {
  if (typeof value !== 'function') {
    return null;
  }
  return value as ActorConstructor;
}

function requireFunctionExport<T>(
  moduleExports: Record<string, unknown>,
  exportName: string,
): T {
  const value = moduleExports[exportName];
  if (typeof value !== 'function') {
    throw new Error(`Expected function export ${exportName}`);
  }
  return value as T;
}

function requireObjectExport<T extends object>(
  moduleExports: Record<string, unknown>,
  exportName: string,
): T {
  const value = moduleExports[exportName];
  if (!value || typeof value !== 'object') {
    throw new Error(`Expected object export ${exportName}`);
  }
  return value as T;
}

function assertActorFileRunOptions(options: IRunCommandOptions): IActorFileRunCommandOptions {
  const mount = options.mount?.trim();
  if (!mount) {
    throw new Error(
      'Actor-file mode requires --mount. '
      + 'For package execution use: mx run . --env <name>',
    );
  }
  return {
    mount,
    root: options.root,
    export: options.export,
    broker: options.broker,
    brokerPort: options.brokerPort,
    federation: options.federation,
    backboneUrl: options.backboneUrl,
    props: options.props,
  };
}

async function waitForShutdownSignal(onShutdown: () => Promise<void>): Promise<void> {
  await new Promise<void>((resolve, reject) => {
    let settled = false;
    const complete = (error?: unknown) => {
      if (settled) {
        return;
      }
      settled = true;
      process.off('SIGINT', handleSignal);
      process.off('SIGTERM', handleSignal);
      if (error) {
        reject(error);
        return;
      }
      resolve();
    };

    const handleSignal = () => {
      void onShutdown()
        .then(() => complete())
        .catch((error) => complete(error));
    };

    process.on('SIGINT', handleSignal);
    process.on('SIGTERM', handleSignal);
  });
}

async function runActorFileCommand(
  actorPath: string,
  options: IActorFileRunCommandOptions,
): Promise<void> {
  const root = options.root ?? 'standalone';
  const mount = options.mount;
  const exportName = options.export ?? 'default';
  const props = options.props ? JSON.parse(options.props) : {};

  // ── Step 1: Import actor class ──────────────────────────────────────
  const moduleUrl = pathToFileURL(actorPath).href;
  const mod = await import(moduleUrl) as Record<string, unknown>;

  // Try named export, then default, then first export that looks like a class
  let ActorClass = actorConstructorFromExport(mod[exportName]);
  if (!ActorClass && exportName === 'default') {
    // Try first exported class
    for (const [key, val] of Object.entries(mod)) {
      const candidate = actorConstructorFromExport(val);
      if (candidate && key !== 'default') {
        ActorClass = candidate;
        console.log(`[mx run] Using export: ${key}`);
        break;
      }
    }
  }
  if (!ActorClass) {
    const available = Object.keys(mod).filter(k => actorConstructorFromExport(mod[k]));
    throw new Error(
      `No actor class found as export '${exportName}'. Available exports: ${available.join(', ') || '(none)'}`
    );
  }

  // ── Step 2: Set up transport ────────────────────────────────────────
  //
  // Three modes:
  //   1. InMemory only (default) — local actors talk, no external access
  //   2. +broker — starts embedded NATS NATS on a port, CLI can connect
  //   3. +federation — connects to NATS hub backbone, world can reach us

  // Dynamic imports — import individual modules to avoid barrel hangs
  const cwd = process.cwd();

  async function importModule(relativePath: string): Promise<Record<string, unknown>> {
    const full = path.resolve(cwd, relativePath);
    return await import(pathToFileURL(full).href) as Record<string, unknown>;
  }

  const [brokerMod, transportMod, contextMod, runtimeMod] = await Promise.all([
    importModule('src/transport/InMemoryBroker.ts'),
    importModule('src/transport/InMemoryTransport.ts'),
    importModule('src/engine/core/MatrixContext.ts'),
    importModule('src/core/MockRuntime.ts'),
  ]);

  const InMemoryBroker = requireFunctionExport<BrokerConstructor>(brokerMod, 'InMemoryBroker');
  const InMemoryTransport = requireFunctionExport<TransportConstructor>(transportMod, 'InMemoryTransport');
  const MatrixContext = requireObjectExport<MatrixContextFactory>(contextMod, 'MatrixContext');
  const MockRuntime = requireFunctionExport<RuntimeConstructor>(runtimeMod, 'MockRuntime');

  const broker = new InMemoryBroker();
  const transport = new InMemoryTransport(broker, { name: 'mx-run' });
  const runtime = new MockRuntime();

  // ── Step 3: Optional embedded NATS NATS server ──────────────────────────────
  let natsServer: IRunningNatsServer | null = null;
  let natsClient: NatsConnection | null = null;

  if (options.broker || options.federation) {
    const port = options.brokerPort ?? 4222;
    try {
      const { connect, JSONCodec, StringCodec } = await import('nats');
      const jsonCodec = JSONCodec();
      const stringCodec = StringCodec();

      natsServer = await startNatsServer(port, cwd);

      console.log(`[mx run] NATS server listening on nats://127.0.0.1:${port}`);

      // Bridge InMemoryBroker ↔ local NATS so actors are reachable via NATS
      natsClient = await connect({
        servers: `nats://127.0.0.1:${port}`,
        name: `mx-run-${process.pid}`,
        timeout: 5000,
      });

      const actorInbox = `${root}.${mount}.$inbox`;
      pumpSubscription(
        natsClient.subscribe(actorInbox),
        actorInbox,
        (_subject, payload) => {
          broker.publish(`${mount}/$inbox`, payload);
        },
        jsonCodec,
        stringCodec,
      );

      const rootInbox = `${root}.$inbox`;
      pumpSubscription(
        natsClient.subscribe(rootInbox),
        rootInbox,
        (_subject, payload) => {
          broker.publish('$inbox', payload);
        },
        jsonCodec,
        stringCodec,
      );

      // Hook actor replies — the actor publishes to the replyTo topic
      const origPublish = broker.publish.bind(broker);
      broker.publish = (topic: string, payload: unknown) => {
        origPublish(topic, payload);
        const subject = localTopicToNatsSubject(topic, root, mount);
        if (subject) {
          natsClient?.publish(subject, jsonCodec.encode(payload));
        }
      };
    } catch (err) {
      throw new Error(`Failed to start NATS server for mx run: ${String(err)}`);
    }
  }

  // ── Step 4: Optional Federation ─────────────────────────────────────
  if (options.federation) {
    const backboneUrl = options.backboneUrl ?? 'nats://hub.example.com:4222';
    try {
      const fedPath = path.resolve(cwd, 'packages', 'federation', 'dist', 'src', 'index.js');
      const fedMod = await import(pathToFileURL(fedPath).href) as Record<string, unknown>;

      const FederationPeer = requireFunctionExport<FederationPeerConstructor>(fedMod, 'FederationPeer');
      const ExactInstanceResolver = requireFunctionExport<ExactInstanceResolverConstructor>(fedMod, 'ExactInstanceResolver');
      const createNatsTransport = requireFunctionExport<CreateNatsTransport>(fedMod, 'createNatsTransport');
      const { connect } = await import('nats');

      // Connect to backbone
      const backboneClient = await connect({
        servers: backboneUrl,
        name: `mx-run-fed-${root}-${process.pid}`,
        timeout: 10000,
      });

      console.log(`[mx run] Federation backbone connected: ${backboneUrl}`);

      const backboneTransport = createNatsTransport(backboneClient);

      if (!natsServer) {
        throw new Error('Federation mode requires the embedded NATS server to be running');
      }
      const localNats = await connect({
        servers: `nats://127.0.0.1:${options.brokerPort ?? 4222}`,
        name: `mx-run-fed-local-${process.pid}`,
        timeout: 3000,
      });
      const localBus = createNatsTransport(localNats);

      const peer = new FederationPeer({
        localRoot: root,
        localBus,
        instanceResolver: new ExactInstanceResolver(),
      });

      await peer.setBackbone(backboneTransport);
      await peer.start();

      console.log(`[mx run] FederationPeer active for root: ${root}`);
      console.log(`[mx run] Actor reachable at: ${root}/${mount}`);
    } catch (err) {
      throw new Error(`Federation setup failed for mx run: ${String(err)}`);
    }
  }

  // ── Step 5: Boot the actor ──────────────────────────────────────────
  const context = MatrixContext.create(mount, transport, runtime);
  const actor = new ActorClass();
  await actor.initialize(context, ActorClass.name || 'Actor');

  // Assign actor properties requested by the CLI.
  for (const [key, value] of Object.entries(props)) {
    Object.defineProperty(actor, key, {
      value,
      writable: true,
      enumerable: true,
      configurable: true,
    });
  }

  const accepts = ActorClass.accepts
    ? Object.keys(ActorClass.accepts).join(', ')
    : 'N/A';

  console.log('');
  console.log('╔════════════════════════════════════════════════╗');
  console.log('║              mx run — Actor Live               ║');
  console.log('╠════════════════════════════════════════════════╣');
  console.log(`║  Actor:  ${ActorClass.name}`.padEnd(49) + '║');
  console.log(`║  Mount:  ${mount}`.padEnd(49) + '║');
  console.log(`║  Root:   ${root}`.padEnd(49) + '║');
  console.log(`║  Accepts: ${accepts}`.padEnd(49) + '║');
  if (options.broker || options.federation) {
    console.log(`║  Broker: nats://127.0.0.1:${options.brokerPort ?? 4222}`.padEnd(49) + '║');
  }
  if (options.federation) {
    console.log(`║  Federation: CONNECTED`.padEnd(49) + '║');
    console.log(`║  URI: ${root}/${mount}`.padEnd(49) + '║');
  }
  console.log('╚════════════════════════════════════════════════╝');
  console.log('');

  if (options.broker) {
    console.log(`Talk to this actor:`);
    console.log(`  matrix send ${mount} $introspect -r ${root}`);
    console.log('');
  }

  if (options.federation) {
    console.log(`From FlowPad or a federated client:`);
    console.log(`  flow().fromValue({}).viaRemote("${root}/${mount}", "introspect")`);
    console.log('');
  }

  console.log('Press Ctrl+C to stop.');

  // Keep process alive
  await new Promise<void>(() => {
    const shutdown = async () => {
      console.log('\n[mx run] Shutting down...');
      try {
        if (natsClient && !natsClient.isClosed()) {
          await natsClient.drain();
        }
      } catch {
        // Best-effort shutdown only.
      }
      try {
        await natsServer?.stop();
      } catch {
        // Best-effort shutdown only.
      }
      process.exit(0);
    };

    process.on('SIGINT', () => {
      void shutdown();
    });
  });
}
