import { connect as connectNats, jwtAuthenticator, type NatsConnection } from 'nats';
import {
  MatrixActor,
  MatrixRuntime,
  NatsTransport,
  TopicRouter,
  exchangeHiveCastActorCredential,
} from '@open-matrix/sdk';

class FreshServerEchoActor extends MatrixActor {
  static accepts = {
    echo: {},
    getStatus: {},
  };

  onEcho(payload: { message?: string }) {
    return {
      ok: true,
      message: payload.message ?? null,
      actor: 'FreshServerEchoActor',
    };
  }

  onGetStatus() {
    return {
      ok: true,
      actor: 'FreshServerEchoActor',
      mode: 'server',
    };
  }
}

function env(name: string): string | undefined {
  const value = process.env[name];
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}

function requiredEnv(name: string): string {
  const value = env(name);
  if (!value) throw new Error(`${name}_REQUIRED`);
  return value;
}

function connectWithJwt(input: {
  readonly natsUrl: string;
  readonly root: string;
  readonly jwt: string;
  readonly seed: string;
}): Promise<NatsConnection> {
  const encoder = new TextEncoder();
  return connectNats({
    servers: [input.natsUrl],
    name: `matrix-sdk-fresh-server-${input.root}-${process.pid}`,
    timeout: 5000,
    authenticator: jwtAuthenticator(
      () => input.jwt,
      () => encoder.encode(input.seed),
    ),
  });
}

async function request(
  transport: NatsTransport,
  mount: string,
  op: string,
  payload: unknown,
): Promise<unknown> {
  const correlationId = `fresh-server-${Date.now()}-${Math.random().toString(16).slice(2)}`;
  const replyTo = `$replies.${correlationId}`;
  let unsubscribe = () => {};
  const response = new Promise<unknown>((resolve, reject) => {
    const timeout = setTimeout(() => {
      unsubscribe();
      reject(new Error(`REQUEST_TIMEOUT: ${mount}.${op}`));
    }, 5000);
    unsubscribe = transport.subscribe(replyTo, (value) => {
      clearTimeout(timeout);
      unsubscribe();
      resolve(value);
    });
  });

  transport.publish(TopicRouter.inbox(mount), {
    op,
    payload,
    replyTo,
    correlationId,
  });

  return response;
}

async function main(): Promise<void> {
  const check = process.argv.includes('--check');
  const cloud = requiredEnv('MATRIX_CLOUD');
  const apiKey = requiredEnv('MATRIX_API_KEY');
  const space = requiredEnv('MATRIX_SPACE');
  const mount = env('MATRIX_MOUNT') ?? 'fresh.server.echo';

  const exchanged = await exchangeHiveCastActorCredential({
    cloud,
    apiKey,
    space,
  }, {
    mount,
    actorId: 'FreshServerEchoActor',
    packageId: 'matrix-sdk-demo-fresh-server-actor',
    runtimeId: `fresh-server-${process.pid}`,
    accepts: FreshServerEchoActor.accepts,
    ttlSeconds: 300,
  });

  if (!exchanged.transport.natsUrl) {
    throw new Error('HiveCast actor credential response did not include transport.natsUrl');
  }

  const connection = await connectWithJwt({
    natsUrl: exchanged.transport.natsUrl,
    root: exchanged.root,
    jwt: exchanged.credentials.jwt,
    seed: exchanged.credentials.seed,
  });
  const transport = new NatsTransport(connection, { root: exchanged.root });
  const runtime = new MatrixRuntime({ transport, logging: false });

  try {
    await runtime.create(FreshServerEchoActor, mount);
    await connection.flush();
    const result = check
      ? await request(transport, mount, 'echo', { message: 'hello-from-fresh-server-demo' })
      : undefined;

    console.log(JSON.stringify({
      ok: true,
      demo: 'fresh-server-actor',
      cloud,
      space,
      root: exchanged.root,
      mount,
      natsUrl: exchanged.transport.natsUrl,
      credentialPipeline: exchanged.credentialPipeline,
      ...(result ? { check: result } : {}),
    }, null, 2));

    if (!check) {
      process.once('SIGINT', () => {
        void runtime.shutdown().finally(() => process.exit(0));
      });
      process.once('SIGTERM', () => {
        void runtime.shutdown().finally(() => process.exit(0));
      });
      await new Promise(() => undefined);
    }
  } finally {
    if (check) {
      await runtime.shutdown();
      if (!connection.isClosed()) await connection.close();
    }
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack : error);
  process.exit(1);
});
