import * as plugins from '../../plugins.js';
import { NetworkProxy } from '../network-proxy/index.js';
import type { IRouteConfig, IRouteTls } from './models/route-types.js';
import type { IAcmeOptions } from './models/interfaces.js';
import { CertStore } from './cert-store.js';

export interface ICertStatus {
  domain: string;
  status: 'valid' | 'pending' | 'expired' | 'error';
  expiryDate?: Date;
  issueDate?: Date;
  source: 'static' | 'acme';
  error?: string;
}

export interface ICertificateData {
  cert: string;
  key: string;
  ca?: string;
  expiryDate: Date;
  issueDate: Date;
}

export class SmartCertManager {
  private certStore: CertStore;
  private smartAcme: plugins.smartacme.SmartAcme | null = null;
  private networkProxy: NetworkProxy | null = null;
  private renewalTimer: NodeJS.Timeout | null = null;
  private pendingChallenges: Map<string, string> = new Map();
  private challengeRoute: IRouteConfig | null = null;
  
  // Track certificate status by route name
  private certStatus: Map<string, ICertStatus> = new Map();
  
  // Global ACME defaults from top-level configuration
  private globalAcmeDefaults: IAcmeOptions | null = null;
  
  // Callback to update SmartProxy routes for challenges
  private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
  
  constructor(
    private routes: IRouteConfig[],
    private certDir: string = './certs',
    private acmeOptions?: {
      email?: string;
      useProduction?: boolean;
      port?: number;
    }
  ) {
    this.certStore = new CertStore(certDir);
  }
  
  public setNetworkProxy(networkProxy: NetworkProxy): void {
    this.networkProxy = networkProxy;
  }
  
  /**
   * Set global ACME defaults from top-level configuration
   */
  public setGlobalAcmeDefaults(defaults: IAcmeOptions): void {
    this.globalAcmeDefaults = defaults;
  }
  
  /**
   * Set callback for updating routes (used for challenge routes)
   */
  public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise<void>): void {
    this.updateRoutesCallback = callback;
  }
  
  /**
   * Initialize certificate manager and provision certificates for all routes
   */
  public async initialize(): Promise<void> {
    // Create certificate directory if it doesn't exist
    await this.certStore.initialize();
    
    // Initialize SmartAcme if we have any ACME routes
    const hasAcmeRoutes = this.routes.some(r => 
      r.action.tls?.certificate === 'auto'
    );
    
    if (hasAcmeRoutes && this.acmeOptions?.email) {
      // Create HTTP-01 challenge handler
      const http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
      
      // Set up challenge handler integration with our routing
      this.setupChallengeHandler(http01Handler);
      
      // Create SmartAcme instance with built-in MemoryCertManager and HTTP-01 handler
      this.smartAcme = new plugins.smartacme.SmartAcme({
        accountEmail: this.acmeOptions.email,
        environment: this.acmeOptions.useProduction ? 'production' : 'integration',
        certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
        challengeHandlers: [http01Handler]
      });
      
      await this.smartAcme.start();
    }
    
    // Provision certificates for all routes
    await this.provisionAllCertificates();
    
    // Start renewal timer
    this.startRenewalTimer();
  }
  
  /**
   * Provision certificates for all routes that need them
   */
  private async provisionAllCertificates(): Promise<void> {
    const certRoutes = this.routes.filter(r => 
      r.action.tls?.mode === 'terminate' || 
      r.action.tls?.mode === 'terminate-and-reencrypt'
    );
    
    for (const route of certRoutes) {
      try {
        await this.provisionCertificate(route);
      } catch (error) {
        console.error(`Failed to provision certificate for route ${route.name}: ${error}`);
      }
    }
  }
  
  /**
   * Provision certificate for a single route
   */
  public async provisionCertificate(route: IRouteConfig): Promise<void> {
    const tls = route.action.tls;
    if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) {
      return;
    }
    
    const domains = this.extractDomainsFromRoute(route);
    if (domains.length === 0) {
      console.warn(`Route ${route.name} has TLS termination but no domains`);
      return;
    }
    
    const primaryDomain = domains[0];
    
    if (tls.certificate === 'auto') {
      // ACME certificate
      await this.provisionAcmeCertificate(route, domains);
    } else if (typeof tls.certificate === 'object') {
      // Static certificate
      await this.provisionStaticCertificate(route, primaryDomain, tls.certificate);
    }
  }
  
  /**
   * Provision ACME certificate
   */
  private async provisionAcmeCertificate(
    route: IRouteConfig, 
    domains: string[]
  ): Promise<void> {
    if (!this.smartAcme) {
      throw new Error(
        'SmartAcme not initialized. This usually means no ACME email was provided. ' +
        'Please ensure you have configured ACME with an email address either:\n' +
        '1. In the top-level "acme" configuration\n' +
        '2. In the route\'s "tls.acme" configuration'
      );
    }
    
    const primaryDomain = domains[0];
    const routeName = route.name || primaryDomain;
    
    // Check if we already have a valid certificate
    const existingCert = await this.certStore.getCertificate(routeName);
    if (existingCert && this.isCertificateValid(existingCert)) {
      console.log(`Using existing valid certificate for ${primaryDomain}`);
      await this.applyCertificate(primaryDomain, existingCert);
      this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
      return;
    }
    
    // Apply renewal threshold from global defaults or route config
    const renewThreshold = route.action.tls?.acme?.renewBeforeDays || 
                         this.globalAcmeDefaults?.renewThresholdDays || 
                         30;
    
    console.log(`Requesting ACME certificate for ${domains.join(', ')} (renew ${renewThreshold} days before expiry)`);
    this.updateCertStatus(routeName, 'pending', 'acme');
    
    try {
      // Add challenge route before requesting certificate
      await this.addChallengeRoute();
      
      try {
        // Use smartacme to get certificate
        const cert = await this.smartAcme.getCertificateForDomain(primaryDomain);
      
      // SmartAcme's Cert object has these properties:
      // - publicKey: The certificate PEM string  
      // - privateKey: The private key PEM string
      // - csr: Certificate signing request
      // - validUntil: Timestamp in milliseconds
      // - domainName: The domain name
      const certData: ICertificateData = {
        cert: cert.publicKey,
        key: cert.privateKey, 
        ca: cert.publicKey, // Use same as cert for now
        expiryDate: new Date(cert.validUntil),
        issueDate: new Date(cert.created)
      };
      
      await this.certStore.saveCertificate(routeName, certData);
      await this.applyCertificate(primaryDomain, certData);
      this.updateCertStatus(routeName, 'valid', 'acme', certData);
      
        console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`);
      } catch (error) {
        console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`);
        this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
        throw error;
      } finally {
        // Always remove challenge route after provisioning
        await this.removeChallengeRoute();
      }
    } catch (error) {
      // Handle outer try-catch from adding challenge route
      console.error(`Failed to setup ACME challenge for ${primaryDomain}: ${error}`);
      this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
      throw error;
    }
  }
  
  /**
   * Provision static certificate
   */
  private async provisionStaticCertificate(
    route: IRouteConfig,
    domain: string,
    certConfig: { key: string; cert: string; keyFile?: string; certFile?: string }
  ): Promise<void> {
    const routeName = route.name || domain;
    
    try {
      let key: string = certConfig.key;
      let cert: string = certConfig.cert;
      
      // Load from files if paths are provided
      if (certConfig.keyFile) {
        const keyFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.keyFile);
        key = keyFile.contents.toString();
      }
      if (certConfig.certFile) {
        const certFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.certFile);
        cert = certFile.contents.toString();
      }
      
      // Parse certificate to get dates
      // Parse certificate to get dates - for now just use defaults
      // TODO: Implement actual certificate parsing if needed
      const certInfo = { validTo: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), validFrom: new Date() };
      
      const certData: ICertificateData = {
        cert,
        key,
        expiryDate: certInfo.validTo,
        issueDate: certInfo.validFrom
      };
      
      // Save to store for consistency
      await this.certStore.saveCertificate(routeName, certData);
      await this.applyCertificate(domain, certData);
      this.updateCertStatus(routeName, 'valid', 'static', certData);
      
      console.log(`Successfully loaded static certificate for ${domain}`);
    } catch (error) {
      console.error(`Failed to provision static certificate for ${domain}: ${error}`);
      this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
      throw error;
    }
  }
  
  /**
   * Apply certificate to NetworkProxy
   */
  private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
    if (!this.networkProxy) {
      console.warn('NetworkProxy not set, cannot apply certificate');
      return;
    }
    
    // Apply certificate to NetworkProxy
    this.networkProxy.updateCertificate(domain, certData.cert, certData.key);
    
    // Also apply for wildcard if it's a subdomain
    if (domain.includes('.') && !domain.startsWith('*.')) {
      const parts = domain.split('.');
      if (parts.length >= 2) {
        const wildcardDomain = `*.${parts.slice(-2).join('.')}`;
        this.networkProxy.updateCertificate(wildcardDomain, certData.cert, certData.key);
      }
    }
  }
  
  /**
   * Extract domains from route configuration
   */
  private extractDomainsFromRoute(route: IRouteConfig): string[] {
    if (!route.match.domains) {
      return [];
    }
    
    const domains = Array.isArray(route.match.domains) 
      ? route.match.domains 
      : [route.match.domains];
    
    // Filter out wildcards and patterns
    return domains.filter(d => 
      !d.includes('*') && 
      !d.includes('{') && 
      d.includes('.')
    );
  }
  
  /**
   * Check if certificate is valid
   */
  private isCertificateValid(cert: ICertificateData): boolean {
    const now = new Date();
    
    // Use renewal threshold from global defaults or fallback to 30 days
    const renewThresholdDays = this.globalAcmeDefaults?.renewThresholdDays || 30;
    const expiryThreshold = new Date(now.getTime() + renewThresholdDays * 24 * 60 * 60 * 1000);
    
    return cert.expiryDate > expiryThreshold;
  }
  
  
  /**
   * Add challenge route to SmartProxy
   */
  private async addChallengeRoute(): Promise<void> {
    if (!this.updateRoutesCallback) {
      throw new Error('No route update callback set');
    }
    
    if (!this.challengeRoute) {
      throw new Error('Challenge route not initialized');
    }
    const challengeRoute = this.challengeRoute;
    
    const updatedRoutes = [...this.routes, challengeRoute];
    await this.updateRoutesCallback(updatedRoutes);
  }
  
  /**
   * Remove challenge route from SmartProxy
   */
  private async removeChallengeRoute(): Promise<void> {
    if (!this.updateRoutesCallback) {
      return;
    }
    
    const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
    await this.updateRoutesCallback(filteredRoutes);
  }
  
  /**
   * Start renewal timer
   */
  private startRenewalTimer(): void {
    // Check for renewals every 12 hours
    this.renewalTimer = setInterval(() => {
      this.checkAndRenewCertificates();
    }, 12 * 60 * 60 * 1000);
    
    // Also do an immediate check
    this.checkAndRenewCertificates();
  }
  
  /**
   * Check and renew certificates that are expiring
   */
  private async checkAndRenewCertificates(): Promise<void> {
    for (const route of this.routes) {
      if (route.action.tls?.certificate === 'auto') {
        const routeName = route.name || this.extractDomainsFromRoute(route)[0];
        const cert = await this.certStore.getCertificate(routeName);
        
        if (cert && !this.isCertificateValid(cert)) {
          console.log(`Certificate for ${routeName} needs renewal`);
          try {
            await this.provisionCertificate(route);
          } catch (error) {
            console.error(`Failed to renew certificate for ${routeName}: ${error}`);
          }
        }
      }
    }
  }
  
  /**
   * Update certificate status
   */
  private updateCertStatus(
    routeName: string,
    status: ICertStatus['status'],
    source: ICertStatus['source'],
    certData?: ICertificateData,
    error?: string
  ): void {
    this.certStatus.set(routeName, {
      domain: routeName,
      status,
      source,
      expiryDate: certData?.expiryDate,
      issueDate: certData?.issueDate,
      error
    });
  }
  
  /**
   * Get certificate status for a route
   */
  public getCertificateStatus(routeName: string): ICertStatus | undefined {
    return this.certStatus.get(routeName);
  }
  
  /**
   * Force renewal of a certificate
   */
  public async renewCertificate(routeName: string): Promise<void> {
    const route = this.routes.find(r => r.name === routeName);
    if (!route) {
      throw new Error(`Route ${routeName} not found`);
    }
    
    // Remove existing certificate to force renewal
    await this.certStore.deleteCertificate(routeName);
    await this.provisionCertificate(route);
  }
  
  /**
   * Setup challenge handler integration with SmartProxy routing
   */
  private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void {
    // Use challenge port from global config or default to 80
    const challengePort = this.globalAcmeDefaults?.port || 80;
    
    // Create a challenge route that delegates to SmartAcme's HTTP-01 handler
    const challengeRoute: IRouteConfig = {
      name: 'acme-challenge',
      priority: 1000,  // High priority
      match: {
        ports: challengePort,
        path: '/.well-known/acme-challenge/*'
      },
      action: {
        type: 'static',
        handler: async (context) => {
          // Extract the token from the path
          const token = context.path?.split('/').pop();
          if (!token) {
            return { status: 404, body: 'Not found' };
          }
          
          // Create mock request/response objects for SmartAcme
          const mockReq = {
            url: context.path,
            method: 'GET',
            headers: context.headers || {}
          };
          
          let responseData: any = null;
          const mockRes = {
            statusCode: 200,
            setHeader: (name: string, value: string) => {},
            end: (data: any) => {
              responseData = data;
            }
          };
          
          // Use SmartAcme's handler
          const handled = await new Promise<boolean>((resolve) => {
            http01Handler.handleRequest(mockReq as any, mockRes as any, () => {
              resolve(false);
            });
            // Give it a moment to process
            setTimeout(() => resolve(true), 100);
          });
          
          if (handled && responseData) {
            return {
              status: mockRes.statusCode,
              headers: { 'Content-Type': 'text/plain' },
              body: responseData
            };
          } else {
            return { status: 404, body: 'Not found' };
          }
        }
      }
    };
    
    // Store the challenge route to add it when needed
    this.challengeRoute = challengeRoute;
  }
  
  /**
   * Stop certificate manager
   */
  public async stop(): Promise<void> {
    if (this.renewalTimer) {
      clearInterval(this.renewalTimer);
      this.renewalTimer = null;
    }
    
    if (this.smartAcme) {
      await this.smartAcme.stop();
    }
    
    // Remove any active challenge routes
    if (this.pendingChallenges.size > 0) {
      this.pendingChallenges.clear();
      await this.removeChallengeRoute();
    }
  }
  
  /**
   * Get ACME options (for recreating after route updates)
   */
  public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined {
    return this.acmeOptions;
  }
}