import { logger } from '../logger.js';
import { AcmeConfigDoc } from '../db/documents/index.js';
import type { IDcRouterOptions } from '../classes.dcrouter.js';
import type { IAcmeConfig } from '../../ts_interfaces/data/acme-config.js';

/**
 * AcmeConfigManager — owns the singleton ACME configuration in the DB.
 *
 * Lifecycle:
 *  - `start()` — loads from the DB; if empty, seeds from legacy constructor
 *    fields (`tls.contactEmail`, `smartProxyConfig.acme.*`) on first boot.
 *  - `getConfig()` — returns the in-memory cached `IAcmeConfig` (or null)
 *  - `updateConfig(args, updatedBy)` — upserts and refreshes the cache
 *
 * Reload semantics: updates take effect on the next dcrouter restart because
 * `SmartAcme` is instantiated once in `setupSmartProxy()`. `renewThresholdDays`
 * applies immediately to the next renewal check. See
 * `ts_web/elements/domains/ops-view-certificates.ts` for the UI warning.
 */
export class AcmeConfigManager {
  private cached: IAcmeConfig | null = null;

  constructor(private options: IDcRouterOptions) {}

  public async start(): Promise<void> {
    logger.log('info', 'AcmeConfigManager: starting');
    let doc = await AcmeConfigDoc.load();

    if (!doc) {
      // First-boot path: seed from legacy constructor fields if present.
      const seed = this.deriveSeedFromOptions();
      if (seed) {
        doc = await this.createSeedDoc(seed);
        logger.log(
          'info',
          `AcmeConfigManager: seeded from constructor legacy fields (accountEmail=${seed.accountEmail}, useProduction=${seed.useProduction})`,
        );
      } else {
        logger.log(
          'info',
          'AcmeConfigManager: no AcmeConfig in DB and no legacy constructor fields — ACME disabled until configured via Domains > Certificates > Settings.',
        );
      }
    } else if (this.deriveSeedFromOptions()) {
      logger.log(
        'warn',
        'AcmeConfigManager: ignoring constructor tls.contactEmail / smartProxyConfig.acme — DB already has AcmeConfigDoc. Manage via Domains > Certificates > Settings.',
      );
    }

    this.cached = doc ? this.toPlain(doc) : null;
    if (this.cached) {
      logger.log(
        'info',
        `AcmeConfigManager: loaded ACME config (accountEmail=${this.cached.accountEmail}, enabled=${this.cached.enabled}, useProduction=${this.cached.useProduction})`,
      );
    }
  }

  public async stop(): Promise<void> {
    this.cached = null;
  }

  /**
   * Returns the current ACME config, or null if not configured.
   * In-memory — does not hit the DB.
   */
  public getConfig(): IAcmeConfig | null {
    return this.cached;
  }

  /**
   * True if there is an enabled ACME config. Used by `setupSmartProxy()` to
   * decide whether to instantiate SmartAcme.
   */
  public hasEnabledConfig(): boolean {
    return this.cached !== null && this.cached.enabled;
  }

  /**
   * Upsert the ACME config. All fields are optional; missing fields are
   * preserved from the existing row (or defaulted if there is no row yet).
   */
  public async updateConfig(
    args: Partial<Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'>>,
    updatedBy: string,
  ): Promise<IAcmeConfig> {
    let doc = await AcmeConfigDoc.load();
    const now = Date.now();

    if (!doc) {
      doc = new AcmeConfigDoc();
      doc.configId = 'acme-config';
      doc.accountEmail = args.accountEmail ?? '';
      doc.enabled = args.enabled ?? true;
      doc.useProduction = args.useProduction ?? true;
      doc.autoRenew = args.autoRenew ?? true;
      doc.renewThresholdDays = args.renewThresholdDays ?? 30;
    } else {
      if (args.accountEmail !== undefined) doc.accountEmail = args.accountEmail;
      if (args.enabled !== undefined) doc.enabled = args.enabled;
      if (args.useProduction !== undefined) doc.useProduction = args.useProduction;
      if (args.autoRenew !== undefined) doc.autoRenew = args.autoRenew;
      if (args.renewThresholdDays !== undefined) doc.renewThresholdDays = args.renewThresholdDays;
    }

    doc.updatedAt = now;
    doc.updatedBy = updatedBy;
    await doc.save();

    this.cached = this.toPlain(doc);
    return this.cached;
  }

  // ==========================================================================
  // Internal helpers
  // ==========================================================================

  /**
   * Build a seed object from the legacy constructor fields. Returns null
   * if the user has not provided any of them.
   *
   * Supports BOTH `tls.contactEmail` (short form) and `smartProxyConfig.acme`
   * (full form). `smartProxyConfig.acme` wins when both are present.
   */
  private deriveSeedFromOptions(): Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'> | null {
    const acme = this.options.smartProxyConfig?.acme;
    const tls = this.options.tls;

    // Prefer the explicit smartProxyConfig.acme block if present.
    if (acme?.accountEmail) {
      return {
        accountEmail: acme.accountEmail,
        enabled: acme.enabled !== false,
        useProduction: acme.useProduction !== false,
        autoRenew: acme.autoRenew !== false,
        renewThresholdDays: acme.renewThresholdDays ?? 30,
      };
    }

    // Fall back to the short tls.contactEmail form.
    if (tls?.contactEmail) {
      return {
        accountEmail: tls.contactEmail,
        enabled: true,
        useProduction: true,
        autoRenew: true,
        renewThresholdDays: 30,
      };
    }

    return null;
  }

  private async createSeedDoc(
    seed: Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'>,
  ): Promise<AcmeConfigDoc> {
    const doc = new AcmeConfigDoc();
    doc.configId = 'acme-config';
    doc.accountEmail = seed.accountEmail;
    doc.enabled = seed.enabled;
    doc.useProduction = seed.useProduction;
    doc.autoRenew = seed.autoRenew;
    doc.renewThresholdDays = seed.renewThresholdDays;
    doc.updatedAt = Date.now();
    doc.updatedBy = 'seed';
    await doc.save();
    return doc;
  }

  private toPlain(doc: AcmeConfigDoc): IAcmeConfig {
    return {
      accountEmail: doc.accountEmail,
      enabled: doc.enabled,
      useProduction: doc.useProduction,
      autoRenew: doc.autoRenew,
      renewThresholdDays: doc.renewThresholdDays,
      updatedAt: doc.updatedAt,
      updatedBy: doc.updatedBy,
    };
  }
}
