import * as plugins from './plugins.js';

/**
 * Represents a domain certificate with various status information
 */
interface IDomainCertificate {
  certObtained: boolean;
  obtainingInProgress: boolean;
  certificate?: string;
  privateKey?: string;
  challengeToken?: string;
  challengeKeyAuthorization?: string;
  expiryDate?: Date;
  lastRenewalAttempt?: Date;
}

/**
 * Configuration options for the ACME Certificate Manager
 */
interface IAcmeCertManagerOptions {
  port?: number;
  contactEmail?: string;
  useProduction?: boolean;
  renewThresholdDays?: number;
  httpsRedirectPort?: number;
  renewCheckIntervalHours?: number;
}

/**
 * Certificate data that can be emitted via events or set from outside
 */
interface ICertificateData {
  domain: string;
  certificate: string;
  privateKey: string;
  expiryDate: Date;
}

/**
 * Events emitted by the ACME Certificate Manager
 */
export enum CertManagerEvents {
  CERTIFICATE_ISSUED = 'certificate-issued',
  CERTIFICATE_RENEWED = 'certificate-renewed',
  CERTIFICATE_FAILED = 'certificate-failed',
  CERTIFICATE_EXPIRING = 'certificate-expiring',
  MANAGER_STARTED = 'manager-started',
  MANAGER_STOPPED = 'manager-stopped',
}

/**
 * Improved ACME Certificate Manager with event emission and external certificate management
 */
export class AcmeCertManager extends plugins.EventEmitter {
  private domainCertificates: Map<string, IDomainCertificate>;
  private server: plugins.http.Server | null = null;
  private acmeClient: plugins.acme.Client | null = null;
  private accountKey: string | null = null;
  private renewalTimer: NodeJS.Timeout | null = null;
  private isShuttingDown: boolean = false;
  private options: Required<IAcmeCertManagerOptions>;

  /**
   * Creates a new ACME Certificate Manager
   * @param options Configuration options
   */
  constructor(options: IAcmeCertManagerOptions = {}) {
    super();
    this.domainCertificates = new Map<string, IDomainCertificate>();
    
    // Default options
    this.options = {
      port: options.port ?? 80,
      contactEmail: options.contactEmail ?? 'admin@example.com',
      useProduction: options.useProduction ?? false, // Safer default: staging
      renewThresholdDays: options.renewThresholdDays ?? 30,
      httpsRedirectPort: options.httpsRedirectPort ?? 443,
      renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
    };
  }

  /**
   * Starts the HTTP server for ACME challenges
   */
  public async start(): Promise<void> {
    if (this.server) {
      throw new Error('Server is already running');
    }
    
    if (this.isShuttingDown) {
      throw new Error('Server is shutting down');
    }

    return new Promise((resolve, reject) => {
      try {
        this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
        
        this.server.on('error', (error: NodeJS.ErrnoException) => {
          if (error.code === 'EACCES') {
            reject(new Error(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`));
          } else if (error.code === 'EADDRINUSE') {
            reject(new Error(`Port ${this.options.port} is already in use.`));
          } else {
            reject(error);
          }
        });
        
        this.server.listen(this.options.port, () => {
          console.log(`AcmeCertManager is listening on port ${this.options.port}`);
          this.startRenewalTimer();
          this.emit(CertManagerEvents.MANAGER_STARTED, this.options.port);
          resolve();
        });
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * Stops the HTTP server and renewal timer
   */
  public async stop(): Promise<void> {
    if (!this.server) {
      return;
    }
    
    this.isShuttingDown = true;
    
    // Stop the renewal timer
    if (this.renewalTimer) {
      clearInterval(this.renewalTimer);
      this.renewalTimer = null;
    }

    return new Promise<void>((resolve) => {
      if (this.server) {
        this.server.close(() => {
          this.server = null;
          this.isShuttingDown = false;
          this.emit(CertManagerEvents.MANAGER_STOPPED);
          resolve();
        });
      } else {
        this.isShuttingDown = false;
        resolve();
      }
    });
  }

  /**
   * Adds a domain to be managed for certificates
   * @param domain The domain to add
   */
  public addDomain(domain: string): void {
    if (!this.domainCertificates.has(domain)) {
      this.domainCertificates.set(domain, { certObtained: false, obtainingInProgress: false });
      console.log(`Domain added: ${domain}`);
    }
  }

  /**
   * Removes a domain from management
   * @param domain The domain to remove
   */
  public removeDomain(domain: string): void {
    if (this.domainCertificates.delete(domain)) {
      console.log(`Domain removed: ${domain}`);
    }
  }
  
  /**
   * Sets a certificate for a domain directly (for externally obtained certificates)
   * @param domain The domain for the certificate
   * @param certificate The certificate (PEM format)
   * @param privateKey The private key (PEM format)
   * @param expiryDate Optional expiry date
   */
  public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
    let domainInfo = this.domainCertificates.get(domain);
    
    if (!domainInfo) {
      domainInfo = { certObtained: false, obtainingInProgress: false };
      this.domainCertificates.set(domain, domainInfo);
    }
    
    domainInfo.certificate = certificate;
    domainInfo.privateKey = privateKey;
    domainInfo.certObtained = true;
    domainInfo.obtainingInProgress = false;
    
    if (expiryDate) {
      domainInfo.expiryDate = expiryDate;
    } else {
      // Try to extract expiry date from certificate
      try {
        // This is a simplistic approach - in a real implementation, use a proper
        // certificate parsing library like node-forge or x509
        const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
        if (matches && matches[1]) {
          domainInfo.expiryDate = new Date(matches[1]);
        }
      } catch (error) {
        console.warn(`Failed to extract expiry date from certificate for ${domain}`);
      }
    }
    
    console.log(`Certificate set for ${domain}`);
    
    // Emit certificate event
    this.emitCertificateEvent(CertManagerEvents.CERTIFICATE_ISSUED, {
      domain,
      certificate,
      privateKey,
      expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
    });
  }
  
  /**
   * Gets the certificate for a domain if it exists
   * @param domain The domain to get the certificate for
   */
  public getCertificate(domain: string): ICertificateData | null {
    const domainInfo = this.domainCertificates.get(domain);
    
    if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) {
      return null;
    }
    
    return {
      domain,
      certificate: domainInfo.certificate,
      privateKey: domainInfo.privateKey,
      expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
    };
  }

  /**
   * Lazy initialization of the ACME client
   * @returns An ACME client instance
   */
  private async getAcmeClient(): Promise<plugins.acme.Client> {
    if (this.acmeClient) {
      return this.acmeClient;
    }
    
    // Generate a new account key
    this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
    
    this.acmeClient = new plugins.acme.Client({
      directoryUrl: this.options.useProduction 
        ? plugins.acme.directory.letsencrypt.production 
        : plugins.acme.directory.letsencrypt.staging,
      accountKey: this.accountKey,
    });
    
    // Create a new account
    await this.acmeClient.createAccount({
      termsOfServiceAgreed: true,
      contact: [`mailto:${this.options.contactEmail}`],
    });
    
    return this.acmeClient;
  }

  /**
   * Handles incoming HTTP requests
   * @param req The HTTP request
   * @param res The HTTP response
   */
  private handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
    const hostHeader = req.headers.host;
    if (!hostHeader) {
      res.statusCode = 400;
      res.end('Bad Request: Host header is missing');
      return;
    }
    
    // Extract domain (ignoring any port in the Host header)
    const domain = hostHeader.split(':')[0];

    // If the request is for an ACME HTTP-01 challenge, handle it
    if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
      this.handleAcmeChallenge(req, res, domain);
      return;
    }

    if (!this.domainCertificates.has(domain)) {
      res.statusCode = 404;
      res.end('Domain not configured');
      return;
    }

    const domainInfo = this.domainCertificates.get(domain)!;

    // If certificate exists, redirect to HTTPS
    if (domainInfo.certObtained) {
      const httpsPort = this.options.httpsRedirectPort;
      const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
      const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
      
      res.statusCode = 301;
      res.setHeader('Location', redirectUrl);
      res.end(`Redirecting to ${redirectUrl}`);
    } else {
      // Trigger certificate issuance if not already running
      if (!domainInfo.obtainingInProgress) {
        this.obtainCertificate(domain).catch(err => {
          this.emit(CertManagerEvents.CERTIFICATE_FAILED, { domain, error: err.message });
          console.error(`Error obtaining certificate for ${domain}:`, err);
        });
      }
      
      res.statusCode = 503;
      res.end('Certificate issuance in progress, please try again later.');
    }
  }

  /**
   * Serves the ACME HTTP-01 challenge response
   * @param req The HTTP request
   * @param res The HTTP response
   * @param domain The domain for the challenge
   */
  private handleAcmeChallenge(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, domain: string): void {
    const domainInfo = this.domainCertificates.get(domain);
    if (!domainInfo) {
      res.statusCode = 404;
      res.end('Domain not configured');
      return;
    }
    
    // The token is the last part of the URL
    const urlParts = req.url?.split('/');
    const token = urlParts ? urlParts[urlParts.length - 1] : '';
    
    if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/plain');
      res.end(domainInfo.challengeKeyAuthorization);
      console.log(`Served ACME challenge response for ${domain}`);
    } else {
      res.statusCode = 404;
      res.end('Challenge token not found');
    }
  }

  /**
   * Obtains a certificate for a domain using ACME HTTP-01 challenge
   * @param domain The domain to obtain a certificate for
   * @param isRenewal Whether this is a renewal attempt
   */
  private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
    // Get the domain info
    const domainInfo = this.domainCertificates.get(domain);
    if (!domainInfo) {
      throw new Error(`Domain not found: ${domain}`);
    }
    
    // Prevent concurrent certificate issuance
    if (domainInfo.obtainingInProgress) {
      console.log(`Certificate issuance already in progress for ${domain}`);
      return;
    }
    
    domainInfo.obtainingInProgress = true;
    domainInfo.lastRenewalAttempt = new Date();
    
    try {
      const client = await this.getAcmeClient();

      // Create a new order for the domain
      const order = await client.createOrder({
        identifiers: [{ type: 'dns', value: domain }],
      });

      // Get the authorizations for the order
      const authorizations = await client.getAuthorizations(order);
      
      for (const authz of authorizations) {
        const challenge = authz.challenges.find(ch => ch.type === 'http-01');
        if (!challenge) {
          throw new Error('HTTP-01 challenge not found');
        }
        
        // Get the key authorization for the challenge
        const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
        
        // Store the challenge data
        domainInfo.challengeToken = challenge.token;
        domainInfo.challengeKeyAuthorization = keyAuthorization;

        // ACME client type definition workaround - use compatible approach
        // First check if challenge verification is needed
        const authzUrl = authz.url;
        
        try {
          // Check if authzUrl exists and perform verification
          if (authzUrl) {
            await client.verifyChallenge(authz, challenge);
          }
          
          // Complete the challenge
          await client.completeChallenge(challenge);
          
          // Wait for validation
          await client.waitForValidStatus(challenge);
          console.log(`HTTP-01 challenge completed for ${domain}`);
        } catch (error) {
          console.error(`Challenge error for ${domain}:`, error);
          throw error;
        }
      }

      // Generate a CSR and private key
      const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
        commonName: domain,
      });
      
      const csr = csrBuffer.toString();
      const privateKey = privateKeyBuffer.toString();

      // Finalize the order with our CSR
      await client.finalizeOrder(order, csr);
      
      // Get the certificate with the full chain
      const certificate = await client.getCertificate(order);

      // Store the certificate and key
      domainInfo.certificate = certificate;
      domainInfo.privateKey = privateKey;
      domainInfo.certObtained = true;
      
      // Clear challenge data
      delete domainInfo.challengeToken;
      delete domainInfo.challengeKeyAuthorization;
      
      // Extract expiry date from certificate
      try {
        const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
        if (matches && matches[1]) {
          domainInfo.expiryDate = new Date(matches[1]);
          console.log(`Certificate for ${domain} will expire on ${domainInfo.expiryDate.toISOString()}`);
        }
      } catch (error) {
        console.warn(`Failed to extract expiry date from certificate for ${domain}`);
      }

      console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
      
      // Emit the appropriate event
      const eventType = isRenewal 
        ? CertManagerEvents.CERTIFICATE_RENEWED 
        : CertManagerEvents.CERTIFICATE_ISSUED;
      
      this.emitCertificateEvent(eventType, {
        domain,
        certificate,
        privateKey,
        expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
      });
      
    } catch (error: any) {
      // Check for rate limit errors
      if (error.message && (
        error.message.includes('rateLimited') || 
        error.message.includes('too many certificates') || 
        error.message.includes('rate limit')
      )) {
        console.error(`Rate limit reached for ${domain}. Waiting before retry.`);
      } else {
        console.error(`Error during certificate issuance for ${domain}:`, error);
      }
      
      // Emit failure event
      this.emit(CertManagerEvents.CERTIFICATE_FAILED, {
        domain,
        error: error.message || 'Unknown error',
        isRenewal
      });
    } finally {
      // Reset flag whether successful or not
      domainInfo.obtainingInProgress = false;
    }
  }

  /**
   * Starts the certificate renewal timer
   */
  private startRenewalTimer(): void {
    if (this.renewalTimer) {
      clearInterval(this.renewalTimer);
    }
    
    // Convert hours to milliseconds
    const checkInterval = this.options.renewCheckIntervalHours * 60 * 60 * 1000;
    
    this.renewalTimer = setInterval(() => this.checkForRenewals(), checkInterval);
    
    // Prevent the timer from keeping the process alive
    if (this.renewalTimer.unref) {
      this.renewalTimer.unref();
    }
    
    console.log(`Certificate renewal check scheduled every ${this.options.renewCheckIntervalHours} hours`);
  }

  /**
   * Checks for certificates that need renewal
   */
  private checkForRenewals(): void {
    if (this.isShuttingDown) {
      return;
    }
    
    console.log('Checking for certificates that need renewal...');
    
    const now = new Date();
    const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
    
    for (const [domain, domainInfo] of this.domainCertificates.entries()) {
      // Skip domains without certificates or already in renewal
      if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
        continue;
      }
      
      // Skip domains without expiry dates
      if (!domainInfo.expiryDate) {
        continue;
      }
      
      const timeUntilExpiry = domainInfo.expiryDate.getTime() - now.getTime();
      
      // Check if certificate is near expiry
      if (timeUntilExpiry <= renewThresholdMs) {
        console.log(`Certificate for ${domain} expires soon, renewing...`);
        this.emit(CertManagerEvents.CERTIFICATE_EXPIRING, {
          domain,
          expiryDate: domainInfo.expiryDate,
          daysRemaining: Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000))
        });
        
        // Start renewal process
        this.obtainCertificate(domain, true).catch(err => {
          console.error(`Error renewing certificate for ${domain}:`, err);
        });
      }
    }
  }
  
  /**
   * Emits a certificate event with the certificate data
   * @param eventType The event type to emit
   * @param data The certificate data
   */
  private emitCertificateEvent(eventType: CertManagerEvents, data: ICertificateData): void {
    this.emit(eventType, data);
  }
}