import * as plugins from '../plugins.js';
import { type INetworkProxyOptions, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js';
import { CertificateManager } from './classes.np.certificatemanager.js';
import { ConnectionPool } from './classes.np.connectionpool.js';
import { RequestHandler, type IMetricsTracker } from './classes.np.requesthandler.js';
import { WebSocketHandler } from './classes.np.websockethandler.js';
import { ProxyRouter } from '../classes.router.js';
import { Port80Handler } from '../port80handler/classes.port80handler.js';

/**
 * NetworkProxy provides a reverse proxy with TLS termination, WebSocket support,
 * automatic certificate management, and high-performance connection pooling.
 */
export class NetworkProxy implements IMetricsTracker {
  // Configuration
  public options: INetworkProxyOptions;
  public proxyConfigs: IReverseProxyConfig[] = [];
  
  // Server instances
  public httpsServer: plugins.https.Server;
  
  // Core components
  private certificateManager: CertificateManager;
  private connectionPool: ConnectionPool;
  private requestHandler: RequestHandler;
  private webSocketHandler: WebSocketHandler;
  private router = new ProxyRouter();
  
  // State tracking
  public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
  public activeContexts: Set<string> = new Set();
  public connectedClients: number = 0;
  public startTime: number = 0;
  public requestsServed: number = 0;
  public failedRequests: number = 0;
  
  // Tracking for PortProxy integration
  private portProxyConnections: number = 0;
  private tlsTerminatedConnections: number = 0;
  
  // Timers
  private metricsInterval: NodeJS.Timeout;
  private connectionPoolCleanupInterval: NodeJS.Timeout;
  
  // Logger
  private logger: ILogger;

  /**
   * Creates a new NetworkProxy instance
   */
  constructor(optionsArg: INetworkProxyOptions) {
    // Set default options
    this.options = {
      port: optionsArg.port,
      maxConnections: optionsArg.maxConnections || 10000,
      keepAliveTimeout: optionsArg.keepAliveTimeout || 120000, // 2 minutes 
      headersTimeout: optionsArg.headersTimeout || 60000, // 1 minute
      logLevel: optionsArg.logLevel || 'info',
      cors: optionsArg.cors || {
        allowOrigin: '*',
        allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
        allowHeaders: 'Content-Type, Authorization',
        maxAge: 86400
      },
      // Defaults for PortProxy integration
      connectionPoolSize: optionsArg.connectionPoolSize || 50,
      portProxyIntegration: optionsArg.portProxyIntegration || false,
      useExternalPort80Handler: optionsArg.useExternalPort80Handler || false,
      // Default ACME options
      acme: {
        enabled: optionsArg.acme?.enabled || false,
        port: optionsArg.acme?.port || 80,
        contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
        useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
        renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
        autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
        certificateStore: optionsArg.acme?.certificateStore || './certs',
        skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
      }
    };
    
    // Initialize logger
    this.logger = createLogger(this.options.logLevel);
    
    // Initialize components
    this.certificateManager = new CertificateManager(this.options);
    this.connectionPool = new ConnectionPool(this.options);
    this.requestHandler = new RequestHandler(this.options, this.connectionPool, this.router);
    this.webSocketHandler = new WebSocketHandler(this.options, this.connectionPool, this.router);
    
    // Connect request handler to this metrics tracker
    this.requestHandler.setMetricsTracker(this);
  }

  /**
   * Implements IMetricsTracker interface to increment request counters
   */
  public incrementRequestsServed(): void {
    this.requestsServed++;
  }
  
  /**
   * Implements IMetricsTracker interface to increment failed request counters
   */
  public incrementFailedRequests(): void {
    this.failedRequests++;
  }

  /**
   * Returns the port number this NetworkProxy is listening on
   * Useful for PortProxy to determine where to forward connections
   */
  public getListeningPort(): number {
    return this.options.port;
  }

  /**
   * Updates the server capacity settings
   * @param maxConnections Maximum number of simultaneous connections
   * @param keepAliveTimeout Keep-alive timeout in milliseconds
   * @param connectionPoolSize Size of the connection pool per backend
   */
  public updateCapacity(maxConnections?: number, keepAliveTimeout?: number, connectionPoolSize?: number): void {
    if (maxConnections !== undefined) {
      this.options.maxConnections = maxConnections;
      this.logger.info(`Updated max connections to ${maxConnections}`);
    }
    
    if (keepAliveTimeout !== undefined) {
      this.options.keepAliveTimeout = keepAliveTimeout;
      
      if (this.httpsServer) {
        this.httpsServer.keepAliveTimeout = keepAliveTimeout;
        this.logger.info(`Updated keep-alive timeout to ${keepAliveTimeout}ms`);
      }
    }
    
    if (connectionPoolSize !== undefined) {
      this.options.connectionPoolSize = connectionPoolSize;
      this.logger.info(`Updated connection pool size to ${connectionPoolSize}`);
      
      // Clean up excess connections in the pool
      this.connectionPool.cleanupConnectionPool();
    }
  }

  /**
   * Returns current server metrics
   * Useful for PortProxy to determine which NetworkProxy to use for load balancing
   */
  public getMetrics(): any {
    return {
      activeConnections: this.connectedClients,
      totalRequests: this.requestsServed,
      failedRequests: this.failedRequests,
      portProxyConnections: this.portProxyConnections,
      tlsTerminatedConnections: this.tlsTerminatedConnections,
      connectionPoolSize: this.connectionPool.getPoolStatus(),
      uptime: Math.floor((Date.now() - this.startTime) / 1000),
      memoryUsage: process.memoryUsage(),
      activeWebSockets: this.webSocketHandler.getConnectionInfo().activeConnections
    };
  }

  /**
   * Sets an external Port80Handler for certificate management
   * This allows the NetworkProxy to use a centrally managed Port80Handler
   * instead of creating its own
   * 
   * @param handler The Port80Handler instance to use
   */
  public setExternalPort80Handler(handler: Port80Handler): void {
    // Connect it to the certificate manager
    this.certificateManager.setExternalPort80Handler(handler);
  }

  /**
   * Starts the proxy server
   */
  public async start(): Promise<void> {
    this.startTime = Date.now();
    
    // Initialize Port80Handler if enabled and not using external handler
    if (this.options.acme?.enabled && !this.options.useExternalPort80Handler) {
      await this.certificateManager.initializePort80Handler();
    }
    
    // Create the HTTPS server
    this.httpsServer = plugins.https.createServer(
      {
        key: this.certificateManager.getDefaultCertificates().key,
        cert: this.certificateManager.getDefaultCertificates().cert,
        SNICallback: (domain, cb) => this.certificateManager.handleSNI(domain, cb)
      },
      (req, res) => this.requestHandler.handleRequest(req, res)
    );

    // Configure server timeouts
    this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
    this.httpsServer.headersTimeout = this.options.headersTimeout;
    
    // Setup connection tracking
    this.setupConnectionTracking();
    
    // Share HTTPS server with certificate manager
    this.certificateManager.setHttpsServer(this.httpsServer);
    
    // Setup WebSocket support
    this.webSocketHandler.initialize(this.httpsServer);
    
    // Start metrics collection
    this.setupMetricsCollection();
    
    // Setup connection pool cleanup interval
    this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup();

    // Start the server
    return new Promise((resolve) => {
      this.httpsServer.listen(this.options.port, () => {
        this.logger.info(`NetworkProxy started on port ${this.options.port}`);
        resolve();
      });
    });
  }

  /**
   * Sets up tracking of TCP connections
   */
  private setupConnectionTracking(): void {
    this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
      // Check if max connections reached
      if (this.socketMap.getArray().length >= this.options.maxConnections) {
        this.logger.warn(`Max connections (${this.options.maxConnections}) reached, rejecting new connection`);
        connection.destroy();
        return;
      }

      // Add connection to tracking
      this.socketMap.add(connection);
      this.connectedClients = this.socketMap.getArray().length;
      
      // Check for connection from PortProxy by inspecting the source port
      const localPort = connection.localPort || 0;
      const remotePort = connection.remotePort || 0;
      
      // If this connection is from a PortProxy (usually indicated by it coming from localhost)
      if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
        this.portProxyConnections++;
        this.logger.debug(`New connection from PortProxy (local: ${localPort}, remote: ${remotePort})`);
      } else {
        this.logger.debug(`New direct connection (local: ${localPort}, remote: ${remotePort})`);
      }
      
      // Setup connection cleanup handlers
      const cleanupConnection = () => {
        if (this.socketMap.checkForObject(connection)) {
          this.socketMap.remove(connection);
          this.connectedClients = this.socketMap.getArray().length;
          
          // If this was a PortProxy connection, decrement the counter
          if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
            this.portProxyConnections--;
          }
          
          this.logger.debug(`Connection closed. ${this.connectedClients} connections remaining`);
        }
      };
      
      connection.on('close', cleanupConnection);
      connection.on('error', (err) => {
        this.logger.debug('Connection error', err);
        cleanupConnection();
      });
      connection.on('end', cleanupConnection);
    });
    
    // Track TLS handshake completions
    this.httpsServer.on('secureConnection', (tlsSocket) => {
      this.tlsTerminatedConnections++;
      this.logger.debug('TLS handshake completed, connection secured');
    });
  }

  /**
   * Sets up metrics collection 
   */
  private setupMetricsCollection(): void {
    this.metricsInterval = setInterval(() => {
      const uptime = Math.floor((Date.now() - this.startTime) / 1000);
      const metrics = {
        uptime,
        activeConnections: this.connectedClients,
        totalRequests: this.requestsServed,
        failedRequests: this.failedRequests,
        portProxyConnections: this.portProxyConnections,
        tlsTerminatedConnections: this.tlsTerminatedConnections,
        activeWebSockets: this.webSocketHandler.getConnectionInfo().activeConnections,
        memoryUsage: process.memoryUsage(),
        activeContexts: Array.from(this.activeContexts),
        connectionPool: this.connectionPool.getPoolStatus()
      };
      
      this.logger.debug('Proxy metrics', metrics);
    }, 60000); // Log metrics every minute
    
    // Don't keep process alive just for metrics
    if (this.metricsInterval.unref) {
      this.metricsInterval.unref();
    }
  }

  /**
   * Updates proxy configurations
   */
  public async updateProxyConfigs(
    proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]
  ): Promise<void> {
    this.logger.info(`Updating proxy configurations (${proxyConfigsArg.length} configs)`);
    
    // Update internal configs
    this.proxyConfigs = proxyConfigsArg;
    this.router.setNewProxyConfigs(proxyConfigsArg);
    
    // Collect all hostnames for cleanup later
    const currentHostNames = new Set<string>();
    
    // Add/update SSL contexts for each host
    for (const config of proxyConfigsArg) {
      currentHostNames.add(config.hostName);
      
      try {
        // Update certificate in cache
        this.certificateManager.updateCertificateCache(
          config.hostName,
          config.publicKey,
          config.privateKey
        );
        
        this.activeContexts.add(config.hostName);
      } catch (error) {
        this.logger.error(`Failed to add SSL context for ${config.hostName}`, error);
      }
    }
    
    // Clean up removed contexts
    for (const hostname of this.activeContexts) {
      if (!currentHostNames.has(hostname)) {
        this.logger.info(`Hostname ${hostname} removed from configuration`);
        this.activeContexts.delete(hostname);
      }
    }
    
    // Register domains with Port80Handler if available
    const domainsForACME = Array.from(currentHostNames)
      .filter(domain => !domain.includes('*')); // Skip wildcard domains
    
    this.certificateManager.registerDomainsWithPort80Handler(domainsForACME);
  }

  /**
   * Converts PortProxy domain configurations to NetworkProxy configs
   * @param domainConfigs PortProxy domain configs
   * @param sslKeyPair Default SSL key pair to use if not specified
   * @returns Array of NetworkProxy configs
   */
  public convertPortProxyConfigs(
    domainConfigs: Array<{
      domains: string[];
      targetIPs?: string[];
      allowedIPs?: string[];
    }>,
    sslKeyPair?: { key: string; cert: string }
  ): plugins.tsclass.network.IReverseProxyConfig[] {
    const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
    
    // Use default certificates if not provided
    const defaultCerts = this.certificateManager.getDefaultCertificates();
    const sslKey = sslKeyPair?.key || defaultCerts.key;
    const sslCert = sslKeyPair?.cert || defaultCerts.cert;
    
    for (const domainConfig of domainConfigs) {
      // Each domain in the domains array gets its own config
      for (const domain of domainConfig.domains) {
        // Skip non-hostname patterns (like IP addresses)
        if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
          continue;
        }
        
        proxyConfigs.push({
          hostName: domain,
          destinationIps: domainConfig.targetIPs || ['localhost'],
          destinationPorts: [this.options.port], // Use the NetworkProxy port
          privateKey: sslKey,
          publicKey: sslCert
        });
      }
    }
    
    this.logger.info(`Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
    return proxyConfigs;
  }

  /**
   * Adds default headers to be included in all responses
   */
  public async addDefaultHeaders(headersArg: { [key: string]: string }): Promise<void> {
    this.logger.info('Adding default headers', headersArg);
    this.requestHandler.setDefaultHeaders(headersArg);
  }

  /**
   * Stops the proxy server
   */
  public async stop(): Promise<void> {
    this.logger.info('Stopping NetworkProxy server');
    
    // Clear intervals
    if (this.metricsInterval) {
      clearInterval(this.metricsInterval);
    }
    
    if (this.connectionPoolCleanupInterval) {
      clearInterval(this.connectionPoolCleanupInterval);
    }
    
    // Stop WebSocket handler
    this.webSocketHandler.shutdown();
    
    // Close all tracked sockets
    for (const socket of this.socketMap.getArray()) {
      try {
        socket.destroy();
      } catch (error) {
        this.logger.error('Error destroying socket', error);
      }
    }
    
    // Close all connection pool connections
    this.connectionPool.closeAllConnections();
    
    // Stop Port80Handler if internally managed
    await this.certificateManager.stopPort80Handler();
    
    // Close the HTTPS server
    return new Promise((resolve) => {
      this.httpsServer.close(() => {
        this.logger.info('NetworkProxy server stopped successfully');
        resolve();
      });
    });
  }

  /**
   * Requests a new certificate for a domain
   * This can be used to manually trigger certificate issuance
   * @param domain The domain to request a certificate for
   * @returns A promise that resolves when the request is submitted (not when the certificate is issued)
   */
  public async requestCertificate(domain: string): Promise<boolean> {
    return this.certificateManager.requestCertificate(domain);
  }

  /**
   * Gets all proxy configurations currently in use
   */
  public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] {
    return [...this.proxyConfigs];
  }
}