import * as plugins from '../plugins.js';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './classes.np.types.js';
import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from '../port80handler/classes.port80handler.js';

/**
 * Manages SSL certificates for NetworkProxy including ACME integration
 */
export class CertificateManager {
  private defaultCertificates: { key: string; cert: string };
  private certificateCache: Map<string, ICertificateEntry> = new Map();
  private port80Handler: Port80Handler | null = null;
  private externalPort80Handler: boolean = false;
  private certificateStoreDir: string;
  private logger: ILogger;
  private httpsServer: plugins.https.Server | null = null;
  
  constructor(private options: INetworkProxyOptions) {
    this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs');
    this.logger = createLogger(options.logLevel || 'info');
    
    // Ensure certificate store directory exists
    try {
      if (!fs.existsSync(this.certificateStoreDir)) {
        fs.mkdirSync(this.certificateStoreDir, { recursive: true });
        this.logger.info(`Created certificate store directory: ${this.certificateStoreDir}`);
      }
    } catch (error) {
      this.logger.warn(`Failed to create certificate store directory: ${error}`);
    }
    
    this.loadDefaultCertificates();
  }
  
  /**
   * Loads default certificates from the filesystem
   */
  public loadDefaultCertificates(): void {
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    const certPath = path.join(__dirname, '..', '..', 'assets', 'certs');
    
    try {
      this.defaultCertificates = {
        key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
        cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
      };
      this.logger.info('Default certificates loaded successfully');
    } catch (error) {
      this.logger.error('Error loading default certificates', error);
      
      // Generate self-signed fallback certificates
      try {
        // This is a placeholder for actual certificate generation code
        // In a real implementation, you would use a library like selfsigned to generate certs
        this.defaultCertificates = {
          key: "FALLBACK_KEY_CONTENT",
          cert: "FALLBACK_CERT_CONTENT"
        };
        this.logger.warn('Using fallback self-signed certificates');
      } catch (fallbackError) {
        this.logger.error('Failed to generate fallback certificates', fallbackError);
        throw new Error('Could not load or generate SSL certificates');
      }
    }
  }
  
  /**
   * Set the HTTPS server reference for context updates
   */
  public setHttpsServer(server: plugins.https.Server): void {
    this.httpsServer = server;
  }
  
  /**
   * Get default certificates
   */
  public getDefaultCertificates(): { key: string; cert: string } {
    return { ...this.defaultCertificates };
  }
  
  /**
   * Sets an external Port80Handler for certificate management
   */
  public setExternalPort80Handler(handler: Port80Handler): void {
    if (this.port80Handler && !this.externalPort80Handler) {
      this.logger.warn('Replacing existing internal Port80Handler with external handler');
      
      // Clean up existing handler if needed
      if (this.port80Handler !== handler) {
        // Unregister event handlers to avoid memory leaks
        this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_ISSUED);
        this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_RENEWED);
        this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_FAILED);
        this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_EXPIRING);
      }
    }
    
    // Set the external handler
    this.port80Handler = handler;
    this.externalPort80Handler = true;
    
    // Register event handlers
    this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
    this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
    this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
    this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
      this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
    });
    
    this.logger.info('External Port80Handler connected to CertificateManager');
    
    // Register domains with Port80Handler if we have any certificates cached
    if (this.certificateCache.size > 0) {
      const domains = Array.from(this.certificateCache.keys())
        .filter(domain => !domain.includes('*')); // Skip wildcard domains
      
      this.registerDomainsWithPort80Handler(domains);
    }
  }
  
  /**
   * Handle newly issued or renewed certificates from Port80Handler
   */
  private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
    const { domain, certificate, privateKey, expiryDate } = data;
    
    this.logger.info(`Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
    
    // Update certificate in HTTPS server
    this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
    
    // Save the certificate to the filesystem if not using external handler
    if (!this.externalPort80Handler && this.options.acme?.certificateStore) {
      this.saveCertificateToStore(domain, certificate, privateKey);
    }
  }
  
  /**
   * Handle certificate issuance failures
   */
  private handleCertificateFailed(data: { domain: string; error: string }): void {
    this.logger.error(`Certificate issuance failed for ${data.domain}: ${data.error}`);
  }
  
  /**
   * Saves certificate and private key to the filesystem
   */
  private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
    try {
      const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
      const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
      
      fs.writeFileSync(certPath, certificate);
      fs.writeFileSync(keyPath, privateKey);
      
      // Ensure private key has restricted permissions
      try {
        fs.chmodSync(keyPath, 0o600);
      } catch (error) {
        this.logger.warn(`Failed to set permissions on private key for ${domain}: ${error}`);
      }
      
      this.logger.info(`Saved certificate for ${domain} to ${certPath}`);
    } catch (error) {
      this.logger.error(`Failed to save certificate for ${domain}: ${error}`);
    }
  }
  
  /**
   * Handles SNI (Server Name Indication) for TLS connections
   * Used by the HTTPS server to select the correct certificate for each domain
   */
  public handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
    this.logger.debug(`SNI request for domain: ${domain}`);
    
    // Check if we have a certificate for this domain
    const certs = this.certificateCache.get(domain);
    
    if (certs) {
      try {
        // Create TLS context with the cached certificate
        const context = plugins.tls.createSecureContext({
          key: certs.key,
          cert: certs.cert
        });
        
        this.logger.debug(`Using cached certificate for ${domain}`);
        cb(null, context);
        return;
      } catch (err) {
        this.logger.error(`Error creating secure context for ${domain}:`, err);
      }
    }
    
    // Check if we should trigger certificate issuance
    if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
      // Check if this domain is already registered
      const certData = this.port80Handler.getCertificate(domain);
      
      if (!certData) {
        this.logger.info(`No certificate found for ${domain}, registering for issuance`);
        
        // Register with new domain options format
        const domainOptions: IDomainOptions = {
          domainName: domain,
          sslRedirect: true,
          acmeMaintenance: true
        };
        
        this.port80Handler.addDomain(domainOptions);
      }
    }
    
    // Fall back to default certificate
    try {
      const context = plugins.tls.createSecureContext({
        key: this.defaultCertificates.key,
        cert: this.defaultCertificates.cert
      });
      
      this.logger.debug(`Using default certificate for ${domain}`);
      cb(null, context);
    } catch (err) {
      this.logger.error(`Error creating default secure context:`, err);
      cb(new Error('Cannot create secure context'), null);
    }
  }
  
  /**
   * Updates certificate in cache
   */
  public updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
    // Update certificate context in HTTPS server if it's running
    if (this.httpsServer) {
      try {
        this.httpsServer.addContext(domain, {
          key: privateKey,
          cert: certificate
        });
        this.logger.debug(`Updated SSL context for domain: ${domain}`);
      } catch (error) {
        this.logger.error(`Error updating SSL context for domain ${domain}:`, error);
      }
    }
    
    // Update certificate in cache
    this.certificateCache.set(domain, {
      key: privateKey,
      cert: certificate,
      expires: expiryDate
    });
  }
  
  /**
   * Gets a certificate for a domain
   */
  public getCertificate(domain: string): ICertificateEntry | undefined {
    return this.certificateCache.get(domain);
  }
  
  /**
   * Requests a new certificate for a domain
   */
  public async requestCertificate(domain: string): Promise<boolean> {
    if (!this.options.acme?.enabled && !this.externalPort80Handler) {
      this.logger.warn('ACME certificate management is not enabled');
      return false;
    }
    
    if (!this.port80Handler) {
      this.logger.error('Port80Handler is not initialized');
      return false;
    }
    
    // Skip wildcard domains - can't get certs for these with HTTP-01 validation
    if (domain.includes('*')) {
      this.logger.error(`Cannot request certificate for wildcard domain: ${domain}`);
      return false;
    }
    
    try {
      // Use the new domain options format
      const domainOptions: IDomainOptions = {
        domainName: domain,
        sslRedirect: true,
        acmeMaintenance: true
      };
      
      this.port80Handler.addDomain(domainOptions);
      this.logger.info(`Certificate request submitted for domain: ${domain}`);
      return true;
    } catch (error) {
      this.logger.error(`Error requesting certificate for domain ${domain}:`, error);
      return false;
    }
  }
  
  /**
   * Registers domains with Port80Handler for ACME certificate management
   */
  public registerDomainsWithPort80Handler(domains: string[]): void {
    if (!this.port80Handler) {
      this.logger.warn('Port80Handler is not initialized');
      return;
    }
    
    for (const domain of domains) {
      // Skip wildcard domains - can't get certs for these with HTTP-01 validation
      if (domain.includes('*')) {
        this.logger.info(`Skipping wildcard domain for ACME: ${domain}`);
        continue;
      }
      
      // Skip domains already with certificates if configured to do so
      if (this.options.acme?.skipConfiguredCerts) {
        const cachedCert = this.certificateCache.get(domain);
        if (cachedCert) {
          this.logger.info(`Skipping domain with existing certificate: ${domain}`);
          continue;
        }
      }
      
      // Register the domain for certificate issuance with new domain options format
      const domainOptions: IDomainOptions = {
        domainName: domain,
        sslRedirect: true,
        acmeMaintenance: true
      };
      
      this.port80Handler.addDomain(domainOptions);
      this.logger.info(`Registered domain for ACME certificate issuance: ${domain}`);
    }
  }
  
  /**
   * Initialize internal Port80Handler
   */
  public async initializePort80Handler(): Promise<Port80Handler | null> {
    // Skip if using external handler
    if (this.externalPort80Handler) {
      this.logger.info('Using external Port80Handler, skipping initialization');
      return this.port80Handler;
    }
    
    if (!this.options.acme?.enabled) {
      return null;
    }
    
    // Create certificate manager
    this.port80Handler = new Port80Handler({
      port: this.options.acme.port,
      contactEmail: this.options.acme.contactEmail,
      useProduction: this.options.acme.useProduction,
      renewThresholdDays: this.options.acme.renewThresholdDays,
      httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
      renewCheckIntervalHours: 24, // Check daily for renewals
      enabled: this.options.acme.enabled,
      autoRenew: this.options.acme.autoRenew,
      certificateStore: this.options.acme.certificateStore,
      skipConfiguredCerts: this.options.acme.skipConfiguredCerts
    });
    
    // Register event handlers
    this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
    this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
    this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
    this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
      this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
    });
    
    // Start the handler
    try {
      await this.port80Handler.start();
      this.logger.info(`Port80Handler started on port ${this.options.acme.port}`);
      return this.port80Handler;
    } catch (error) {
      this.logger.error(`Failed to start Port80Handler: ${error}`);
      this.port80Handler = null;
      return null;
    }
  }
  
  /**
   * Stop the Port80Handler if it was internally created
   */
  public async stopPort80Handler(): Promise<void> {
    if (this.port80Handler && !this.externalPort80Handler) {
      try {
        await this.port80Handler.stop();
        this.logger.info('Port80Handler stopped');
      } catch (error) {
        this.logger.error('Error stopping Port80Handler', error);
      }
    }
  }
}