/**
 * Certificate Requirement Manager
 *
 * Manages the lifecycle of SSL certificates based on service requirements.
 * Automatically acquires, renews, and cleans up certificates.
 */

import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
import { OneboxDatabase } from './database.ts';
import { OneboxSslManager } from './ssl.ts';
import type { ICertRequirement, ICertificate, IDomain } from '../types.ts';

export class CertRequirementManager {
  private database: OneboxDatabase;
  private sslManager: OneboxSslManager;

  // Certificate renewal threshold (30 days before expiry)
  private readonly RENEWAL_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1000;

  // Certificate cleanup delay (90 days after becoming invalid)
  private readonly CLEANUP_DELAY_MS = 90 * 24 * 60 * 60 * 1000;

  constructor(database: OneboxDatabase, sslManager: OneboxSslManager) {
    this.database = database;
    this.sslManager = sslManager;
  }

  /**
   * Process all pending certificate requirements
   * Matches requirements to existing certificates or schedules acquisition
   */
  async processPendingRequirements(): Promise<void> {
    try {
      const allRequirements = this.database.getAllCertRequirements();
      const pendingRequirements = allRequirements.filter((req) => req.status === 'pending');

      logger.info(`Processing ${pendingRequirements.length} pending certificate requirement(s)`);

      for (const requirement of pendingRequirements) {
        try {
          await this.processRequirement(requirement);
        } catch (error) {
          logger.error(
            `Failed to process requirement ${requirement.id}: ${getErrorMessage(error)}`
          );
        }
      }
    } catch (error) {
      logger.error(`Failed to process pending requirements: ${getErrorMessage(error)}`);
    }
  }

  /**
   * Process a single certificate requirement
   */
  private async processRequirement(requirement: ICertRequirement): Promise<void> {
    const domain = this.database.getDomainById(requirement.domainId);
    if (!domain) {
      logger.error(`Domain ${requirement.domainId} not found for requirement ${requirement.id}`);
      return;
    }

    // Construct the full domain name
    const fullDomain = requirement.subdomain
      ? `${requirement.subdomain}.${domain.domain}`
      : domain.domain;

    logger.debug(`Processing requirement for domain: ${fullDomain}`);

    // Check if a valid certificate already exists
    const existingCert = this.findValidCertificate(domain, requirement.subdomain);

    if (existingCert) {
      // Link existing certificate to requirement
      this.database.updateCertRequirement(requirement.id!, {
        certificateId: existingCert.id,
        status: 'active',
      });
      logger.info(`Linked existing certificate to requirement for ${fullDomain}`);
    } else {
      // Schedule certificate acquisition
      await this.acquireCertificate(requirement, domain, fullDomain);
    }
  }

  /**
   * Find a valid certificate for the given domain and subdomain
   */
  private findValidCertificate(
    domain: IDomain,
    subdomain: string
  ): ICertificate | null {
    const certificates = this.database.getCertificatesByDomain(domain.id!);
    const now = Date.now();

    for (const cert of certificates) {
      // Skip invalid or expired certificates
      if (!cert.isValid || cert.expiryDate <= now) {
        continue;
      }

      // Check if certificate covers the required domain
      if (cert.isWildcard && !subdomain) {
        // Wildcard cert covers base domain
        return cert;
      } else if (cert.isWildcard && subdomain) {
        // Wildcard cert covers first-level subdomains
        const levels = subdomain.split('.').length;
        if (levels === 1) {
          return cert;
        }
      } else if (!cert.isWildcard && cert.certDomain === domain.domain && !subdomain) {
        // Exact match for base domain
        return cert;
      } else if (!cert.isWildcard && subdomain) {
        // Exact match for specific subdomain
        const fullDomain = `${subdomain}.${domain.domain}`;
        if (cert.certDomain === fullDomain) {
          return cert;
        }
      }
    }

    return null;
  }

  /**
   * Acquire a new certificate for the requirement
   */
  private async acquireCertificate(
    requirement: ICertRequirement,
    domain: IDomain,
    fullDomain: string
  ): Promise<void> {
    try {
      logger.info(`Acquiring certificate for ${fullDomain}...`);

      // Determine if we should use wildcard
      const useWildcard = domain.defaultWildcard && !requirement.subdomain;

      // Acquire certificate using SSL manager
      const certData = await this.sslManager.acquireCertificate(
        domain.domain,
        useWildcard
      );

      // Store certificate in database
      const now = Date.now();
      const certificate = this.database.createCertificate({
        domainId: domain.id!,
        certDomain: domain.domain,
        isWildcard: useWildcard,
        certPem: certData.certPem,
        keyPem: certData.keyPem,
        fullchainPem: certData.fullchainPem,
        expiryDate: certData.expiryDate,
        issuer: certData.issuer,
        isValid: true,
        createdAt: now,
        updatedAt: now,
      });

      // Link certificate to requirement
      this.database.updateCertRequirement(requirement.id!, {
        certificateId: certificate.id,
        status: 'active',
      });

      logger.success(`Certificate acquired for ${fullDomain}`);
    } catch (error) {
      logger.error(`Failed to acquire certificate for ${fullDomain}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Check all certificates for renewal
   */
  async checkCertificateRenewal(): Promise<void> {
    try {
      const allCertificates = this.database.getAllCertificates();
      const now = Date.now();

      for (const cert of allCertificates) {
        // Skip invalid certificates
        if (!cert.isValid) {
          continue;
        }

        // Check if certificate is expired
        if (cert.expiryDate <= now) {
          // Mark as invalid
          this.database.updateCertificate(cert.id!, { isValid: false });
          logger.warn(`Certificate ${cert.id} for ${cert.certDomain} has expired`);
          continue;
        }

        // Check if certificate needs renewal
        const timeUntilExpiry = cert.expiryDate - now;
        if (timeUntilExpiry <= this.RENEWAL_THRESHOLD_MS) {
          await this.renewCertificate(cert);
        }
      }
    } catch (error) {
      logger.error(`Failed to check certificate renewal: ${getErrorMessage(error)}`);
    }
  }

  /**
   * Renew a certificate
   */
  private async renewCertificate(cert: ICertificate): Promise<void> {
    try {
      logger.info(`Renewing certificate for ${cert.certDomain}...`);

      const domain = this.database.getDomainById(cert.domainId);
      if (!domain) {
        logger.error(`Domain ${cert.domainId} not found for certificate ${cert.id}`);
        return;
      }

      // Mark all requirements using this certificate as renewing
      const requirements = this.database.getAllCertRequirements();
      const relatedRequirements = requirements.filter(
        (req) => req.certificateId === cert.id
      );

      for (const req of relatedRequirements) {
        this.database.updateCertRequirement(req.id!, { status: 'renewing' });
      }

      // Acquire new certificate
      const certData = await this.sslManager.acquireCertificate(
        domain.domain,
        cert.isWildcard
      );

      // Update certificate in database
      this.database.updateCertificate(cert.id!, {
        certPem: certData.certPem,
        keyPem: certData.keyPem,
        fullchainPem: certData.fullchainPem,
        expiryDate: certData.expiryDate,
        issuer: certData.issuer,
      });

      // Mark requirements as active again
      for (const req of relatedRequirements) {
        this.database.updateCertRequirement(req.id!, { status: 'active' });
      }

      logger.success(`Certificate renewed for ${cert.certDomain}`);
    } catch (error) {
      logger.error(`Failed to renew certificate for ${cert.certDomain}: ${getErrorMessage(error)}`);
    }
  }

  /**
   * Clean up old invalid certificates (90+ days old)
   */
  cleanupOldCertificates(): void {
    try {
      const allCertificates = this.database.getAllCertificates();
      const now = Date.now();
      let deletedCount = 0;

      for (const cert of allCertificates) {
        // Only clean up invalid certificates
        if (!cert.isValid) {
          // Check if certificate has been invalid for 90+ days
          const timeSinceExpiry = now - cert.expiryDate;
          if (timeSinceExpiry >= this.CLEANUP_DELAY_MS) {
            // Delete from database (PEM content is stored in DB, no files to delete)
            this.database.deleteCertificate(cert.id!);
            deletedCount++;
            logger.info(
              `Deleted old certificate ${cert.id} for ${cert.certDomain} (expired ${new Date(
                cert.expiryDate
              ).toISOString()})`
            );
          }
        }
      }

      if (deletedCount > 0) {
        logger.info(`Cleaned up ${deletedCount} old certificate(s)`);
      }
    } catch (error) {
      logger.error(`Failed to cleanup old certificates: ${getErrorMessage(error)}`);
    }
  }

  /**
   * Get certificate status for a domain
   */
  getCertificateStatus(domainId: number): {
    valid: number;
    expiringSoon: number;
    expired: number;
    pending: number;
  } {
    const certificates = this.database.getCertificatesByDomain(domainId);
    const requirements = this.database.getCertRequirementsByDomain(domainId);
    const now = Date.now();

    let valid = 0;
    let expiringSoon = 0;
    let expired = 0;

    for (const cert of certificates) {
      if (!cert.isValid) {
        expired++;
      } else if (cert.expiryDate <= now) {
        expired++;
      } else if (cert.expiryDate - now <= this.RENEWAL_THRESHOLD_MS) {
        expiringSoon++;
      } else {
        valid++;
      }
    }

    const pending = requirements.filter((req) => req.status === 'pending').length;

    return { valid, expiringSoon, expired, pending };
  }
}
