import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
import { AcmeCertDoc, ProxyCertDoc } from '../../db/index.js';

export class CertificateHandler {
  constructor(private opsServerRef: OpsServer) {
    this.registerHandlers();
  }

  private registerHandlers(): void {
    const viewRouter = this.opsServerRef.viewRouter;
    const adminRouter = this.opsServerRef.adminRouter;

    // ---- Read endpoints (viewRouter — valid identity required via middleware) ----

    // Get Certificate Overview
    viewRouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
        'getCertificateOverview',
        async (dataArg) => {
          const certificates = await this.buildCertificateOverview();
          const summary = this.buildSummary(certificates);
          return { certificates, summary };
        }
      )
    );

    // ---- Write endpoints (adminRouter — admin identity required via middleware) ----

    // Legacy route-based reprovision (backward compat)
    adminRouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
        'reprovisionCertificate',
        async (dataArg) => {
          return this.reprovisionCertificateByRoute(dataArg.routeName);
        }
      )
    );

    // Domain-based reprovision (preferred)
    adminRouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
        'reprovisionCertificateDomain',
        async (dataArg) => {
          return this.reprovisionCertificateDomain(dataArg.domain);
        }
      )
    );

    // Delete certificate
    adminRouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
        'deleteCertificate',
        async (dataArg) => {
          return this.deleteCertificate(dataArg.domain);
        }
      )
    );

    // Export certificate
    adminRouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
        'exportCertificate',
        async (dataArg) => {
          return this.exportCertificate(dataArg.domain);
        }
      )
    );

    // Import certificate
    adminRouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
        'importCertificate',
        async (dataArg) => {
          return this.importCertificate(dataArg.cert);
        }
      )
    );
  }

  /**
   * Build domain-centric certificate overview.
   * Instead of one row per route, we produce one row per unique domain.
   */
  private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
    const dcRouter = this.opsServerRef.dcRouterRef;
    const smartProxy = dcRouter.smartProxy;
    if (!smartProxy) return [];

    const routes = smartProxy.routeManager.getRoutes();

    // Phase 1: Collect unique domains with their associated route info
    const domainMap = new Map<string, {
      routeNames: string[];
      source: interfaces.requests.TCertificateSource;
      tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
      canReprovision: boolean;
    }>();

    for (const route of routes) {
      if (!route.name) continue;

      const tls = route.action?.tls;
      if (!tls) continue;

      // Skip passthrough routes - they don't manage certificates
      if (tls.mode === 'passthrough') continue;

      const routeDomains = route.match.domains
        ? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
        : [];

      // Determine source
      let source: interfaces.requests.TCertificateSource = 'none';
      if (tls.certificate === 'auto') {
        if ((smartProxy.settings as any).certProvisionFunction) {
          source = 'provision-function';
        } else {
          source = 'acme';
        }
      } else if (tls.certificate && typeof tls.certificate === 'object') {
        source = 'static';
      }

      const canReprovision = source === 'acme' || source === 'provision-function';
      const tlsMode = tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough';

      for (const domain of routeDomains) {
        const existing = domainMap.get(domain);
        if (existing) {
          // Add this route name to the existing domain entry
          if (!existing.routeNames.includes(route.name)) {
            existing.routeNames.push(route.name);
          }
          // Upgrade source if more specific
          if (existing.source === 'none' && source !== 'none') {
            existing.source = source;
            existing.canReprovision = canReprovision;
          }
        } else {
          domainMap.set(domain, {
            routeNames: [route.name],
            source,
            tlsMode,
            canReprovision,
          });
        }
      }
    }

    // Phase 2: Resolve status for each unique domain
    const certificates: interfaces.requests.ICertificateInfo[] = [];

    for (const [domain, info] of domainMap) {
      let status: interfaces.requests.TCertificateStatus = 'unknown';
      let expiryDate: string | undefined;
      let issuedAt: string | undefined;
      let issuer: string | undefined;
      let error: string | undefined;

      // Check event-based status from certificateStatusMap (now keyed by domain)
      const eventStatus = dcRouter.certificateStatusMap.get(domain);
      if (eventStatus) {
        status = eventStatus.status;
        expiryDate = eventStatus.expiryDate;
        issuedAt = eventStatus.issuedAt;
        error = eventStatus.error;
        if (eventStatus.source) {
          issuer = eventStatus.source;
        }
      }

      // Try SmartProxy certificate status if no event data
      if (status === 'unknown' && info.routeNames.length > 0) {
        try {
          const rustStatus = await smartProxy.getCertificateStatus(info.routeNames[0]);
          if (rustStatus) {
            if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
            if (rustStatus.issuer) issuer = rustStatus.issuer;
            if (rustStatus.issuedAt) issuedAt = rustStatus.issuedAt;
            if (rustStatus.status === 'valid' || rustStatus.status === 'expired') {
              status = rustStatus.status;
            }
          }
        } catch {
          // Rust bridge may not support this command yet — ignore
        }
      }

      // Check persisted cert data from smartdata document classes
      if (status === 'unknown') {
        const cleanDomain = domain.replace(/^\*\.?/, '');
        const acmeDoc = await AcmeCertDoc.findByDomain(cleanDomain);
        const proxyDoc = !acmeDoc ? await ProxyCertDoc.findByDomain(domain) : null;

        if (acmeDoc?.validUntil) {
          expiryDate = new Date(acmeDoc.validUntil).toISOString();
          if (acmeDoc.created) {
            issuedAt = new Date(acmeDoc.created).toISOString();
          }
          issuer = 'smartacme-dns-01';
        } else if (proxyDoc?.publicKey) {
          // certStore has the cert — parse PEM for expiry
          try {
            const x509 = new plugins.crypto.X509Certificate(proxyDoc.publicKey);
            expiryDate = new Date(x509.validTo).toISOString();
            issuedAt = new Date(x509.validFrom).toISOString();
          } catch { /* PEM parsing failed */ }
          status = 'valid';
          issuer = 'cert-store';
        } else if (acmeDoc || proxyDoc) {
          status = 'valid';
          issuer = 'cert-store';
        }
      }

      // Compute status from expiry date
      if (expiryDate && (status === 'valid' || status === 'unknown')) {
        const expiry = new Date(expiryDate);
        const now = new Date();
        const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);

        if (daysUntilExpiry < 0) {
          status = 'expired';
        } else if (daysUntilExpiry < 30) {
          status = 'expiring';
        } else {
          status = 'valid';
        }
      }

      // Static certs with no other info default to 'valid'
      if (info.source === 'static' && status === 'unknown') {
        status = 'valid';
      }

      // ACME/provision-function routes with no cert data are still provisioning
      if (status === 'unknown' && (info.source === 'acme' || info.source === 'provision-function')) {
        status = 'provisioning';
      }

      // Phase 3: Attach backoff info
      let backoffInfo: interfaces.requests.ICertificateInfo['backoffInfo'];
      if (dcRouter.certProvisionScheduler) {
        const bi = await dcRouter.certProvisionScheduler.getBackoffInfo(domain);
        if (bi) {
          backoffInfo = bi;
        }
      }

      certificates.push({
        domain,
        routeNames: info.routeNames,
        status,
        source: info.source,
        tlsMode: info.tlsMode,
        expiryDate,
        issuer,
        issuedAt,
        error,
        canReprovision: info.canReprovision,
        backoffInfo,
      });
    }

    return certificates;
  }

  private buildSummary(certificates: interfaces.requests.ICertificateInfo[]): {
    total: number;
    valid: number;
    expiring: number;
    expired: number;
    failed: number;
    unknown: number;
  } {
    const summary = { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 };
    summary.total = certificates.length;
    for (const cert of certificates) {
      switch (cert.status) {
        case 'valid': summary.valid++; break;
        case 'expiring': summary.expiring++; break;
        case 'expired': summary.expired++; break;
        case 'failed': summary.failed++; break;
        case 'provisioning': // count as unknown
        case 'unknown': summary.unknown++; break;
      }
    }
    return summary;
  }

  /**
   * Legacy route-based reprovisioning
   */
  private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
    const dcRouter = this.opsServerRef.dcRouterRef;
    const smartProxy = dcRouter.smartProxy;

    if (!smartProxy) {
      return { success: false, message: 'SmartProxy is not running' };
    }

    try {
      await smartProxy.provisionCertificate(routeName);
      // Clear event-based status for domains in this route
      for (const [domain, entry] of dcRouter.certificateStatusMap) {
        if (entry.routeNames.includes(routeName)) {
          dcRouter.certificateStatusMap.delete(domain);
        }
      }
      return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
    } catch (err: unknown) {
      return { success: false, message: (err as Error).message || 'Failed to reprovision certificate' };
    }
  }

  /**
   * Domain-based reprovisioning — clears backoff first, then triggers provision
   */
  private async reprovisionCertificateDomain(domain: string): Promise<{ success: boolean; message?: string }> {
    const dcRouter = this.opsServerRef.dcRouterRef;
    const smartProxy = dcRouter.smartProxy;

    if (!smartProxy) {
      return { success: false, message: 'SmartProxy is not running' };
    }

    // Clear backoff for this domain (user override)
    if (dcRouter.certProvisionScheduler) {
      await dcRouter.certProvisionScheduler.clearBackoff(domain);
    }

    // Clear status map entry so it gets refreshed
    dcRouter.certificateStatusMap.delete(domain);

    // Try to provision via SmartAcme directly
    if (dcRouter.smartAcme) {
      try {
        await dcRouter.smartAcme.getCertificateForDomain(domain);
        return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}'` };
      } catch (err: unknown) {
        return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
      }
    }

    // Fallback: try provisioning via the first matching route
    const routeNames = dcRouter.findRouteNamesForDomain(domain);
    if (routeNames.length > 0) {
      try {
        await smartProxy.provisionCertificate(routeNames[0]);
        return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}' via route '${routeNames[0]}'` };
      } catch (err: unknown) {
        return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
      }
    }

    return { success: false, message: `No routes found for domain '${domain}'` };
  }

  /**
   * Delete certificate data for a domain from storage
   */
  private async deleteCertificate(domain: string): Promise<{ success: boolean; message?: string }> {
    const dcRouter = this.opsServerRef.dcRouterRef;
    const cleanDomain = domain.replace(/^\*\.?/, '');

    // Delete from smartdata document classes
    const acmeDoc = await AcmeCertDoc.findByDomain(cleanDomain);
    if (acmeDoc) {
      await acmeDoc.delete();
    }

    // Try both original domain and clean domain for proxy certs
    for (const d of [domain, cleanDomain]) {
      const proxyDoc = await ProxyCertDoc.findByDomain(d);
      if (proxyDoc) {
        await proxyDoc.delete();
      }
    }

    // Clear from in-memory status map
    dcRouter.certificateStatusMap.delete(domain);

    // Clear backoff info
    if (dcRouter.certProvisionScheduler) {
      await dcRouter.certProvisionScheduler.clearBackoff(domain);
    }

    return { success: true, message: `Certificate data deleted for '${domain}'` };
  }

  /**
   * Export certificate data for a domain as ICert-shaped JSON
   */
  private async exportCertificate(domain: string): Promise<{
    success: boolean;
    cert?: {
      id: string;
      domainName: string;
      created: number;
      validUntil: number;
      privateKey: string;
      publicKey: string;
      csr: string;
    };
    message?: string;
  }> {
    const cleanDomain = domain.replace(/^\*\.?/, '');

    // Try AcmeCertDoc first (has full ICert fields)
    const acmeDoc = await AcmeCertDoc.findByDomain(cleanDomain);
    if (acmeDoc && acmeDoc.publicKey && acmeDoc.privateKey) {
      return {
        success: true,
        cert: {
          id: acmeDoc.id || plugins.crypto.randomUUID(),
          domainName: acmeDoc.domainName || domain,
          created: acmeDoc.created || Date.now(),
          validUntil: acmeDoc.validUntil || 0,
          privateKey: acmeDoc.privateKey,
          publicKey: acmeDoc.publicKey,
          csr: acmeDoc.csr || '',
        },
      };
    }

    // Fallback: try ProxyCertDoc with original domain, then clean domain
    let proxyDoc = await ProxyCertDoc.findByDomain(domain);
    if (!proxyDoc || !proxyDoc.publicKey) {
      proxyDoc = await ProxyCertDoc.findByDomain(cleanDomain);
    }

    if (proxyDoc && proxyDoc.publicKey && proxyDoc.privateKey) {
      return {
        success: true,
        cert: {
          id: plugins.crypto.randomUUID(),
          domainName: domain,
          created: proxyDoc.validFrom || Date.now(),
          validUntil: proxyDoc.validUntil || 0,
          privateKey: proxyDoc.privateKey,
          publicKey: proxyDoc.publicKey,
          csr: '',
        },
      };
    }

    return { success: false, message: `No certificate data found for '${domain}'` };
  }

  /**
   * Import a certificate from ICert-shaped JSON
   */
  private async importCertificate(cert: {
    id: string;
    domainName: string;
    created: number;
    validUntil: number;
    privateKey: string;
    publicKey: string;
    csr: string;
  }): Promise<{ success: boolean; message?: string }> {
    // Validate PEM content
    if (!cert.publicKey || !cert.publicKey.includes('-----BEGIN CERTIFICATE-----')) {
      return { success: false, message: 'Invalid publicKey: must contain a PEM-encoded certificate' };
    }
    if (!cert.privateKey || !cert.privateKey.includes('-----BEGIN')) {
      return { success: false, message: 'Invalid privateKey: must contain a PEM-encoded key' };
    }

    const dcRouter = this.opsServerRef.dcRouterRef;
    const cleanDomain = cert.domainName.replace(/^\*\.?/, '');

    // Save to AcmeCertDoc (SmartAcme-compatible)
    let acmeDoc = await AcmeCertDoc.findByDomain(cleanDomain);
    if (!acmeDoc) {
      acmeDoc = new AcmeCertDoc();
      acmeDoc.domainName = cleanDomain;
    }
    acmeDoc.id = cert.id;
    acmeDoc.created = cert.created;
    acmeDoc.validUntil = cert.validUntil;
    acmeDoc.privateKey = cert.privateKey;
    acmeDoc.publicKey = cert.publicKey;
    acmeDoc.csr = cert.csr || '';
    await acmeDoc.save();

    // Also save to ProxyCertDoc (proxy-cert format)
    let proxyDoc = await ProxyCertDoc.findByDomain(cert.domainName);
    if (!proxyDoc) {
      proxyDoc = new ProxyCertDoc();
      proxyDoc.domain = cert.domainName;
    }
    proxyDoc.publicKey = cert.publicKey;
    proxyDoc.privateKey = cert.privateKey;
    proxyDoc.ca = '';
    proxyDoc.validUntil = cert.validUntil;
    proxyDoc.validFrom = cert.created;
    await proxyDoc.save();

    // Update in-memory status map
    dcRouter.certificateStatusMap.set(cert.domainName, {
      status: 'valid',
      source: 'static',
      expiryDate: cert.validUntil ? new Date(cert.validUntil).toISOString() : undefined,
      issuedAt: cert.created ? new Date(cert.created).toISOString() : undefined,
      routeNames: [],
    });

    return { success: true, message: `Certificate imported for '${cert.domainName}'` };
  }
}
