import { connect } from '@nats-io/transport-node';
import log4js from 'log4js';
import dotenv from 'dotenv';

dotenv.config();

const NATS_HOST = process.env.NATS_HOST || 'localhost';
const NATS_PORT = process.env.NATS_PORT || 4222;
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';

const logger = log4js.getLogger('Nats');
logger.level = LOG_LEVEL;

class NatsServer {
  constructor() {
    this.client = null;
    this.subscriptions = new Map(); // subject → { subscription, callbacks }
    this.requestHandlers = new Map(); // subject → { handler, callbacks }
    this.queuedSubscriptions = new Map(); // subject → { subscription, callbacks, queue }
    this.servers = [`nats://${NATS_HOST}:${NATS_PORT}`];
    this.textEncoder = new TextEncoder();
    this.textDecoder = new TextDecoder();

    logger.trace(`NatsServer: servers set to ${JSON.stringify(this.servers)}`);
  }

  async connect() {
    if (!this.client) {
      logger.info('Connecting to NATS...');
      logger.trace(`Creating NATS client with servers ${JSON.stringify(this.servers)}`);

      try {
        this.client = await connect({
          servers: this.servers,
          reconnect: true,
          maxReconnectAttempts: -1, // unlimited reconnects
          reconnectTimeWait: 1000,
          timeout: 20000,
        });

        // Test connection by checking if client is connected
        try {
          if (this.client.isClosed()) {
            throw new Error('NATS client connection failed');
          }
          logger.trace('NATS client connected successfully.');
        } catch (error) {
          throw error;
        }
      } catch (error) {
        logger.error('Failed to connect to NATS:', error);
        throw error;
      }
    } else {
      logger.trace('NATS client already exists, skipping connection.');
    }
    return this.client;
  }

  async getClient() {
    if (!this.client) {
      logger.trace('No client found, calling connect().');
      await this.connect();
    }
    return this.client;
  }

  async publish(subject, data) {
    const client = await this.getClient();
    const payload = typeof data === 'string' ? data : JSON.stringify(data);

    try {
      client.publish(subject, this.textEncoder.encode(payload));
      logger.trace(`Published to subject: ${subject}, data: ${payload}`);
      return { success: true };
    } catch (error) {
      logger.error(`Failed to publish to subject ${subject}:`, error);
      throw error;
    }
  }

  async request(subject, data, timeout = 30000) {
    const client = await this.getClient();
    const payload = typeof data === 'string' ? data : JSON.stringify(data);

    try {
      const response = await client.request(subject, this.textEncoder.encode(payload), {
        timeout: timeout,
      });

      const responseData = this.textDecoder.decode(response.data);
      logger.trace(`Request to subject: ${subject}, response: ${responseData}`);

      // Try to parse as JSON, fallback to string
      try {
        return JSON.parse(responseData);
      } catch {
        return responseData;
      }
    } catch (error) {
      if (error.code === 'TIMEOUT') {
        logger.trace(`Request timeout for subject: ${subject}`);
        return null;
      }
      throw error;
    }
  }

  async subscribe(subject, owner, callback) {
    const client = await this.getClient();
    const subscriptionKey = subject;

    if (this.subscriptions.has(subscriptionKey)) {
      this.subscriptions.get(subscriptionKey).callbacks.set(owner, callback);
      logger.trace(`Added subscription callback for owner=${owner} on subject=${subject}`);
      return { success: true };
    }

    logger.trace(`Creating new subscription for subject: ${subject}`);
    const subscription = client.subscribe(subject);
    const callbacks = new Map();
    callbacks.set(owner, callback);

    (async () => {
      for await (const msg of subscription) {
        logger.trace(`Message received on subject: ${subject}`);
        const data = this.textDecoder.decode(msg.data);
        let parsedData;

        try {
          parsedData = JSON.parse(data);
        } catch {
          parsedData = data;
        }

        for (const [ownerId, cb] of callbacks) {
          try {
            cb(subject, parsedData, msg);
          } catch (err) {
            logger.error(
              `Error in subscription callback for owner=${ownerId}, subject=${subject}:`,
              err
            );
          }
        }
      }
    })().catch((err) => {
      logger.error(`Subscription error for subject ${subject}:`, err);
    });

    this.subscriptions.set(subscriptionKey, { subscription, callbacks });
    return { success: true };
  }

  async setRequestHandler(subject, owner, handler) {
    const client = await this.getClient();
    const handlerKey = subject;

    if (this.requestHandlers.has(handlerKey)) {
      this.requestHandlers.get(handlerKey).callbacks.set(owner, handler);
      logger.trace(`Added request handler for owner=${owner} on subject=${subject}`);
      return { success: true };
    }

    logger.trace(`Creating new request handler for subject: ${subject}`);
    const subscription = client.subscribe(subject);
    const callbacks = new Map();
    callbacks.set(owner, handler);

    (async () => {
      for await (const msg of subscription) {
        logger.trace(`Request received on subject: ${subject}`);
        const data = this.textDecoder.decode(msg.data);
        let parsedData;

        try {
          parsedData = JSON.parse(data);
        } catch {
          parsedData = data;
        }

        for (const [ownerId, cb] of callbacks) {
          try {
            const response = await cb(subject, parsedData, msg);
            const responsePayload =
              typeof response === 'string' ? response : JSON.stringify(response);
            msg.respond(this.textEncoder.encode(responsePayload));
          } catch (err) {
            logger.error(`Error in request handler for owner=${ownerId}, subject=${subject}:`, err);
            // Send error response
            msg.respond(this.textEncoder.encode(JSON.stringify({ error: err.message })));
          }
        }
      }
    })().catch((err) => {
      logger.error(`Request handler error for subject ${subject}:`, err);
    });

    this.requestHandlers.set(handlerKey, { subscription, callbacks });
    return { success: true };
  }

  async removeSubscription(subject, owner) {
    const entry = this.subscriptions.get(subject);

    if (!entry) {
      logger.trace(`Subscription not found for subject: ${subject}`);
      return false;
    }

    if (entry.callbacks.delete(owner)) {
      logger.trace(`Removed subscription callback for owner: ${owner} on subject: ${subject}`);
    } else {
      logger.trace(`No subscription callback found for owner: ${owner} on subject: ${subject}`);
    }

    if (entry.callbacks.size === 0) {
      logger.trace(`No callbacks left, stopping subscription for ${subject}`);
      entry.subscription.unsubscribe();
      this.subscriptions.delete(subject);
    }

    return true;
  }

  async removeQueuedSubscription(subject, queue, owner) {
    const subscriptionKey = `${subject}:${queue}`;
    const entry = this.queuedSubscriptions.get(subscriptionKey);

    if (!entry) {
      logger.trace(`Queued subscription not found for subject: ${subject}, queue: ${queue}`);
      return false;
    }

    if (entry.callbacks.delete(owner)) {
      logger.trace(
        `Removed queued subscription callback for owner: ${owner} on subject: ${subject}, queue: ${queue}`
      );
    } else {
      logger.trace(
        `No queued subscription callback found for owner: ${owner} on subject: ${subject}, queue: ${queue}`
      );
    }

    if (entry.callbacks.size === 0) {
      logger.trace(
        `No callbacks left, stopping queued subscription for ${subject}, queue: ${queue}`
      );
      entry.subscription.unsubscribe();
      this.queuedSubscriptions.delete(subscriptionKey);
    }

    return true;
  }

  async removeRequestHandler(subject, owner) {
    const entry = this.requestHandlers.get(subject);

    if (!entry) {
      logger.trace(`Request handler not found for subject: ${subject}`);
      return false;
    }

    if (entry.callbacks.delete(owner)) {
      logger.trace(`Removed request handler for owner: ${owner} on subject: ${subject}`);
    } else {
      logger.trace(`No request handler found for owner: ${owner} on subject: ${subject}`);
    }

    if (entry.callbacks.size === 0) {
      logger.trace(`No handlers left, stopping request handler for ${subject}`);
      entry.subscription.unsubscribe();
      this.requestHandlers.delete(subject);
    }

    return true;
  }

  async disconnect() {
    logger.info('Disconnecting from NATS...');

    // Stop all subscriptions
    for (const [subject, entry] of this.subscriptions) {
      logger.trace(`Stopping subscription: ${subject}`);
      entry.subscription.unsubscribe();
    }
    this.subscriptions.clear();

    // Stop all queued subscriptions
    for (const [key, entry] of this.queuedSubscriptions) {
      logger.trace(`Stopping queued subscription: ${key}`);
      entry.subscription.unsubscribe();
    }
    this.queuedSubscriptions.clear();

    // Stop all request handlers
    for (const [subject, entry] of this.requestHandlers) {
      logger.trace(`Stopping request handler: ${subject}`);
      entry.subscription.unsubscribe();
    }
    this.requestHandlers.clear();

    if (this.client) {
      await this.client.close();
      this.client = null;
      logger.info('Disconnected from NATS');
    }
  }
}

const natsServer = new NatsServer();

export { NatsServer, natsServer };
