import Redis from 'ioredis';

import { validateConfig } from '@hexahost/config';

import { logger } from './logger';

export const NODE_COMMANDS_CHANNEL = 'hgc:node:commands';

export interface NodeCommandEnvelope {
  protocolVersion: number;
  messageId: string;
  type: string;
  timestamp: string;
  payload: Record<string, unknown>;
}

export interface NodeCommandMessage {
  nodeId: string;
  envelope: NodeCommandEnvelope;
}

export interface NodeResponseMessage {
  success: boolean;
  resultCode?: string;
  errorCode?: string;
  errorMessage?: string;
}

let publisher: Redis | null = null;

export function getRedisPublisher(): Redis {
  if (!publisher) {
    const config = validateConfig();
    publisher = new Redis(config.REDIS_URL, {
      maxRetriesPerRequest: null,
    });
  }

  return publisher;
}

export async function publishNodeCommand(message: NodeCommandMessage): Promise<void> {
  const redis = getRedisPublisher();
  await redis.publish(NODE_COMMANDS_CHANNEL, JSON.stringify(message));
  logger.info(
    { nodeId: message.nodeId, type: message.envelope.type, messageId: message.envelope.messageId },
    'Published node command',
  );
}

export function nodeResponseChannel(messageId: string): string {
  return `hgc:node:responses:${messageId}`;
}

export async function waitForNodeResponse(
  messageId: string,
  timeoutMs: number,
): Promise<NodeResponseMessage | null> {
  const config = validateConfig();
  const subscriber = new Redis(config.REDIS_URL, {
    maxRetriesPerRequest: null,
  });
  const channel = nodeResponseChannel(messageId);

  return new Promise((resolve) => {
    let settled = false;

    const finish = (value: NodeResponseMessage | null): void => {
      if (settled) {
        return;
      }
      settled = true;
      clearTimeout(timer);
      void subscriber.unsubscribe(channel).finally(() => {
        subscriber.disconnect();
      });
      resolve(value);
    };

    const timer = setTimeout(() => finish(null), timeoutMs);

    subscriber.on('message', (receivedChannel, message) => {
      if (receivedChannel !== channel) {
        return;
      }

      try {
        finish(JSON.parse(message) as NodeResponseMessage);
      } catch {
        finish({
          success: false,
          errorMessage: 'Invalid node response payload',
        });
      }
    });

    void subscriber.subscribe(channel).catch((error: Error) => {
      logger.error({ err: error, channel }, 'Failed to subscribe for node response');
      finish(null);
    });
  });
}

export async function closeRedisConnections(): Promise<void> {
  if (publisher) {
    await publisher.quit();
    publisher = null;
  }
}
