import * as http from 'http';
import * as acme from 'acme-client';

interface IDomainCertificate {
  certObtained: boolean;
  obtainingInProgress: boolean;
  certificate?: string;
  privateKey?: string;
  challengeToken?: string;
  challengeKeyAuthorization?: string;
}

export class Port80Handler {
  private domainCertificates: Map<string, IDomainCertificate>;
  private server: http.Server;
  private acmeClient: acme.Client | null = null;
  private accountKey: string | null = null;

  constructor() {
    this.domainCertificates = new Map<string, IDomainCertificate>();

    // Create and start an HTTP server on port 80.
    this.server = http.createServer((req, res) => this.handleRequest(req, res));
    this.server.listen(80, () => {
      console.log('Port80Handler is listening on port 80');
    });
  }

  /**
   * Adds a domain to be managed.
   * @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}`);
    }
  }

  /**
   * Lazy initialization of the ACME client.
   * Uses Let’s Encrypt’s production directory (for testing you might switch to staging).
   */
  private async getAcmeClient(): Promise<acme.Client> {
    if (this.acmeClient) {
      return this.acmeClient;
    }
    // Generate a new account key and convert Buffer to string.
    this.accountKey = (await acme.forge.createPrivateKey()).toString();
    this.acmeClient = new acme.Client({
      directoryUrl: acme.directory.letsencrypt.production, // Use production for a real certificate
      // For testing, you could use:
      // directoryUrl: acme.directory.letsencrypt.staging,
      accountKey: this.accountKey,
    });
    // Create a new account. Make sure to update the contact email.
    await this.acmeClient.createAccount({
      termsOfServiceAgreed: true,
      contact: ['mailto:admin@example.com'],
    });
    return this.acmeClient;
  }

  /**
   * Handles incoming HTTP requests on port 80.
   * If the request is for an ACME challenge, it responds with the key authorization.
   * If the domain has a certificate, it redirects to HTTPS; otherwise, it initiates certificate issuance.
   */
  private handleRequest(req: http.IncomingMessage, res: 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 on port 443.
    if (domainInfo.certObtained) {
      const redirectUrl = `https://${domain}:443${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) {
        domainInfo.obtainingInProgress = true;
        this.obtainCertificate(domain).catch(err => {
          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.
   */
  private handleAcmeChallenge(req: http.IncomingMessage, res: 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');
    }
  }

  /**
   * Uses acme-client to perform a full ACME HTTP-01 challenge to obtain a certificate.
   * On success, it stores the certificate and key in memory and clears challenge data.
   */
  private async obtainCertificate(domain: string): Promise<void> {
    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);
        const domainInfo = this.domainCertificates.get(domain)!;
        domainInfo.challengeToken = challenge.token;
        domainInfo.challengeKeyAuthorization = keyAuthorization;

        // Notify the ACME server that the challenge is ready.
        // The acme-client examples show that verifyChallenge takes three arguments:
        // (authorization, challenge, keyAuthorization). However, the official TypeScript
        // types appear to be out-of-sync. As a workaround, we cast client to 'any'.
        await (client as any).verifyChallenge(authz, challenge, keyAuthorization);

        await client.completeChallenge(challenge);
        // Wait until the challenge is validated.
        await client.waitForValidStatus(challenge);
        console.log(`HTTP-01 challenge completed for ${domain}`);
      }

      // Generate a CSR and a new private key for the domain.
      // Convert the resulting Buffers to strings.
      const [csrBuffer, privateKeyBuffer] = await acme.forge.createCsr({
        commonName: domain,
      });
      const csr = csrBuffer.toString();
      const privateKey = privateKeyBuffer.toString();

      // Finalize the order and obtain the certificate.
      await client.finalizeOrder(order, csr);
      const certificate = await client.getCertificate(order);

      const domainInfo = this.domainCertificates.get(domain)!;
      domainInfo.certificate = certificate;
      domainInfo.privateKey = privateKey;
      domainInfo.certObtained = true;
      domainInfo.obtainingInProgress = false;
      delete domainInfo.challengeToken;
      delete domainInfo.challengeKeyAuthorization;

      console.log(`Certificate obtained for ${domain}`);
      // In a production system, persist the certificate and key and reload your TLS server.
    } catch (error) {
      console.error(`Error during certificate issuance for ${domain}:`, error);
      const domainInfo = this.domainCertificates.get(domain);
      if (domainInfo) {
        domainInfo.obtainingInProgress = false;
      }
    }
  }
}